[
  {
    "path": ".dockerignore",
    "content": "testdata/\ncmd/maddy/maddy\nmaddy\ntests/maddy.cover\n"
  },
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\nend_of_line = lf\ninsert_final_newline = true\ntrim_trailing_whitespace = true\n\n[*.{scd,go}]\nindent_style = tab\nindent_size = 4\n\n[*.yml]\nindent_style = tab\nindent_size = 2\n"
  },
  {
    "path": ".gitattributes",
    "content": "* text=auto eol=lf\n"
  },
  {
    "path": ".github/CODE_OF_CONDUCT.md",
    "content": "# Code of Merit\n\n**1.** The project creators, lead developers, core team, constitute the managing\nmembers of the project and have final say in every decision of the project,\ntechnical or otherwise, including overruling previous decisions. There are no\nlimitations to this decisional power.\n\n**2.** Contributions are an expected result of your membership on the project.\nDon’t expect others to do your work or help you with your work forever.\n\n**3.** All members have the same opportunities to seek any challenge they want\nwithin the project.\n\n**4.** Authority or position in the project will be proportional to the accrued\ncontribution. Seniority must be earned.\n\n**5.** Software is evolutive: the better implementations must supersede lesser\nimplementations. Technical advantage is the primary evaluation metric.\n\n**6.** This is a space for technical prowess; topics outside of the project will\nnot be tolerated.\n\n**7.** Non technical conflicts will be discussed in a separate space. Disruption\nof the project will not be allowed.\n\n**8.** Individual characteristics, including but not limited to, body, sex,\nsexual preference, race, language, religion, nationality, or political\npreferences are irrelevant in the scope of the project and will not be taken\ninto account concerning your value or that of your contribution to the project.\n\n**9.** Discuss or debate the idea, not the person.\n\n**10.** There is no room for ambiguity: Ambiguity will be met with questioning;\nfurther ambiguity will be met with silence. It is the responsibility of the\noriginator to provide requested context.\n\n**11.** If something is illegal outside the scope of the project, it is illegal\nin the scope of the project. This Code of Merit does not take precedence over\ngoverning law.\n\n**12.** This Code of Merit governs the technical procedures of the project not\nthe activities outside of it.\n\n**13.** Participation on the project equates to agreement of this Code of Merit.\n\n**14.** No objectives beyond the stated objectives of this project are relevant\nto the project. Any intent to deviate the project from its original purpose of\nexistence will constitute grounds for remedial action which may include\nexpulsion from the project.\n\nThis document is the Code of Merit\n(<strike>`http://code-of-merit.org`</strike>), version 1.0.\n"
  },
  {
    "path": ".github/CONTRIBUTING.md",
    "content": "# Contributing Guidelines\n\nOf course, we love our contributors. Thanks for spending time on making maddy\nbetter.\n\n## Reporting bugs\n\n**Issue tracker is meant to be used only if you have a problem or a feature\nrequest. If you just have some questions about maddy - prefer to use the\n[IRC channel](https://webchat.oftc.net/?channels=maddy&uio=MT11bmRlZmluZWQb1).**\n\n- Provide log files, preferably with 'debug' directive set.\n- Provide the exact steps to reproduce the issue.\n- Provide the example message that causes the error, if applicable.\n- \"Too much information is better than not enough information\".\n\nIssues without enough information will be ignored and possibly closed.\nTake some time to be more useful.\n\nSee SECURITY.md for information on how to report vulnerabilities.\n\n## Contributing Code\n\n0. Use common sense.\n1. Learn Git. Especially, what is `git rebase`. We may ask you to use it if\n   needed.\n2. Tell us that you are willing to work on an issue.\n3. Fork the repo. Create a new branch based on `dev`, write your code. Open a\n   PR.\n\nAsk for advice if you are not sure. We don't bite.\n\nmaddy design summary and some recommendations are provided in\n[HACKING.md](../HACKING.md) file.\n\n## Commits\n\n1. Prefix commit message with a package path if it affects only a single\n   package. Omit `internal/` for brevity.\n2. Provide reasoning for details in the source code itself (via comments),\n   provide reasoning for high-level decisions in the commit message.\n3. Make sure every commit builds & passes tests. Otherwise `git bisect` becomes\n   unusable.\n\n## Git workflow\n\n`dev` branch includes the in-development version for the next feature release.\nIt is based on commit of the latest stable release and is merged into `master`\non release via fast-forward. Unlike `master`, `dev` **is not a protected branch\nand may get force-pushes**.\n\n`master` branch contains the latest stable release and is frozen between\nreleases.\n\n`fix-X.Y` are temporary branches containing backported security fixes.\nThey are based on the commit of the corresponding stable release and exist\nwhile the corresponding release is maintained. A `fix-*` branch is not created\nfor the latest release. Changes are added to these branches by cherry-picking\nneeded commits from the `dev` branch.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug-report.md",
    "content": "---\nname: Bug report\nabout: If you think something is broken\ntitle: Bug report\nlabels: bug\nassignees: ''\n\n---\n\n# Describe the bug\n\nWhat do you think is wrong?\n\n# Steps to reproduce\n\n# Log files\n\nUse a service like hastebin.com or attach a file if it is big\n\n# Configuration file\n\nLocated in /etc/maddy/maddy.conf by default, don't forget to remove DB passwords\nand other security-related stuff.\n\n# Environment information\n\n* maddy version: ?\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "contact_links:\n  - name: Questions \n    url: \"https://github.com/foxcpp/maddy/discussions/new?category=q-a\"\n    about: \"Use GitHub discussions for any questions\"\n  - name: IRC channel\n    url: \"https://webchat.oftc.net/?channels=maddy&uio=MT11bmRlZmluZWQb1\"\n    about: \"... or there is also an IRC channel for any discussions\"\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature-request.md",
    "content": "---\nname: Feature request\nabout: If you would like to see a new feature in maddy.\ntitle: Feature request\nlabels: new feature\nassignees: ''\n\n---\n\n# Use case\n\nWhat problem you are trying to solve?\n\nNote alternatives you considered and why they are not useful.\n\n# Your idea for a solution\n\nHow your solution would work in general?\nNote that some overly complicated solutions may be rejected because maddy is\nmeant to be simple.\n\n- [ ] I'm willing to help with the implementation\n"
  },
  {
    "path": ".github/SECURITY.md",
    "content": "# Security Policy\n\n## Supported Versions\n\nTwo latest incompatible releases (e.g. 2.0.0 and 1.9.0).\n\nLatest release gets all bug fixes, features, etc. Previous incompatible release\ngets security fixes and fixes for problems that render software completely\nunusable in certain configurations with no workaround.\n\n## Reporting a Vulnerability\n\nIf you believe the vulnerabilitiy does have a big impact on existing\ndeployments - email `fox.cpp at disroot.org`, put \"[maddy security]\" in the\nSubject.\n\nOtherwise, open a public issue.\n"
  },
  {
    "path": ".github/releases.md",
    "content": "# Release preparation\n\n1. Run linters, fix all simple warnings. If the behavior is intentional - add\n`nolint` comment and explanation. If the warning is non-trviail to fix - open\nan issue.\n```\ngolangci-lint run\n```\n\n2. Run unit tests suite. Verify that all disabled tests are not related to\n   serious problems and have corresponding issue open.\n```\ngo test ./...\n```\n\n3. Run integration tests suite. Verify that all disabled tests are not related\n   to serious problems and have corresponding issue open.\n```\ncd tests/\n./run.sh\n```\n\n4. Write release notes.\n\n5. Create PGP-signed Git tag and push it to GitHub (do not create a \"release\"\n   yet).\n\n5. Use environment configuration from maddy-repro bundle\n   (https://foxcpp.dev/maddy-repro) to build release artifacts.\n\n6. Create detached PGP signatures for artifacts using key\n   3197BBD95137E682A59717B434BB2007081396F4.\n\n7. Create sha256sums file for artifacts.\n\n8. Create release on GitHub using the same text for\n   release notes. Attach signed artifacts and sha256sums file.\n\n9. Build the Docker container and push it to hub.docker.com.\n\n10. Post a message on the sr.ht mailing list.\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: \"Prepare release artifacts\"\n\non:\n  push:\n    tags: [ \"v*\" ]\n\npermissions:\n  id-token: write\n  contents: read\n  attestations: write\n  packages: write\n\njobs:\n  artifact-builder-x86:\n    name: \"Prepare release artifacts (x86)\"\n    if: github.ref_type == 'tag'\n    runs-on: ubuntu-latest\n    container:\n      image: \"alpine:edge\"\n    steps:\n      - uses: actions/checkout@v1 # v2 does not work with containers\n      - name: \"Install build dependencies\"\n        run: |\n          apk add --no-cache gcc go zstd\n      - name: \"Create and package build tree\"\n        run: |\n          ./build.sh --builddir ~/package-output/ --static build\n          ver=$(cat .version)\n          if [ \"v$ver\" != \"${{github.ref_name}}\" ]; then echo \".version does not match the Git tag\"; exit 1; fi\n          mv ~/package-output/ ~/maddy-$ver-x86_64-linux-musl\n          cd ~\n          tar c ./maddy-$ver-x86_64-linux-musl | zstd > ~/maddy-x86_64-linux-musl.tar.zst\n          cd -\n      - name: \"Save source tree\"\n        run: |\n          rm -rf .git\n          ver=$(cat .version)\n          cp -r . ~/maddy-$ver-src\n          cd ~\n          tar c ./maddy-$ver-src | zstd > ~/maddy-src.tar.zst\n          cd -\n      - name: \"Upload source tree\"\n        uses: actions/upload-artifact@v4\n        with:\n          name: maddy-src.tar.zst\n          path: '~/maddy-src.tar.zst'\n          if-no-files-found: error\n      - name: \"Upload binary tree\"\n        uses: actions/upload-artifact@v4\n        with:\n          name: maddy-binary.tar.zst\n          path: '~/maddy-x86_64-linux-musl.tar.zst'\n          if-no-files-found: error\n      - name: \"Generate artifact attestation\"\n        uses: actions/attest-build-provenance@v2\n        with:\n          subject-path: '~/maddy-x86_64-linux-musl.tar.zst'\n  artifact-builder-arm:\n    name: \"Prepare release artifacts (aarch64)\"\n    if: github.ref_type == 'tag'\n    runs-on: ubuntu-22.04-arm\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n      # Building in a Docker container is a workaround for the issue of\n      # JavaScript-based GitHub Actions not being supported in Alpine\n      # containers on the Arm64 platform. Otherwise, we could completely reuse\n      # artifact-builder-x86 as a matrix job by running it on an Arm runner.\n      - name: Build in Docker container\n        run: |\n          # Create Dockerfile for the build\n          cat > Dockerfile << 'EOF'\n          FROM alpine:edge\n          RUN apk add --no-cache gcc go zstd musl-dev scdoc\n          WORKDIR /build\n          COPY . .\n          RUN ./build.sh --builddir /package-output/ --static build && \\\n              ver=$(cat .version) && \\\n              if [ \"v$ver\" != \"${{github.ref_name}}\" ]; then echo \".version does not match the Git tag\"; exit 1; fi && \\\n              mv /package-output/ /maddy-$ver-aarch64-linux-musl && \\\n              cd / && \\\n              tar c ./maddy-$ver-aarch64-linux-musl | zstd > /maddy-aarch64-linux-musl.tar.zst\n          EOF\n          # Build the image, create a temporary container and copy the artifact.\n          docker build -t maddy-builder .\n          container_id=$(docker create maddy-builder)\n          docker cp $container_id:/maddy-aarch64-linux-musl.tar.zst .\n          docker rm $container_id\n      - name: Upload binary tree\n        uses: actions/upload-artifact@v4\n        with:\n          name: maddy-binary-aarch64.tar.zst\n          path: maddy-aarch64-linux-musl.tar.zst\n          if-no-files-found: error\n      - name: \"Generate artifact attestation\"\n        uses: actions/attest-build-provenance@v2\n        with:\n          subject-path: 'maddy-aarch64-linux-musl.tar.zst'\n  docker-builder:\n    name: \"Build & push Docker image\"\n    if: github.ref_type == 'tag'\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n      - name: \"Set up QEMU\"\n        uses: docker/setup-qemu-action@v1\n        with:\n          platforms: arm64\n      - name: \"Set up Docker Buildx\"\n        id: buildx\n        uses: docker/setup-buildx-action@v3\n      - name: \"Login to Docker Hub\"\n        uses: docker/login-action@v3\n        with:\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_PASSWORD }}\n          logout: false\n      - name: \"Login to GitHub Container Registry\"\n        uses: docker/login-action@v3\n        with:\n          registry: \"ghcr.io\"\n          username: ${{ github.repository_owner }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n          logout: false # https://news.ycombinator.com/item?id=28607735\n      - name: \"Generate container metadata\"\n        uses: docker/metadata-action@v5\n        id: meta\n        with:\n          images: |\n            foxcpp/maddy\n            ghcr.io/foxcpp/maddy\n          tags: |\n            type=semver,pattern={{version}}\n            type=semver,pattern={{major}}.{{minor}}\n          labels: |\n            org.opencontainers.image.title=Maddy Mail Server\n            org.opencontainers.image.documentation=https://maddy.email/docker/\n            org.opencontainers.image.url=https://maddy.email\n      - name: \"Build and push\"\n        uses: docker/build-push-action@v6\n        id: docker\n        with:\n          context: .\n          platforms: linux/amd64 #,linux/arm64  Temporary disabled due to SIGSEGV in gcc.\n          file: Dockerfile\n          push: true\n          tags: ${{ steps.meta.outputs.tags }}\n          labels: ${{ steps.meta.outputs.labels }}\n      - name: \"Generate container attestation\"\n        uses: actions/attest-build-provenance@v2\n        with:\n          subject-name: ghcr.io/foxcpp/maddy\n          subject-digest: ${{ steps.docker.outputs.digest }}\n          push-to-registry: true\n\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "name: \"Testing\"\n\non:\n  push:\n    branches: [ master, dev ]\n    tags: [ \"v*\" ]\n  pull_request:\n    branches: [ master, dev ]\n\npermissions:\n  contents: read\n  pull-requests: read\n  checks: write\n\njobs:\n  golangci:\n    name: Lint\n    runs-on: ubuntu-24.04\n    steps:\n      - uses: actions/checkout@v6\n      - uses: actions/setup-go@v6\n        with:\n          go-version-file: 'go.mod'\n      - name: \"Install libpam\"\n        run: |\n          sudo apt-get update\n          sudo apt-get install -y libpam-dev\n      - uses: golangci/golangci-lint-action@v9\n        with:\n          version: v2.11\n  buildsh:\n    name: \"Verify build.sh\"\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n      - uses: actions/setup-go@v6\n        with:\n          go-version-file: 'go.mod'\n      - name: \"Install libpam\"\n        run: |\n          sudo apt-get update\n          sudo apt-get install -y libpam-dev\n      - name: \"Verify build.sh\"\n        run: |\n          ./build.sh\n          ./build.sh --destdir destdir/ install\n          find destdir/\n  test:\n    name: \"Build and test\"\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@v6\n    - uses: actions/setup-go@v6\n      with:\n        go-version-file: 'go.mod'\n    - name: \"Install libpam\"\n      run: |\n        sudo apt-get update\n        sudo apt-get install -y libpam-dev\n    - name: \"Unit & module tests\"\n      run: |\n        go test ./... -coverprofile=coverage.out -covermode=atomic\n    - name: \"Integration tests\"\n      run: |\n        cd tests/\n        ./run.sh\n"
  },
  {
    "path": ".gitignore",
    "content": "# gitignore.io\n*.o\n*.a\n*.so\n_obj\n_test\n*.[568vq]\n[568vq].out\n*.cgo1.go\n*.cgo2.c\n_cgo_defun.c\n_cgo_gotypes.go\n_cgo_export.*\n_testmain.go\n*.exe\n*.exe~\n*.test\n*.prof\n**/.envrc\n**/.DS_Store\n\n# Tests coverage\n*.out\n\n# Compiled binaries\ncmd/maddy/maddy\ncmd/maddy-*-helper/maddy-*-helper\n/maddy\n\n# Man pages\ndocs/man/*.1\ndocs/man/*.5\n\n# Certificates and private keys.\n*.pem\n*.crt\n*.key\n\n# Some directories that may be created during test-runs\n# in repo directory.\ncmd/maddy/*mtasts-cache\ncmd/maddy/*queue\n\nbuild/\n\ntests/maddy.cover\ntests/maddy\n\n.idea/\n"
  },
  {
    "path": ".golangci.yml",
    "content": "version: \"2\"\nlinters:\n  enable:\n  - errcheck\n  - staticcheck\n  - ineffassign\n  - govet\n  - unused\n  - prealloc\n  - unconvert\n  - misspell\n  - whitespace\n  - nakedret\n  - dogsled\n  - copyloopvar\n  - sqlclosecheck\n  - testifylint\n  - rowserrcheck\n  - recvcheck\n  settings:\n    errcheck:\n      disable-default-exclusions: false\nformatters:\n  enable:\n  - goimports\n"
  },
  {
    "path": ".mkdocs.yml",
    "content": "site_name: maddy\n\nrepo_url: https://github.com/foxcpp/maddy\n\ntheme: alb\n\nmarkdown_extensions:\n  - codehilite:\n      guess_lang: false\n\nnav:\n  - faq.md\n  - Tutorials:\n    - tutorials/setting-up.md\n    - tutorials/building-from-source.md\n    - tutorials/alias-to-remote.md\n    - tutorials/pam.md\n  - Release builds: 'https://maddy.email/builds/'\n  - multiple-domains.md\n  - upgrading.md\n  - seclevels.md\n  - docker.md\n  - Reference manual:\n      - reference/modules.md\n      - reference/global-config.md\n      - reference/tls.md\n      - reference/tls-acme.md\n      - Endpoints configuration:\n          - reference/endpoints/imap.md\n          - reference/endpoints/smtp.md\n          - reference/endpoints/openmetrics.md\n      - IMAP storage:\n          - reference/storage/imap-filters.md\n          - reference/storage/imapsql.md\n          - Blob storage:\n            - reference/blob/fs.md\n            - reference/blob/s3.md\n      - reference/smtp-pipeline.md\n      - SMTP targets:\n          - reference/targets/queue.md\n          - reference/targets/remote.md\n          - reference/targets/smtp.md\n      - SMTP checks:\n          - reference/checks/actions.md\n          - reference/checks/dkim.md\n          - reference/checks/spf.md\n          - reference/checks/milter.md\n          - reference/checks/rspamd.md\n          - reference/checks/dnsbl.md\n          - reference/checks/command.md\n          - reference/checks/authorize_sender.md\n          - reference/checks/misc.md\n      - SMTP modifiers:\n          - reference/modifiers/dkim.md\n          - reference/modifiers/envelope.md\n      - Lookup tables (string translation):\n          - reference/table/static.md\n          - reference/table/regexp.md\n          - reference/table/file.md\n          - reference/table/sql_query.md\n          - reference/table/chain.md\n          - reference/table/email_localpart.md\n          - reference/table/email_with_domain.md\n          - reference/table/auth.md\n      - Authentication providers:\n          - reference/auth/pass_table.md\n          - reference/auth/pam.md\n          - reference/auth/shadow.md\n          - reference/auth/external.md\n          - reference/auth/ldap.md\n          - reference/auth/dovecot_sasl.md\n          - reference/auth/plain_separate.md\n          - reference/auth/netauth.md\n      - reference/config-syntax.md\n  - Integration with software:\n      - third-party/dovecot.md\n      - third-party/smtp-servers.md\n      - third-party/rspamd.md\n      - third-party/mailman3.md\n  - Internals:\n    - internals/specifications.md\n    - internals/unicode.md\n    - internals/quirks.md\n    - internals/sqlite.md\n"
  },
  {
    "path": ".version",
    "content": "0.9.4\n"
  },
  {
    "path": "AGENTS.md",
    "content": "# AGENTS.md — Maddy Mail Server\n\n## Architecture\n\nMaddy is a composable all-in-one mail server (MTA/MX/IMAP) written in Go. The core abstraction is the **module system**: every functional component (auth, storage, checks, targets, endpoints) implements `module.Module` from `framework/module/module.go` and registers itself via `module.Register(name, factory)` in an `init()` function.\n\n- **`framework/`** — Stable, reusable packages (config parsing, module interfaces, address handling, error types, logging). Interfaces live here to avoid circular imports.\n- **`internal/`** — All module implementations. Subdirectories map to module roles: `endpoint/` (protocol listeners), `target/` (delivery destinations), `auth/`, `check/` (message inspectors), `modify/` (header modifiers), `storage/`, `table/` (string→string lookups).\n- **`maddy.go`** — Side-effect imports that pull all `internal/` modules into the binary, plus the `Run`/`moduleConfigure`/`RegisterModules` startup sequence.\n- **`cmd/maddy/main.go`** — Thin entrypoint; imports root package for module registration, then calls `maddycli.Run()`.\n\nModules are wired together at runtime via `maddy.conf` configuration. Top-level blocks are lazily initialized through `module.Registry`. The **message pipeline** (`internal/msgpipeline/`) routes messages from endpoints through checks, modifiers, and to delivery targets based on sender/recipient matching rules.\n\n## Build & Test\n\n```sh\n# Build (produces ./build/maddy by default):\n./build.sh build\n\n# Build with specific tags (e.g. for Docker):\n./build.sh --tags \"docker\" build\n\n# Unit tests (standard Go):\ngo test ./...\n\n# Integration tests\ncd tests && ./run.sh\n```\n\nThe build embeds version via `-ldflags -X github.com/foxcpp/maddy.Version=...`. A C compiler is needed for SQLite support (`mattn/go-sqlite3`).\n\n## Adding a New Module\n\n1. Create a package under the appropriate `internal/` subdirectory (e.g. `internal/check/mycheck/`).\n2. Implement `module.Module` plus the relevant role interface (`module.Check`, `module.DeliveryTarget`, `module.PlainAuth`, `module.Table`, etc.) from `framework/module/`.\n3. Register in `init()`: `module.Register(\"check.mycheck\", NewMyCheck)`. Use naming convention: `check.`, `target.`, `auth.`, `table.`, `modify.` prefixes.\n4. Add a blank import `_ \"github.com/foxcpp/maddy/internal/check/mycheck\"` in `maddy.go`.\n5. For checks: use the skeleton at `internal/check/skeleton.go` or `check.RegisterStatelessCheck` (see `internal/check/dns/` for a stateless example).\n\n## Error Handling\n\nUse `framework/exterrors` — not bare `fmt.Errorf`. Errors crossing module boundaries must carry:\n- SMTP status info via `exterrors.SMTPError{Code, EnhancedCode, Message, CheckName/TargetName}`\n- Temporary flag via `exterrors.WithTemporary`\n- Module name field\n\nKeep SMTP error messages generic (no server config details). Use `exterrors.WithFields` for unexpected errors. See `HACKING.md` for full guidelines.\n\n## Key Conventions\n\n- **No shared state between messages** — check/modifier code runs in parallel across messages.\n- **Panic recovery** — any goroutine you spawn must recover panics to avoid crashing the server.\n- **Address normalization** — domain parts must be U-labels with NFC normalization and case-folding. Use `framework/address.CleanDomain`.\n- **Configuration parsing** — modules receive config via `config.Map` in their `Configure` method. See `framework/config/` and existing modules for the pattern.\n- **Logging** — use `framework/log.Logger`, not `log` stdlib. Per-delivery loggers via `target.DeliveryLogger(...)`.\n\n"
  },
  {
    "path": "COPYING",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  To protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\n  Developers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\n  For the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\n  Some devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\n  Finally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Use with the GNU Affero General Public License.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If the program does terminal interaction, make it output a short\nnotice like this when it starts in an interactive mode:\n\n    <program>  Copyright (C) <year>  <name of author>\n    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, your program's commands\nmight be different; for a GUI interface, you would use an \"about box\".\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU GPL, see\n<https://www.gnu.org/licenses/>.\n\n  The GNU General Public License does not permit incorporating your program\ninto proprietary programs.  If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications with\nthe library.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.  But first, please read\n<https://www.gnu.org/licenses/why-not-lgpl.html>.\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM golang:1.23-alpine AS build-env\n\nARG ADDITIONAL_BUILD_TAGS=\"\"\n\nRUN set -ex && \\\n    apk upgrade --no-cache --available && \\\n    apk add --no-cache build-base\n\nWORKDIR /maddy\n\nCOPY go.mod go.sum ./\nRUN go mod download\n\nCOPY . ./\nRUN mkdir -p /pkg/data && \\\n    cp maddy.conf.docker /pkg/data/maddy.conf && \\\n    ./build.sh --builddir /tmp --destdir /pkg/ --tags \"docker ${ADDITIONAL_BUILD_TAGS}\" build install\n\nFROM alpine:3.21.2\nLABEL maintainer=\"fox.cpp@disroot.org\"\nLABEL org.opencontainers.image.source=https://github.com/foxcpp/maddy\n\nRUN set -ex && \\\n    apk upgrade --no-cache --available && \\\n    apk --no-cache add ca-certificates\nCOPY --from=build-env /pkg/data/maddy.conf /data/maddy.conf\nCOPY --from=build-env /pkg/usr/local/bin/maddy /bin/\n\nEXPOSE 25 143 993 587 465\nVOLUME [\"/data\"]\nENTRYPOINT [\"/bin/maddy\", \"-config\", \"/data/maddy.conf\"]\nCMD [\"run\"]\n"
  },
  {
    "path": "HACKING.md",
    "content": "## Design goals\n\n- **Make it easy to deploy.**\n  Minimal configuration changes should be required to get a typical mail server\n  running. Though, it is important to avoid making guesses for a\n  \"zero-configuration\". A wrong guess is worse than no guess.\n\n- **Provide 80% of needed components.**\n  E-mail has evolved into a huge mess. With a single package to do one thing, it\n  quickly turns into a maintenance nightmare. Put all stuff mail server\n  typically needs into a single package. Though, leave controversial or highly\n  opinionated stuff out, don't force people to do things our way\n  (see next point).\n\n- **Interoperate with existing software.**\n  Implement (de-facto) standard protocols not only for clients but also for\n  various server-side helper software (content filters, etc).\n\n- **Be secure but interoperable.**\n  Verify DKIM signatures by default, use DMRAC policies by default, etc. This\n  makes default setup as secure as possible while maintaining reasonable\n  interoperability. Though, users can configure maddy to be stricter.\n\n- **Achieve flexibility through composability.**\n  Allow connecting components in arbitrary ways instead of restricting users to\n  predefined templates.\n\n- **Use Go concurrency features to the full extent.**\n  Do as much I/O as possible in parallel to minimize latencies. It is silly to\n  not do so when it is possible.\n\n## Design summary\n\nHere is a summary of how things are organized in maddy in general. It explains\nthings from the developer perspective and is meant to be used as an\nintroduction by the new developers/contributors. It is recommended to read\nuser documentation to understand how things work from the user perspective as\nwell.\n\n- User documentation: [maddy.conf(5)](docs/man/maddy.5.scd)\n- Design rationale: [Comments on design (Wiki)][1]\n\nThere are components called \"modules\". They are represented by objects\nimplementing the module.Module interface. Each module gets its unique name.\nThe function used to create a module instance is saved with this name as a key\ninto the global map called \"modules registry\".\n\nWhenever module needs another module for some functionality, it references it\nusing a configuration directive with a matcher that internally calls\n`modconfig.ModuleFromNode`. That function looks up the module \"constructor\" in\nthe registry, calls it with corresponding arguments, checks whether the\nreturned module satisfies the needed interfaces and then initializes it.\n\nAlternatively, if configuration uses &-syntax to reference existing\nconfiguration block, `ModuleFromNode` simply looks it up in the global instances\nregistry. All modules defined the configuration as a separate top-level blocks\nare created before main initialization and are placed in the instances registry\nwhere they can be looked up as mentioned before.\n\nTop-level defined module instances are initialized (`Init` method) lazily as\nthey are required by other modules. 'smtp' and 'imap' modules follow a special\ninitialization path, so they are always initialized directly.\n\n## Error handling\n\nFamiliarize yourself with the `github.com/foxcpp/maddy/framework/exterrors`\npackage and make sure you have the following for returned errors:\n- SMTP status information (smtp\\_code, smtp\\_enchcode, smtp\\_msg fields)\n  - SMTP message text should contain a generic description of the error\n    condition without any details to prevent accidental disclosure of the\n    server configuration details.\n- `Temporary() == true` for temporary errors (see `exterrors.WithTemporary`)\n- Field that includes the module name\n\nThe easiest way to get all of these is to use `exterrors.SMTPError`.\nPut the original error into the `Err` field, so it can be inspected using\n`errors.Is`, `errors.Unwrap`, etc. Put the module name into `CheckName` or\n`TargetName`. Add any additional context information using the `Misc` field.\nNote, the SMTP status code overrides the result of `exterrors.IsTemporary()`\nfor that error object, so set it using `exterrors.SMTPCode` that uses\n`IsTemporary` to select between two codes.\n\nIf the error you are wrapping contains details in its structure fields (like\n`*net.OpError`) - copy these values into `Misc` map, put the underlying error\nobject (`net.OpError.Err`, for example) into the `Err` field.\nAvoid using `Reason` unless you are sure you can provide the error message\nbetter than the `Err.Error()` or `Err` is `nil`.\n\nDo not attempt to add a SMTP status information for every single possible\nerror. Use `exterrors.WithFields` with basic information for errors you don't\nexpect. The SMTP client will get the \"Internal server error\" message and this\nis generally the right thing to do on unexpected errors.\n\n### Goroutines and panics\n\nIf you start any goroutines - make sure to catch panics to make sure severe\nbugs will not bring the whole server down.\n\n## Adding a check\n\n\"Check\" is a module that inspects the message and flags it as spam or rejects\nit altogether based on some condition.\n\nThe skeleton for the stateful check module can be found in\n`internal/check/skeleton.go`.  Throw it into a file in\n`internal/check/check_name` directory and start ~~breaking~~ extending it.\n\nIf you don't need any per-message state, you can use `StatelessCheck` wrapper.\nSee `check/dns` directory for a working example.\n\nHere are some guidelines to make sure your check works well:\n- RTFM, docs will tell you about any caveats.\n- Don't share any state _between_ messages, your code will be executed in\n  parallel.\n- Use `github.com/foxcpp/maddy/check.FailAction` to select behavior on check\n  failures. See other checks for examples on how to use it.\n- You can assume that order of check functions execution is as follows:\n  `CheckConnection`, `CheckSender`, `CheckRcpt`, `CheckBody`.\n\n## Adding a modifier\n\n\"Modifier\" is a module that can modify some parts of the message data.\n\nNote, currently this is not possible to modify the body contents, only header\ncan be modified.\n\nStructure of the modifier implementation is similar to the structure of check\nimplementation, check `modify/replace\\_addr.go` for a working example.\n\n[1]: https://github.com/foxcpp/maddy/wiki/Dev:-Comments-on-design\n"
  },
  {
    "path": "README.md",
    "content": "Maddy Mail Server\n=====================\n> Composable all-in-one mail server.\n\nMaddy Mail Server implements all functionality required to run a e-mail\nserver. It can send messages via SMTP (works as MTA), accept messages via SMTP\n(works as MX) and store messages while providing access to them via IMAP.\nIn addition to that it implements auxiliary protocols that are mandatory\nto keep email reasonably secure (DKIM, SPF, DMARC, DANE, MTA-STS).\n\nIt replaces Postfix, Dovecot, OpenDKIM, OpenSPF, OpenDMARC and more with one\ndaemon with uniform configuration and minimal maintenance cost.\n\n**Note:** IMAP storage is \"beta\". If you are looking for stable and\nfeature-packed implementation you may want to use Dovecot instead. maddy still\ncan handle message delivery business.\n\n[![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)\n[![Issues tracker](https://img.shields.io/github/issues/foxcpp/maddy?style=flat-square)](https://github.com/foxcpp/maddy)\n\n* [Setup tutorial](https://maddy.email/tutorials/setting-up/)\n* [Documentation](https://maddy.email/)\n\n* [IRC channel](https://webchat.oftc.net/?channels=maddy&uio=MT11bmRlZmluZWQb1)\n* [Mailing list](https://lists.sr.ht/~foxcpp/maddy)\n"
  },
  {
    "path": "build.sh",
    "content": "#!/bin/sh\n\ndestdir=/\nbuilddir=\"$PWD/build\"\nprefix=/usr/local\nversion=\nstatic=0\nif [ \"${GOFLAGS}\" = \"\" ]; then\n\tGOFLAGS=\"-trimpath\" # set some flags to avoid passing \"\" to go\nfi\n\nprint_help() {\n\tcat >&2 <<EOF\nUsage:\n\t./build.sh [options] {build,install}\n\nScript to build, package or install Maddy Mail Server.\n\nOptions:\n    -h, --help              guess!\n    --builddir              directory to build in (default: $builddir)\n\nOptions for ./build.sh build:\n    --static                build static self-contained executables (musl-libc recommended)\n    --tags <tags>           build tags to use\n    --version <version>     version tag to embed into executables (default: auto-detect)\n\nAdditional flags for \"go build\" can be provided using GOFLAGS environment variable.\n\nOptions for ./build.sh install:\n    --prefix <path>         installation prefix (default: $prefix)\n    --destdir <path>        system root (default: $destdir)\nEOF\n}\n\nwhile :; do\n\tcase \"$1\" in\n\t\t-h|--help)\n\t\t   print_help\n\t\t   exit\n\t\t   ;;\n\t\t--builddir)\n\t\t   shift\n\t\t   builddir=\"$1\"\n\t\t   ;;\n\t\t--prefix)\n\t\t   shift\n\t\t   prefix=\"$1\"\n\t\t   ;;\n\t\t--destdir)\n\t\t\tshift\n\t\t\tdestdir=\"$1\"\n\t\t\t;;\n\t\t--version)\n\t\t\tshift\n\t\t\tversion=\"$1\"\n\t\t\t;;\n\t\t--static)\n\t\t\tstatic=1\n\t\t\t;;\n\t\t--tags)\n\t\t\tshift\n\t\t\ttags=\"$1\"\n\t\t\t;;\n\t\t--)\n\t\t\tbreak\n\t\t\tshift\n\t\t\t;;\n\t\t-?*)\n\t\t\techo \"Unknown option: ${1}. See --help.\" >&2\n\t\t\texit 2\n\t\t\t;;\n\t\t*)\n\t\t\tbreak\n\tesac\n\tshift\ndone\n\nconfigdir=\"${destdir}etc/maddy\"\n\nif [ \"$version\" = \"\" ]; then\n\tversion=unknown\n\tif [ -e .version ]; then\n\t\tversion=\"$(cat .version)\"\n\tfi\n\tif [ -e .git ] && command -v git 2>/dev/null >/dev/null; then\n\t\tversion=\"${version}+$(git rev-parse --short HEAD)\"\n\tfi\nfi\n\nset -e\n\nbuild_man_pages() {\n\tset +e\n\tif ! command -v scdoc >/dev/null 2>/dev/null; then\n\t\techo '-- [!] No scdoc utility found. Skipping man pages building.' >&2\n\t\tset -e\n\t\treturn\n\tfi\n\tset -e\n\n\techo '-- Building man pages...' >&2\n\n\tmkdir -p \"${builddir}/man\"\n\tfor f in ./docs/man/*.1.scd; do\n\t\tscdoc < \"$f\" > \"${builddir}/man/$(basename \"$f\" .scd)\"\n\tdone\n}\n\nbuild() {\n\tmkdir -p \"${builddir}\"\n\techo \"-- Version: ${version}\" >&2\n\tif [ \"$(go env CC)\" = \"\" ]; then\n        echo '-- [!] No C compiler available. maddy will be built without SQLite3 support and default configuration will be unusable.' >&2\n    fi\n\n\tif [ \"$static\" -eq 1 ]; then\n\t\techo \"-- Building main server executable...\" >&2\n\t\t# This is literally impossible to specify this line of arguments as part of ${GOFLAGS}\n\t\t# using only POSIX sh features (and even with Bash extensions I can't figure it out).\n\t\tgo build -trimpath -buildmode pie -tags \"$tags osusergo netgo static_build\" \\\n\t\t\t-ldflags \"-extldflags '-fno-PIC -static' -X \\\"github.com/foxcpp/maddy.Version=${version}\\\"\" \\\n\t\t\t-o \"${builddir}/maddy\" ${GOFLAGS} ./cmd/maddy\n\telse\n\t\techo \"-- Building main server executable...\" >&2\n\t\tgo build -tags \"$tags\" -trimpath -ldflags=\"-X \\\"github.com/foxcpp/maddy.Version=${version}\\\"\" -o \"${builddir}/maddy\" ${GOFLAGS} ./cmd/maddy\n\tfi\n\n\tbuild_man_pages\n\n\techo \"-- Copying misc files...\" >&2\n\n\tmkdir -p \"${builddir}/systemd\"\n\tcp dist/systemd/*.service \"${builddir}/systemd/\"\n\tcp maddy.conf \"${builddir}/maddy.conf\"\n}\n\ninstall() {\n\techo \"-- Installing built files...\" >&2\n\n\tcommand install -m 0755 -d \"${destdir}/${prefix}/bin/\"\n\tcommand install -m 0755 \"${builddir}/maddy\" \"${destdir}/${prefix}/bin/\"\n\tcommand ln -sf maddy \"${destdir}/${prefix}/bin/maddyctl\"\n\tcommand install -m 0755 -d \"${configdir}\"\n\n\n\t# We do not want to overwrite existing configuration.\n\t# If the file exists, then save it with .default suffix and warn user.\n\tif [ ! -e \"${configdir}/maddy.conf\" ]; then\n\t\tcommand install -m 0644 ./maddy.conf \"${configdir}/maddy.conf\"\n\telse\n\t\techo \"-- [!] Configuration file ${configdir}/maddy.conf exists, saving to ${configdir}/maddy.conf.default\" >&2\n\t\tcommand install -m 0644 ./maddy.conf \"${configdir}/maddy.conf.default\"\n\tfi\n\n\t# Attempt to install systemd units only for Linux.\n\t# Check is done using GOOS instead of uname -s to account for possible\n\t# package cross-compilation.\n\t# Though go command might be unavailable if build.sh is run\n\t# with sudo and go installation is user-specific, so fallback\n\t# to using uname -s in the end.\n\tset +e\n\tif command -v go >/dev/null 2>/dev/null; then\n\t\tset -e\n\t\tif [ \"$(go env GOOS)\" = \"linux\" ]; then\n\t\t\tcommand install -m 0755 -d \"${destdir}/${prefix}/lib/systemd/system/\"\n\t\t\tcommand install -m 0644 \"${builddir}\"/systemd/*.service \"${destdir}/${prefix}/lib/systemd/system/\"\n\t\tfi\n\telse\n\t\tset -e\n\t\tif [ \"$(uname -s)\" = \"Linux\" ]; then\n\t\t\tcommand install -m 0755 -d \"${destdir}/${prefix}/lib/systemd/system/\"\n\t\t\tcommand install -m 0644 \"${builddir}\"/systemd/*.service \"${destdir}/${prefix}/lib/systemd/system/\"\n\t\tfi\n\tfi\n\n\tif [ -e \"${builddir}\"/man ]; then\n\t\tcommand install -m 0755 -d \"${destdir}/${prefix}/share/man/man1/\"\n\t\tfor f in \"${builddir}\"/man/*.1; do\n\t\t\tcommand install -m 0644 \"$f\" \"${destdir}/${prefix}/share/man/man1/\"\n\t\tdone\n\tfi\n}\n\n# Old build.sh compatibility\ninstall_pkg() {\n\techo \"-- [!] Replace 'install_pkg' with 'install' in build.sh invocation\" >&2\n\tinstall\n}\npackage() {\n\techo \"-- [!] Replace 'package' with 'build' in build.sh invocation\" >&2\n\tbuild\n}\n\nif [ $# -eq 0 ]; then\n\tbuild\nelse\n\tfor arg in \"$@\"; do\n\t\teval \"$arg\"\n\tdone\nfi\n"
  },
  {
    "path": "cmd/README.md",
    "content": "maddy executables\n-------------------\n\n### maddy\n\nMain server executable.\n\n### maddy-pam-helper, maddy-shadow-helper\n\nUtilities compatible with the auth.external module that call libpam or read\n/etc/shadow on Unix systems.\n"
  },
  {
    "path": "cmd/maddy/main.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage main\n\nimport (\n\t_ \"github.com/foxcpp/maddy\"\n\tmaddycli \"github.com/foxcpp/maddy/internal/cli\"\n\t_ \"github.com/foxcpp/maddy/internal/cli/ctl\"\n)\n\nfunc main() {\n\tmaddycli.Run()\n}\n"
  },
  {
    "path": "cmd/maddy-pam-helper/README.md",
    "content": "## maddy-pam-helper\n\nExternal setuid binary for interaction with shadow passwords database or other\nprivileged objects necessary to run PAM authentication.\n\n### Building\n\nIt is really easy to build it using any GCC:\n```\ngcc pam.c main.c -lpam -o maddy-pam-helper\n```\n\nYes, it is not a Go binary.\n\n\n### Installation\n\nmaddy-pam-helper is kinda dangerous binary and should not be allowed to be\nexecuted by everybody but maddy's user. At the same moment it needs to have\naccess to read-protected files. For this reason installation should be done\nvery carefully to make sure to not introduce any security \"holes\".\n\n#### First method\n\n```shell\nchown maddy: /usr/bin/maddy-pam-helper\nchmod u+x,g-x,o-x /usr/bin/maddy-pam-helper\n```\n\nAlso maddy-pam-helper needs access to /etc/shadow, one of the ways to provide\nit is to set file capability CAP_DAC_READ_SEARCH:\n```\nsetcap cap_dac_read_search+ep /usr/bin/maddy-pam-helper\n```\n\n#### Second method\n\nAnother, less restrictive is to make it setuid-root (assuming you have both maddy user and group):\n```\nchown root:maddy /usr/bin/maddy-pam-helper\nchmod u+xs,g+x,o-x /usr/bin/maddy-pam-helper\n```\n\n#### Third method\n\nThe best way actually is to create `shadow` group and grant access to\n/etc/shadow to it and then make maddy-pam-helper setgid-shadow:\n```\ngroupadd shadow\nchown :shadow /etc/shadow\nchmod g+r /etc/shadow\nchown maddy:shadow /usr/bin/maddy-pam-helper\nchmod u+x,g+xs /usr/bin/maddy-pam-helper\n```\n\nPick what works best for you.\n\n### PAM service\n\nmaddy-pam-helper uses custom service instead of pretending to be su or sudo.\nBecause of this you should configure PAM to accept it.\n\nMinimal example using local passwd/shadow database for authentication can be\nfound in [maddy.conf][maddy.conf] file.\nIt should be put into /etc/pam.d/maddy.\n"
  },
  {
    "path": "cmd/maddy-pam-helper/maddy.conf",
    "content": "#%PAM-1.0\nauth\trequired\tpam_unix.so\naccount\trequired\tpam_unix.so\n"
  },
  {
    "path": "cmd/maddy-pam-helper/main.c",
    "content": "#define _POSIX_C_SOURCE 200809L\n#include <stdio.h>\n#include <stdlib.h>\n#include <security/pam_appl.h>\n#include \"pam.h\"\n\n/*\nI really doubt it is a good idea to bring Go to the binary whose primary task\nis to call libpam using CGo anyway.\n*/\n\nint run(void) {\n    char *username = NULL, *password = NULL;\n    size_t username_buf_len = 0, password_buf_len = 0;\n\n    ssize_t username_len = getline(&username, &username_buf_len, stdin);\n    if (username_len < 0) {\n        perror(\"getline username\");\n        return 2;\n    }\n\n    ssize_t password_len = getline(&password, &password_buf_len, stdin);\n    if (password_len < 0) {\n        perror(\"getline password\");\n        return 2;\n    }\n\n    // Cut trailing \\n.\n    if (username_len > 0) {\n        username[username_len - 1] = 0;\n    }\n    if (password_len > 0) {\n        password[password_len - 1] = 0;\n    }\n\n    struct error_obj err = run_pam_auth(username, password);\n    if (err.status != 0) {\n        if (err.status == 2) {\n            fprintf(stderr, \"%s: %s\\n\", err.func_name, err.error_msg);\n        }\n        return err.status;\n    }\n\n    return 0;\n}\n\n#ifndef CGO\nint main() {\n    return run();\n}\n#endif\n"
  },
  {
    "path": "cmd/maddy-pam-helper/main.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage main\n\n/*\n#cgo LDFLAGS: -lpam\n#cgo CFLAGS: -DCGO -Wall -Wextra -Werror -Wno-unused-parameter -Wno-error=unused-parameter -Wpedantic -std=c99\nextern int run();\n*/\nimport \"C\"\nimport \"os\"\n\n/*\nApparently, some people would not want to build it manually by calling GCC.\nHere we do it for them. Not going to tell them that resulting file is 800KiB\nbigger than one built using only C compiler.\n*/\n\nfunc main() {\n\ti := int(C.run())\n\tos.Exit(i)\n}\n"
  },
  {
    "path": "cmd/maddy-pam-helper/pam.c",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2022 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\n#define _POSIX_C_SOURCE 200809L\n#include <stdio.h>\n#include <stdlib.h>\n#include <string.h>\n#include <security/pam_appl.h>\n#include \"pam.h\"\n\nstatic int conv_func(int num_msg, const struct pam_message **msg, struct pam_response **resp, void *appdata_ptr) {\n    struct pam_response *reply = malloc(sizeof(struct pam_response));\n    if (reply == NULL) {\n        return PAM_CONV_ERR;\n    }\n\n    char* password_cpy = malloc(strlen((char*)appdata_ptr)+1);\n    if (password_cpy == NULL) {\n        return PAM_CONV_ERR;\n    }\n    memcpy(password_cpy, (char*)appdata_ptr, strlen((char*)appdata_ptr)+1);\n\n    reply->resp = password_cpy;\n    reply->resp_retcode = 0;\n\n    // PAM frees pam_response for us.\n    *resp = reply;\n\n    return PAM_SUCCESS;\n}\n\nstruct error_obj run_pam_auth(const char *username, char *password) {\n    const struct pam_conv local_conv = { conv_func, password };\n    pam_handle_t *local_auth = NULL;\n    int status = pam_start(\"maddy\", username, &local_conv, &local_auth);\n    if (status != PAM_SUCCESS) {\n        struct error_obj ret_val;\n        ret_val.status = 2;\n        ret_val.func_name = \"pam_start\";\n        ret_val.error_msg = pam_strerror(local_auth, status);\n        return ret_val;\n    }\n\n    status = pam_authenticate(local_auth, PAM_SILENT|PAM_DISALLOW_NULL_AUTHTOK);\n    if (status != PAM_SUCCESS) {\n        struct error_obj ret_val;\n        if (status == PAM_AUTH_ERR || status == PAM_USER_UNKNOWN) {\n            ret_val.status = 1;\n        } else {\n            ret_val.status = 2;\n        }\n        ret_val.func_name = \"pam_authenticate\";\n        ret_val.error_msg = pam_strerror(local_auth, status);\n        return ret_val;\n    }\n\n    status = pam_acct_mgmt(local_auth, PAM_SILENT|PAM_DISALLOW_NULL_AUTHTOK);\n    if (status != PAM_SUCCESS) {\n        struct error_obj ret_val;\n        if (status == PAM_AUTH_ERR || status == PAM_USER_UNKNOWN || status == PAM_NEW_AUTHTOK_REQD) {\n            ret_val.status = 1;\n        } else {\n            ret_val.status = 2;\n        }\n        ret_val.func_name = \"pam_acct_mgmt\";\n        ret_val.error_msg = pam_strerror(local_auth, status);\n        return ret_val;\n    }\n\n    status = pam_end(local_auth, status);\n    if (status != PAM_SUCCESS) {\n        struct error_obj ret_val;\n        ret_val.status = 2;\n        ret_val.func_name = \"pam_end\";\n        ret_val.error_msg = pam_strerror(local_auth, status);\n        return ret_val;\n    }\n\n    struct error_obj ret_val;\n    ret_val.status = 0;\n    ret_val.func_name = NULL;\n    ret_val.error_msg = NULL;\n    return ret_val;\n}\n\n"
  },
  {
    "path": "cmd/maddy-pam-helper/pam.h",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\n#pragma once\n\nstruct error_obj {\n    int status;\n    const char* func_name;\n    const char* error_msg;\n};\n\nstruct error_obj run_pam_auth(const char *username, char *password);\n"
  },
  {
    "path": "cmd/maddy-shadow-helper/README.md",
    "content": "## maddy-shadow-helper\n\nExternal helper binary for interaction with shadow passwords database.\nUnlike maddy-pam-helper it supports only local shadow database but it does\nnot have any C dependencies.\n\n### Installation\n\nmaddy-shadow-helper is kinda dangerous binary and should not be allowed to be\nexecuted by everybody but maddy's user. At the same moment it needs to have\naccess to read-protected files. For this reason installation should be done\nvery carefully to make sure to not introduce any security \"holes\".\n\n#### First method\n\n```shell\nchown maddy: /usr/bin/maddy-shadow-helper\nchmod u+x,g-x,o-x /usr/bin/maddy-shadow-helper\n```\n\nAlso maddy-shadow-helper needs access to /etc/shadow, one of the ways to provide\nit is to set file capability CAP_DAC_READ_SEARCH:\n```\nsetcap cap_dac_read_search+ep /usr/bin/maddy-shadow-helper\n```\n\n#### Second method\n\nAnother, less restrictive is to make it setuid-root (assuming you have both maddy user and group):\n```\nchown root:maddy /usr/bin/maddy-shadow-helper\nchmod u+xs,g+x,o-x /usr/bin/maddy-shadow-helper\n```\n\n#### Third method\n\nThe best way actually is to create `shadow` group and grant access to\n/etc/shadow to it and then make maddy-shadow-helper setgid-shadow:\n```\ngroupadd shadow\nchown :shadow /etc/shadow\nchmod g+r /etc/shadow\nchown maddy:shadow /usr/bin/maddy-shadow-helper\nchmod u+x,g+xs /usr/bin/maddy-shadow-helper\n```\n\nPick what works best for you.\n"
  },
  {
    "path": "cmd/maddy-shadow-helper/main.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage main\n\nimport (\n\t\"bufio\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/foxcpp/maddy/internal/auth/shadow\"\n)\n\nfunc main() {\n\tscnr := bufio.NewScanner(os.Stdin)\n\n\tif !scnr.Scan() {\n\t\tfmt.Fprintln(os.Stderr, scnr.Err())\n\t\tos.Exit(2)\n\t}\n\tusername := scnr.Text()\n\n\tif !scnr.Scan() {\n\t\tfmt.Fprintln(os.Stderr, scnr.Err())\n\t\tos.Exit(2)\n\t}\n\tpassword := scnr.Text()\n\n\tent, err := shadow.Lookup(username)\n\tif err != nil {\n\t\tif errors.Is(err, shadow.ErrNoSuchUser) {\n\t\t\tos.Exit(1)\n\t\t}\n\t\tfmt.Fprintln(os.Stderr, err)\n\t\tos.Exit(2)\n\t}\n\n\tif !ent.IsAccountValid() {\n\t\tfmt.Fprintln(os.Stderr, \"account is expired\")\n\t\tos.Exit(1)\n\t}\n\n\tif !ent.IsPasswordValid() {\n\t\tfmt.Fprintln(os.Stderr, \"password is expired\")\n\t\tos.Exit(1)\n\t}\n\n\tif err := ent.VerifyPassword(password); err != nil {\n\t\tif errors.Is(err, shadow.ErrWrongPassword) {\n\t\t\tos.Exit(1)\n\t\t}\n\t\tfmt.Fprintln(os.Stderr, err)\n\t\tos.Exit(1)\n\t}\n}\n"
  },
  {
    "path": "config.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage maddy\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/log\"\n)\n\n/*\nConfig matchers for module interfaces.\n*/\n\n// logOut structure wraps log.Output and preserves\n// configuration directive it was constructed from, allowing\n// dynamic reinitialization for purposes of log file rotation.\ntype logOut struct {\n\targs []string\n\tlog.Output\n}\n\nfunc logOutput(_ *config.Map, node config.Node) (interface{}, error) {\n\tif len(node.Args) == 0 {\n\t\treturn nil, config.NodeErr(node, \"expected at least 1 argument\")\n\t}\n\tif len(node.Children) != 0 {\n\t\treturn nil, config.NodeErr(node, \"can't declare block here\")\n\t}\n\n\treturn LogOutputOption(node.Args)\n}\n\nfunc LogOutputOption(args []string) (log.Output, error) {\n\touts := make([]log.Output, 0, len(args))\n\tfor i, arg := range args {\n\t\tswitch arg {\n\t\tcase \"stderr\":\n\t\t\touts = append(outs, log.WriterOutput(os.Stderr, false))\n\t\tcase \"stderr_ts\":\n\t\t\touts = append(outs, log.WriterOutput(os.Stderr, true))\n\t\tcase \"syslog\":\n\t\t\tsyslogOut, err := log.SyslogOutput()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to connect to syslog daemon: %v\", err)\n\t\t\t}\n\t\t\touts = append(outs, syslogOut)\n\t\tcase \"off\":\n\t\t\tif len(args) != 1 {\n\t\t\t\treturn nil, errors.New(\"'off' can't be combined with other log targets\")\n\t\t\t}\n\t\t\treturn log.NopOutput{}, nil\n\t\tdefault:\n\t\t\t// log file paths are converted to absolute to make sure\n\t\t\t// we will be able to recreate them in right location\n\t\t\t// after changing working directory to the state dir.\n\t\t\tabsPath, err := filepath.Abs(arg)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\t// We change the actual argument, so logOut object will\n\t\t\t// keep the absolute path for reinitialization.\n\t\t\targs[i] = absPath\n\n\t\t\tw, err := os.OpenFile(absPath, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0o666)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to create log file: %v\", err)\n\t\t\t}\n\n\t\t\touts = append(outs, log.WriteCloserOutput(w, true))\n\t\t}\n\t}\n\n\tif len(outs) == 1 {\n\t\treturn logOut{args, outs[0]}, nil\n\t}\n\treturn logOut{args, log.MultiOutput(outs...)}, nil\n}\n\nfunc defaultLogOutput() (interface{}, error) {\n\treturn nil, nil\n}\n\nfunc reinitLogging() {\n\tout, ok := log.DefaultLogger.Out.(logOut)\n\tif !ok {\n\t\tlog.Println(\"Can't reinitialize logger because it was replaced before, this is a bug\")\n\t\treturn\n\t}\n\n\tnewOut, err := LogOutputOption(out.args)\n\tif err != nil {\n\t\tlog.Println(\"Can't reinitialize logger:\", err)\n\t\treturn\n\t}\n\n\tif err := out.Close(); err != nil {\n\t\tlog.Println(\"Can't close old logger:\", err)\n\t}\n\n\tlog.DefaultLogger.Out = newOut\n}\n"
  },
  {
    "path": "contrib/README.md",
    "content": "# Community contributed resources\n\nDisclaimer: Nothing inside subdirectories here is directly supported by Maddy\nMail Server maintainers. Some community members may be able to help you or not.\n\n- Kubernetes helm chart is maintained by @acim.\n"
  },
  {
    "path": "contrib/kubernetes/chart/.helmignore",
    "content": "# Patterns to ignore when building packages.\n# This supports shell glob matching, relative path matching, and\n# negation (prefixed with !). Only one pattern per line.\n.DS_Store\n# Common VCS dirs\n.git/\n.gitignore\n.bzr/\n.bzrignore\n.hg/\n.hgignore\n.svn/\n# Common backup files\n*.swp\n*.bak\n*.tmp\n*.orig\n*~\n# Various IDEs\n.project\n.idea/\n*.tmproj\n.vscode/\n"
  },
  {
    "path": "contrib/kubernetes/chart/Chart.yaml",
    "content": "apiVersion: v2\nname: maddy\ndescription: A Helm chart for Kubernetes\n\n# A chart can be either an 'application' or a 'library' chart.\n#\n# Application charts are a collection of templates that can be packaged into versioned archives\n# to be deployed.\n#\n# Library charts provide useful utilities or functions for the chart developer. They're included as\n# a dependency of application charts to inject those utilities and functions into the rendering\n# pipeline. Library charts do not define any templates and therefore cannot be deployed.\ntype: application\n\n# This is the chart version. This version number should be incremented each time you make changes\n# to the chart and its templates, including the app version.\n# Versions are expected to follow Semantic Versioning (https://semver.org/)\nversion: 0.2.6\n\n# This is the version number of the application being deployed. This version number should be\n# incremented each time you make changes to the application. Versions are not expected to\n# follow Semantic Versioning. They should reflect the version the application is using.\nappVersion: 0.4.0\n"
  },
  {
    "path": "contrib/kubernetes/chart/README.md",
    "content": "# maddy Helm chart for Kubernetes\n\n![Version: 0.2.5](https://img.shields.io/badge/Version-0.2.5-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.4.1](https://img.shields.io/badge/AppVersion-0.4.1-informational?style=flat-square)\n\nThis is just initial effort to run maddy within Kubernetes cluster. We have used Deployment resource which has some downsides\nbut at least this chart will allow you to install maddy relatively easily on your Kubernetes cluster. We have considered\nStatefulSet and DaemonSet but such solutions would require much more configuration and in casae of DaemonSet also a TCP\nload balancer in front of the nodes.\n\n## Requirement\n\nIn order to run maddy properly, you need to have TLS secret under name maddy present in the cluster. If you have commercial\ncertificate, you can create it by the following command:\n\n```sh\nkubectl create secret tls maddy --cert=fullchain.pem --key=privkey.pem\n```\n\nIf you use cert-manager, just create the secret under name maddy.\n\n## Replication\n\nDefault for this chart is 1 replica of maddy. If you try to increase this, you will probably get an error because of\nthe busy ports 25, 143, 587, etc. We do not support this feature at the moment, so please use just 1 replica. Like said\nat the beginning of this document, multiple replicas would probably require to switch do DaemonSet which would further require\nto have TCP load balancer and shared storage between all replicas. This is not supported by this chart, sorry.\nThis chart is used on one node cluster and then installation is straight forward, like described bellow, but if you have\nmultiple node cluster, please use taints and tolerations to select the desired node. This chart supports tolerations to\nbe set.\n\n## Configuration\n\n| Key                        | Type   | Default           | Description |\n| -------------------------- | ------ | ----------------- | ----------- |\n| affinity                   | object | `{}`              |             |\n| fullnameOverride           | string | `\"\"`              |             |\n| image.pullPolicy           | string | `\"IfNotPresent\"`  |             |\n| image.repository           | string | `\"foxcpp/maddy\"`  |             |\n| image.tag                  | string | `\"\"`              |             |\n| imagePullSecrets           | list   | `[]`              |             |\n| nameOverride               | string | `\"\"`              |             |\n| nodeSelector               | object | `{}`              |             |\n| persistence.accessMode     | string | `\"ReadWriteOnce\"` |             |\n| persistence.annotations    | object | `{}`              |             |\n| persistence.enabled        | bool   | `false`           |             |\n| persistence.path           | string | `\"/data\"`         |             |\n| persistence.size           | string | `\"128Mi\"`         |             |\n| podAnnotations             | object | `{}`              |             |\n| podSecurityContext         | object | `{}`              |             |\n| replicaCount               | int    | `1`               |             |\n| resources                  | object | `{}`              |             |\n| securityContext            | object | `{}`              |             |\n| service.type               | string | `\"NodePort\"`      |             |\n| serviceAccount.annotations | object | `{}`              |             |\n| serviceAccount.create      | bool   | `true`            |             |\n| serviceAccount.name        | string | `\"\"`              |             |\n| tolerations                | list   | `[]`              |             |\n\n## Installing the chart\n\n```sh\nhelm upgrade --install maddy ./chart --set service.externapIPs[0]=1.2.3.4\n```\n\n1.2.3.4 is your public IP of the node.\n\n## maddy configuration\n\nFeel free to tweak files/maddy.conf and files/aliases according to your needs.\n"
  },
  {
    "path": "contrib/kubernetes/chart/files/aliases",
    "content": "info@example.org: foxcpp@example.org\n"
  },
  {
    "path": "contrib/kubernetes/chart/files/maddy.conf",
    "content": "## maddy 0.3 - default configuration file (2020-05-31)\n# Suitable for small-scale deployments. Uses its own format for local users DB,\n# should be managed via maddy subcommands.\n#\n# See tutorials at https://foxcpp.dev/maddy for guidance on typical\n# configuration changes.\n#\n# See manual pages (also available at https://foxcpp.dev/maddy) for reference\n# documentation.\n\n# ----------------------------------------------------------------------------\n# Base variables\n\n$(hostname) = mx1.example.org\n$(primary_domain) = example.org\n$(local_domains) = $(primary_domain)\n\ntls file /etc/maddy/certs/fullchain.pem /etc/maddy/certs/privkey.pem\n\n# ----------------------------------------------------------------------------\n# Local storage & authentication\n\n# pass_table provides local hashed passwords storage for authentication of\n# users. It can be configured to use any \"table\" module, in default\n# configuration a table in SQLite DB is used.\n# Table can be replaced to use e.g. a file for passwords. Or pass_table module\n# can be replaced altogether to use some external source of credentials (e.g.\n# PAM, /etc/shadow file).\n#\n# If table module supports it (sql_table does) - credentials can be managed\n# using 'maddy creds' command.\n\nauth.pass_table local_authdb {\n    table sql_table {\n        driver sqlite3\n        dsn credentials.db\n        table_name passwords\n    }\n}\n\n# imapsql module stores all indexes and metadata necessary for IMAP using a\n# relational database. It is used by IMAP endpoint for mailbox access and\n# also by SMTP & Submission endpoints for delivery of local messages.\n#\n# IMAP accounts, mailboxes and all message metadata can be inspected using\n# imap-* subcommands of maddy.\n\nstorage.imapsql local_mailboxes {\n    driver sqlite3\n    dsn imapsql.db\n}\n\n# ----------------------------------------------------------------------------\n# SMTP endpoints + message routing\n\nhostname $(hostname)\n\nmsgpipeline local_routing {\n    dmarc yes\n    check {\n        require_matching_ehlo\n        require_mx_record\n        dkim\n        spf\n    }\n\n    # Insert handling for special-purpose local domains here.\n    # e.g.\n    # destination lists.example.org {\n    #     deliver_to lmtp tcp://127.0.0.1:8024\n    # }\n\n    destination postmaster $(local_domains) {\n        modify {\n            replace_rcpt regexp \"(.+)\\+(.+)@(.+)\" \"$1@$3\"\n            replace_rcpt file /data/aliases\n        }\n\n        deliver_to &local_mailboxes\n    }\n\n    default_destination {\n        reject 550 5.1.1 \"User doesn't exist\"\n    }\n}\n\nsmtp tcp://0.0.0.0:25 {\n    limits {\n        # Up to 20 msgs/sec across max. 10 SMTP connections.\n        all rate 20 1s\n        all concurrency 10\n    }\n\n    source $(local_domains) {\n        reject 501 5.1.8 \"Use Submission for outgoing SMTP\"\n    }\n    default_source {\n        destination postmaster $(local_domains) {\n            deliver_to &local_routing\n        }\n        default_destination {\n            reject 550 5.1.1 \"User doesn't exist\"\n        }\n    }\n}\n\nsubmission tls://0.0.0.0:465 tcp://0.0.0.0:587 {\n    limits {\n        # Up to 50 msgs/sec across any amount of SMTP connections.\n        all rate 50 1s\n    }\n\n    auth &local_authdb\n\n    source $(local_domains) {\n        destination postmaster $(local_domains) {\n            deliver_to &local_routing\n        }\n        default_destination {\n            modify {\n                dkim $(primary_domain) $(local_domains) default\n            }\n            deliver_to &remote_queue\n        }\n    }\n    default_source {\n        reject 501 5.1.8 \"Non-local sender domain\"\n    }\n}\n\ntarget.remote outbound_delivery {\n    limits {\n        # Up to 20 msgs/sec across max. 10 SMTP connections\n        # for each recipient domain.\n        destination rate 20 1s\n        destination concurrency 10\n    }\n    mx_auth {\n        dane\n        mtasts {\n            cache fs\n            fs_dir mtasts_cache/\n        }\n        local_policy {\n            min_tls_level encrypted\n            min_mx_level none\n        }\n    }\n}\n\ntarget.queue remote_queue {\n    target &outbound_delivery\n\n    autogenerated_msg_domain $(primary_domain)\n    bounce {\n        destination postmaster $(local_domains) {\n            deliver_to &local_routing\n        }\n        default_destination {\n            reject 550 5.0.0 \"Refusing to send DSNs to non-local addresses\"\n        }\n    }\n}\n\n# ----------------------------------------------------------------------------\n# IMAP endpoints\n\nimap tls://0.0.0.0:993 tcp://0.0.0.0:143 {\n    auth &local_authdb\n    storage &local_mailboxes\n}\n"
  },
  {
    "path": "contrib/kubernetes/chart/templates/NOTES.txt",
    "content": ""
  },
  {
    "path": "contrib/kubernetes/chart/templates/_helpers.tpl",
    "content": "{{/*\nExpand the name of the chart.\n*/}}\n{{- define \"maddy.name\" -}}\n{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix \"-\" }}\n{{- end }}\n\n{{/*\nCreate a default fully qualified app name.\nWe truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).\nIf release name contains chart name it will be used as a full name.\n*/}}\n{{- define \"maddy.fullname\" -}}\n{{- if .Values.fullnameOverride }}\n{{- .Values.fullnameOverride | trunc 63 | trimSuffix \"-\" }}\n{{- else }}\n{{- $name := default .Chart.Name .Values.nameOverride }}\n{{- if contains $name .Release.Name }}\n{{- .Release.Name | trunc 63 | trimSuffix \"-\" }}\n{{- else }}\n{{- printf \"%s-%s\" .Release.Name $name | trunc 63 | trimSuffix \"-\" }}\n{{- end }}\n{{- end }}\n{{- end }}\n\n{{/*\nCreate chart name and version as used by the chart label.\n*/}}\n{{- define \"maddy.chart\" -}}\n{{- printf \"%s-%s\" .Chart.Name .Chart.Version | replace \"+\" \"_\" | trunc 63 | trimSuffix \"-\" }}\n{{- end }}\n\n{{/*\nCommon labels\n*/}}\n{{- define \"maddy.labels\" -}}\nhelm.sh/chart: {{ include \"maddy.chart\" . }}\n{{ include \"maddy.selectorLabels\" . }}\n{{- if .Chart.AppVersion }}\napp.kubernetes.io/version: {{ .Chart.AppVersion | quote }}\n{{- end }}\napp.kubernetes.io/managed-by: {{ .Release.Service }}\n{{- end }}\n\n{{/*\nSelector labels\n*/}}\n{{- define \"maddy.selectorLabels\" -}}\napp.kubernetes.io/name: {{ include \"maddy.name\" . }}\napp.kubernetes.io/instance: {{ .Release.Name }}\n{{- end }}\n\n{{/*\nCreate the name of the service account to use\n*/}}\n{{- define \"maddy.serviceAccountName\" -}}\n{{- if .Values.serviceAccount.create }}\n{{- default (include \"maddy.fullname\" .) .Values.serviceAccount.name }}\n{{- else }}\n{{- default \"default\" .Values.serviceAccount.name }}\n{{- end }}\n{{- end }}\n"
  },
  {
    "path": "contrib/kubernetes/chart/templates/configmap.yaml",
    "content": "apiVersion: v1\nkind: ConfigMap\nmetadata:\n  name: {{include \"maddy.fullname\" .}}\n  labels: {{- include \"maddy.labels\" . | nindent 4}}\ndata:\n  maddy.conf: |\n{{ tpl (.Files.Get \"files/maddy.conf\") . | printf \"%s\" | indent 4 }}\n  aliases: |\n{{ tpl (.Files.Get \"files/aliases\") . | printf \"%s\" | indent 4 }}\n"
  },
  {
    "path": "contrib/kubernetes/chart/templates/deployment.yaml",
    "content": "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: {{ include \"maddy.fullname\" . }}\n  labels:\n    {{- include \"maddy.labels\" . | nindent 4 }}\nspec:\n  replicas: {{ .Values.replicaCount }}\n  selector:\n    matchLabels:\n      {{- include \"maddy.selectorLabels\" . | nindent 6 }}\n  template:\n    metadata:\n      annotations:\n        checksum/config: {{ tpl (.Files.Get \"files/maddy.conf\") . | printf \"%s\" | sha256sum }}\n        checksum/aliases: {{ tpl (.Files.Get \"files/aliases\") . | printf \"%s\" | sha256sum }}\n    {{- with .Values.podAnnotations }}\n        {{- toYaml . | nindent 8 }}\n    {{- end }}\n      labels:\n        {{- include \"maddy.selectorLabels\" . | nindent 8 }}\n    spec:\n      {{- with .Values.imagePullSecrets }}\n      imagePullSecrets:\n        {{- toYaml . | nindent 8 }}\n      {{- end }}\n      serviceAccountName: {{ include \"maddy.serviceAccountName\" . }}\n      securityContext:\n        {{- toYaml .Values.podSecurityContext | nindent 8 }}\n      initContainers:\n        - name: init\n          securityContext:\n            {{- toYaml .Values.securityContext | nindent 12 }}\n          image: busybox\n          imagePullPolicy: {{ .Values.image.pullPolicy }}\n          command:\n            - sh\n            - -c\n            - cp /tmp/maddy/* /data/.\n          resources:\n            {{- toYaml .Values.resources | nindent 12 }}\n          volumeMounts:\n            - name: data\n              mountPath: {{ .Values.persistence.path }}\n              {{- if .Values.persistence.subPath }}\n              subPath: {{ .Values.persistence.subPath }}\n              {{- end }}\n            - name: config\n              mountPath: /tmp/maddy\n      containers:\n        - name: {{ .Chart.Name }}\n          securityContext:\n            {{- toYaml .Values.securityContext | nindent 12 }}\n          image: \"{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}\"\n          imagePullPolicy: {{ .Values.image.pullPolicy }}\n          ports:\n            - name: smtp\n              containerPort: 25\n              protocol: TCP\n            - name: imaps\n              containerPort: 993\n              protocol: TCP\n            - name: starttls\n              containerPort: 587\n              protocol: TCP\n          # livenessProbe:\n          #   httpGet:\n          #     path: /\n          #     port: http\n          # readinessProbe:\n          #   httpGet:\n          #     path: /\n          #     port: http\n          resources:\n            {{- toYaml .Values.resources | nindent 12 }}\n          volumeMounts:\n            - name: data\n              mountPath: {{ .Values.persistence.path }}\n              {{- if .Values.persistence.subPath }}\n              subPath: {{ .Values.persistence.subPath }}\n              {{- end }}\n            - name: tls\n              mountPath: /etc/maddy/certs/fullchain.pem\n              subPath: tls.crt\n            - name: tls\n              mountPath: /etc/maddy/certs/privkey.pem\n              subPath: tls.key\n      volumes:\n        - name: data\n          {{- if .Values.persistence.enabled }}\n          persistentVolumeClaim:\n            claimName: {{ default (include \"maddy.fullname\" .) .Values.persistence.existingClaim }}\n          {{- else }}\n          emptyDir: {}\n          {{- end }}\n        - name: config\n          configMap:\n            name: {{include \"maddy.fullname\" .}}\n        - name: tls\n          secret:\n            secretName: {{include \"maddy.fullname\" .}}\n      {{- with .Values.nodeSelector }}\n      nodeSelector:\n        {{- toYaml . | nindent 8 }}\n      {{- end }}\n      {{- with .Values.affinity }}\n      affinity:\n        {{- toYaml . | nindent 8 }}\n      {{- end }}\n      {{- with .Values.tolerations }}\n      tolerations:\n        {{- toYaml . | nindent 8 }}\n      {{- end }}\n"
  },
  {
    "path": "contrib/kubernetes/chart/templates/pvc.yaml",
    "content": "{{- if and .Values.persistence.enabled (not .Values.persistence.existingClaim) -}}\napiVersion: v1\nkind: PersistentVolumeClaim\nmetadata:\n  name: {{ include \"maddy.fullname\" . }}\n  annotations:\n  {{- with .Values.persistence.annotations  }}\n  {{ toYaml . | indent 4 }}\n  {{- end }}\n  labels:\n    {{- include \"maddy.labels\" . | nindent 4 }}\nspec:\n  accessModes:\n    - {{ .Values.persistence.accessMode }}\n  resources:\n    requests:\n      storage: {{ .Values.persistence.size }}\n  {{- if .Values.persistence.storageClass }}\n  storageClassName: {{ .Values.persistence.storageClass }}\n  {{- end }}\n{{- end -}}\n\n"
  },
  {
    "path": "contrib/kubernetes/chart/templates/service.yaml",
    "content": "apiVersion: v1\nkind: Service\nmetadata:\n  name: {{ include \"maddy.fullname\" . }}\n  labels:\n    {{- include \"maddy.labels\" . | nindent 4 }}\nspec:\n  type: {{ .Values.service.type }}\n  ports:\n    - port: 25\n      targetPort: smtp\n      protocol: TCP\n      name: smtp\n    - port: 993\n      targetPort: imaps\n      protocol: TCP\n      name: imaps\n    - port: 587\n      targetPort: starttls\n      protocol: TCP\n      name: starttls\n  selector:\n    {{- include \"maddy.selectorLabels\" . | nindent 4 }}\n  {{- with .Values.service.externalIPs }}\n  externalIPs:\n  {{- toYaml . | nindent 6 }}\n  {{- end -}}\n"
  },
  {
    "path": "contrib/kubernetes/chart/templates/serviceaccount.yaml",
    "content": "{{- if .Values.serviceAccount.create -}}\napiVersion: v1\nkind: ServiceAccount\nmetadata:\n  name: {{ include \"maddy.serviceAccountName\" . }}\n  labels:\n    {{- include \"maddy.labels\" . | nindent 4 }}\n  {{- with .Values.serviceAccount.annotations }}\n  annotations:\n    {{- toYaml . | nindent 4 }}\n  {{- end }}\n{{- end }}\n"
  },
  {
    "path": "contrib/kubernetes/chart/templates/tests/test-connection.yaml",
    "content": "apiVersion: v1\nkind: Pod\nmetadata:\n  name: \"{{ include \"maddy.fullname\" . }}-test-connection\"\n  labels:\n    {{- include \"maddy.labels\" . | nindent 4 }}\n  annotations:\n    \"helm.sh/hook\": test-success\nspec:\n  containers:\n    - name: wget\n      image: busybox\n      command: ['wget']\n      args: ['{{ include \"maddy.fullname\" . }}:{{ .Values.service.port }}']\n  restartPolicy: Never\n"
  },
  {
    "path": "contrib/kubernetes/chart/values.yaml",
    "content": "# Default values for maddy.\n# This is a YAML-formatted file.\n# Declare variables to be passed into your templates.\n\nreplicaCount: 1 # Multiple replicas are not supported, please don't change this.\n\nimage:\n  repository: foxcpp/maddy\n  pullPolicy: IfNotPresent\n  # Overrides the image tag whose default is the chart appVersion.\n  tag: \"\"\n\nimagePullSecrets: []\nnameOverride: \"\"\nfullnameOverride: \"\"\n\nserviceAccount:\n  # Specifies whether a service account should be created\n  create: true\n  # Annotations to add to the service account\n  annotations: {}\n  # The name of the service account to use.\n  # If not set and create is true, a name is generated using the fullname template\n  name: \"\"\n\npodAnnotations: {}\n\npodSecurityContext:\n  {}\n  # fsGroup: 2000\n\nsecurityContext:\n  {}\n  # capabilities:\n  #   drop:\n  #   - ALL\n  # readOnlyRootFilesystem: true\n  # runAsNonRoot: true\n  # runAsUser: 1000\n\n# Set externalPIs to your public IP(s) of the node running maddy. In case of multiple nodes, you need to set tolerations\n# and taints in order to run maddy on the exact node.\nservice:\n  type: NodePort\n  # externalIPs:\n\nresources:\n  {}\n  # We usually recommend not to specify default resources and to leave this as a conscious\n  # choice for the user. This also increases chances charts run on environments with little\n  # resources, such as Minikube. If you do want to specify resources, uncomment the following\n  # lines, adjust them as necessary, and remove the curly braces after 'resources:'.\n  # limits:\n  #   cpu: 100m\n  #   memory: 128Mi\n  # requests:\n  #   cpu: 100m\n  #   memory: 128Mi\n\npersistence:\n  enabled: false\n  # existingClaim: \"\"\n  accessMode: ReadWriteOnce\n  size: 128Mi\n  # storageClass: \"\"\n  path: /data\n  annotations: {}\n  # subPath: \"\" # only mount a subpath of the Volume into the pod\n\nnodeSelector: {}\n\ntolerations: []\n\naffinity: {}\n"
  },
  {
    "path": "directories.go",
    "content": "//go:build !docker\n// +build !docker\n\npackage maddy\n\nvar (\n\t// ConfigDirectory specifies platform-specific value\n\t// that should be used as a location of default configuration\n\t//\n\t// It should not be changed and is defined as a variable\n\t// only for purposes of modification using -X linker flag.\n\tConfigDirectory = \"/etc/maddy\"\n\n\t// DefaultStateDirectory specifies platform-specific\n\t// default for StateDirectory.\n\t//\n\t// Most code should use StateDirectory instead since\n\t// it will contain the effective location of the state\n\t// directory.\n\t//\n\t// It should not be changed and is defined as a variable\n\t// only for purposes of modification using -X linker flag.\n\tDefaultStateDirectory = \"/var/lib/maddy\"\n\n\t// DefaultRuntimeDirectory specifies platform-specific\n\t// default for RuntimeDirectory.\n\t//\n\t// Most code should use RuntimeDirectory instead since\n\t// it will contain the effective location of the state\n\t// directory.\n\t//\n\t// It should not be changed and is defined as a variable\n\t// only for purposes of modification using -X linker flag.\n\tDefaultRuntimeDirectory = \"/run/maddy\"\n\n\t// DefaultLibexecDirectory specifies platform-specific\n\t// default for LibexecDirectory.\n\t//\n\t// Most code should use LibexecDirectory since it will\n\t// contain the effective location of the libexec\n\t// directory.\n\t//\n\t// It should not be changed and is defined as a variable\n\t// only for purposes of modification using -X linker flag.\n\tDefaultLibexecDirectory = \"/usr/lib/maddy\"\n)\n"
  },
  {
    "path": "directories_docker.go",
    "content": "//go:build docker\n// +build docker\n\npackage maddy\n\nvar (\n\tConfigDirectory         = \"/data\"\n\tDefaultStateDirectory   = \"/data\"\n\tDefaultRuntimeDirectory = \"/tmp\"\n\tDefaultLibexecDirectory = \"/usr/lib/maddy\"\n)\n"
  },
  {
    "path": "dist/README.md",
    "content": "Distribution files for maddy\n------------------------------\n\n**Disclaimer:** Most of the files here are maintained in a \"best-effort\" way.\nThat is, they may break or become outdated from time to time. Caveat emptor.\n\n## integration + scripts\n\nThese directories provide pre-made configuration snippets suitable for\neasy integration with external software.\n\nUsually, this is what you use when you put `import integration/something` in\nyour config.\n\n## systemd unit\n\n`maddy.service` launches using default config path (/etc/maddy/maddy.conf).\n`maddy@.service` launches maddy using custom config path. E.g.\n`maddy@foo.service` will use /etc/maddy/foo.conf.\n\nAdditionally, unit files apply strict sandboxing, limiting maddy permissions on\nthe system to a bare minimum. Subset of these options makes it impossible for\nprivileged authentication helper binaries to gain required permissions, so you\nmay have to disable it when using system account-based authentication with\nmaddy running as a unprivileged user.\n\n## fail2ban configuration\n\nConfiguration files for use with fail2ban. Assume either `backend = systemd` specified\nin system-wide configuration or log file written to /var/log/maddy/maddy.log.\n\nSee https://github.com/foxcpp/maddy/wiki/fail2ban-configuration for details.\n\n## logrotate configuration\n\nMeant for logs rotation when logging to file is used.\n\n## vim ftdetect/ftplugin/syntax files\n\nMinimal supplement to make configuration files more readable and help you see\ntypos in directive names.\n"
  },
  {
    "path": "dist/apparmor/dev.foxcpp.maddy",
    "content": "# AppArmor profile for maddy daemon.\n# vim:syntax=apparmor:ts=2:sw=2:et\n\n#include <tunables/global>\n\nprofile dev.foxcpp.maddy /usr{/local,}/bin/maddy {\n  #include <abstractions/base>\n  #include <abstractions/ssl_certs>\n  #include <abstractions/ssl_keys>\n  /etc/ca-certificates/** r,\n\n  /etc/resolv.conf r,\n  /proc/sys/net/core/somaxconn r,\n  /sys/kernel/mm/transparent_hugepage/hpage_pmd_size r,\n  deny ptrace,\n  capability net_bind_service,\n  network tcp,\n  network unix,\n\n  # systemd process management and Type=notify\n  signal (receive) peer=unconfined,\n  signal (receive) peer=/usr/bin/systemd,\n  unix (create, connect, send, setopt) type=dgram addr=@*,\n  /run/systemd/notify w,\n\n  /etc/maddy/** r,\n  owner /run/maddy/ rw,\n  owner /run/maddy/** rwkl,\n  owner /var/lib/maddy/ rw,\n  owner /var/lib/maddy/** rwk,\n  owner /var/lib/maddy/**.db-{wal,shm} rmk,\n\n  /usr{/local,}/lib/maddy/* PUx,\n\n  /usr{/local,}/bin/maddy{,ctl} rmix,\n\n  #include if exists <local/dev.foxcpp.maddy>\n}\n"
  },
  {
    "path": "dist/fail2ban/filter.d/maddy-auth.conf",
    "content": "[INCLUDES]\nbefore = common.conf\n\n[Definition]\nfailregex    = authentication failed\\t\\{\\\"reason\\\":\\\".*\\\",\\\"src_ip\\\"\\:\\\"<HOST>:\\d+\\\"\\,\\\"username\\\"\\:\\\".*\\\"\\}$\njournalmatch = _SYSTEMD_UNIT=maddy.service + _COMM=maddy\n"
  },
  {
    "path": "dist/fail2ban/filter.d/maddy-dictonary-attack.conf",
    "content": "[INCLUDES]\nbefore = common.conf\n\n[Definition]\nfailregex    = smtp\\: MAIL FROM error repeated a lot\\, possible dictonary attack\\t\\{\\\"count\\\"\\:\\d+,\\\"msg_id\\\":\\\".+\\\",\\\"src_ip\\\"\\:\\\"<HOST>:\\d+\\\"\\}$\n               smtp\\: too many RCPT errors\\, possible dictonary attack\\t\\{\\\"msg_id\\\":\\\".+\\\",\"src_ip\":\"<HOST>:\\d+\\\"\\}\njournalmatch = _SYSTEMD_UNIT=maddy.service + _COMM=maddy\n"
  },
  {
    "path": "dist/fail2ban/jail.d/maddy-auth.conf",
    "content": "[maddy-auth]\nport     = 993,465,25\nfilter   = maddy-auth\nbantime  = 96h\nbackend  = systemd\n"
  },
  {
    "path": "dist/fail2ban/jail.d/maddy-dictonary-attack.conf",
    "content": "[maddy-dictonary-attack]\nport     = 993,465,25\nfilter   = maddy-dictonary-attack\nbantime  = 72h\nmaxretry = 3\nfindtime = 6h\nbackend  = systemd\n"
  },
  {
    "path": "dist/install.sh",
    "content": "#!/bin/bash\n\nDESTDIR=$DESTDIR\nif [ -z \"$PREFIX\" ]; then\n    PREFIX=/usr/local\nfi\nif [ -z \"$FAIL2BANDIR\" ]; then\n    FAIL2BANDIR=/etc/fail2ban\nfi\nif [ -z \"$CONFDIR\" ]; then\n    CONFDIR=/etc/maddy\nfi\n\nscript_dir=\"$( cd \"$( dirname \"${BASH_SOURCE[0]}\" )\" >/dev/null 2>&1 && pwd )\"\ncd $script_dir\n\ninstall -Dm 0644 -t \"$DESTDIR/$PREFIX/share/vim/vimfiles/ftdetect/\" vim/ftdetect/maddy-conf.vim\ninstall -Dm 0644 -t \"$DESTDIR/$PREFIX/share/vim/vimfiles/ftplugin/\" vim/ftplugin/maddy-conf.vim\ninstall -Dm 0644 -t \"$DESTDIR/$PREFIX/share/vim/vimfiles/syntax/\" vim/syntax/maddy-conf.vim\n\ninstall -Dm 0644 -t \"$DESTDIR/$FAIL2BANDIR/jail.d/\" fail2ban/jail.d/*\ninstall -Dm 0644 -t \"$DESTDIR/$FAIL2BANDIR/filter.d/\" fail2ban/filter.d/*\n\ninstall -Dm 0644 -t \"$DESTDIR/$PREFIX/lib/systemd/system/\" systemd/maddy.service systemd/maddy@.service\n"
  },
  {
    "path": "dist/logrotate.d/maddy",
    "content": "/var/log/maddy/maddy.log {\n    missingok\n    su maddy maddy\n    postrotate\n        /usr/bin/killall -USR1 maddy\n    endscript\n}\n"
  },
  {
    "path": "dist/systemd/maddy.service",
    "content": "[Unit]\nDescription=maddy mail server\nDocumentation=man:maddy(1)\nDocumentation=man:maddy.conf(5)\nDocumentation=https://maddy.email\nAfter=network-online.target\n\n[Service]\nType=notify\nNotifyAccess=main\n\nUser=maddy\nGroup=maddy\n\n# cd to state directory to make sure any relative paths\n# in config will be relative to it unless handled specially.\nWorkingDirectory=/var/lib/maddy\n\nConfigurationDirectory=maddy\nRuntimeDirectory=maddy\nStateDirectory=maddy\nLogsDirectory=maddy\nReadOnlyPaths=/usr/lib/maddy\nReadWritePaths=/var/lib/maddy\n\n# Strict sandboxing. You have no reason to trust code written by strangers from GitHub.\nPrivateTmp=true\nProtectHome=true\nProtectSystem=strict\nProtectKernelTunables=true\nProtectHostname=true\nProtectClock=true\nProtectControlGroups=true\nRestrictAddressFamilies=AF_UNIX AF_INET AF_INET6\n\n# Additional sandboxing. You need to disable all of these options\n# for privileged helper binaries (for system auth) to work correctly.\nNoNewPrivileges=true\nPrivateDevices=true\nDeviceAllow=/dev/syslog\nRestrictSUIDSGID=true\nProtectKernelModules=true\nMemoryDenyWriteExecute=true\nRestrictNamespaces=true\nRestrictRealtime=true\nLockPersonality=true\n\n# Graceful shutdown with a reasonable timeout.\nTimeoutStopSec=7s\nKillMode=mixed\nKillSignal=SIGTERM\n\n# Required to bind on ports lower than 1024.\nAmbientCapabilities=CAP_NET_BIND_SERVICE\nCapabilityBoundingSet=CAP_NET_BIND_SERVICE\n\n# Force all files created by maddy to be only readable by it\n# and maddy group.\nUMask=0007\n\n# Bump FD limitations. Even idle mail server can have a lot of FDs open (think\n# of idle IMAP connections, especially ones abandoned on the other end and\n# slowly timing out).\nLimitNOFILE=131072\n\n# Limit processes count to something reasonable to\n# prevent resources exhausting due to big amounts of helper\n# processes launched.\nLimitNPROC=512\n\n# Restart server on any problem.\nRestart=on-failure\n# ... Unless it is a configuration problem.\nRestartPreventExitStatus=2\n\nExecStart=/usr/local/bin/maddy run\n\nExecReload=/bin/kill -USR2 $MAINPID\n\n[Install]\nWantedBy=multi-user.target\n"
  },
  {
    "path": "dist/systemd/maddy@.service",
    "content": "[Unit]\nDescription=maddy mail server (using %i.conf)\nDocumentation=man:maddy(1)\nDocumentation=man:maddy.conf(5)\nDocumentation=https://maddy.email\nAfter=network-online.target\n\n[Service]\nType=notify\nNotifyAccess=main\n\nUser=maddy\nGroup=maddy\n\nConfigurationDirectory=maddy\nRuntimeDirectory=maddy\nStateDirectory=maddy\nLogsDirectory=maddy\nReadOnlyPaths=/usr/lib/maddy\nReadWritePaths=/var/lib/maddy\n\n# Strict sandboxing. You have no reason to trust code written by strangers from GitHub.\nPrivateTmp=true\nPrivateHome=true\nProtectSystem=strict\nProtectKernelTunables=true\nProtectHostname=true\nProtectClock=true\nProtectControlGroups=true\nRestrictAddressFamilies=AF_UNIX AF_INET AF_INET6\nDeviceAllow=/dev/syslog\n\n# Additional sandboxing. You need to disable all of these options\n# for privileged helper binaries (for system auth) to work correctly.\nNoNewPrivileges=true\nPrivateDevices=true\nRestrictSUIDSGID=true\nProtectKernelModules=true\nMemoryDenyWriteExecute=true\nRestrictNamespaces=true\nRestrictRealtime=true\nLockPersonality=true\n\n# Graceful shutdown with a reasonable timeout.\nTimeoutStopSec=7s\nKillMode=mixed\nKillSignal=SIGTERM\n\n# Required to bind on ports lower than 1024.\nAmbientCapabilities=CAP_NET_BIND_SERVICE\nCapabilityBoundingSet=CAP_NET_BIND_SERVICE\n\n# Force all files created by maddy to be only readable by it and\n# maddy group.\nUMask=0007\n\n# Bump FD limitations. Even idle mail server can have a lot of FDs open (think\n# of idle IMAP connections, especially ones abandoned on the other end and\n# slowly timing out).\nLimitNOFILE=131072\n\n# Limit processes count to something reasonable to\n# prevent resources exhausting due to big amounts of helper\n# processes launched.\nLimitNPROC=512\n\n# Restart server on any problem.\nRestart=on-failure\n# ... Unless it is a configuration problem.\nRestartPreventExitStatus=2\n\nExecStart=/usr/local/bin/maddy --config /etc/maddy/%i.conf run\n\nExecReload=/bin/kill -USR2 $MAINPID\n\n[Install]\nWantedBy=multi-user.target\n"
  },
  {
    "path": "dist/vim/ftdetect/maddy-conf.vim",
    "content": "au BufNewFile,BufRead /etc/maddy/*,maddy.conf setf maddy-conf\n"
  },
  {
    "path": "dist/vim/ftplugin/maddy-conf.vim",
    "content": "setlocal commentstring=#\\ %s\n\n\" That is convention for maddy configs. Period.\n\"    - fox.cpp (maddy developer)\nsetlocal expandtab\nsetlocal tabstop=4\nsetlocal softtabstop=4\nsetlocal shiftwidth=4\n"
  },
  {
    "path": "dist/vim/syntax/maddy-conf.vim",
    "content": "\" vim: noexpandtab ts=4 sw=4\n\nif exists(\"b:current_syntax\")\n\tfinish\nendif\n\n\" Lexer-defined rules\nsyn match\t\tmaddyComment\t\"#.*\"\nsyn region      maddyString\t\tstart=+\"+ skip=+\\\\\\\\\\|\\\\\"+ end=+\"+ oneline\n\nsyn region\t\tmaddyBlock\t\tstart=\"{\" end=\"}\" transparent fold\n\nhi def link maddyComment\tComment\nhi def link maddyString\t\tString\n\n\" Parser-defined rules\nsyn match\t\tmaddyMacroName\t\"[a-z0-9_]\" contained containedin=maddyMacro\nsyn match\t\tmaddyMacro      \"$(.\\{-})\" contains=maddyMacroName\n\nsyn match\t\tmaddyMacroDefSign \"=\" contained\nsyn match\t\tmaddyMacroDef     \"\\^$([a-z0-9_]\\{-})\\s=\\s.\\+\" contains=maddyMacro,maddyMacroDefSign\n\nhi def link maddyMacroName\t\t\tIdentifier\nhi def link maddyMacro\t\t\t\tSpecial\nhi def link maddyMacroDefSign\t\tSpecial\n\n\" config.Map values\nsyn keyword maddyBool yes no\n\nsyn match maddyInt '\\<\\d\\+\\>'\nsyn match maddyInt '\\<[-+]\\d\\+\\>'\nsyn match maddyFloat '\\<[-+]\\d\\+\\.\\d*\\<'\n\nsyn match maddyReference /[ \\t]&[^ \\t]\\+/ms=s+1 contains=maddyReferenceSign\nsyn match maddyReferenceSign /&/ contained\n\nhi def link maddyBool\t\tBoolean\nhi def link maddyInt\t\tNumber\nhi def link maddyFloat\t\tFloat\n\nhi def link maddyReferenceSign\tSpecial\n\n\" Module values\n\n\" grep --no-file -E 'Register.*\\(\".+\", ' **.go | sed -E 's/.+Register.*\\(\"([^\"]+)\", .+/\\1/' | sort -u\nsyn keyword maddyModule\n\t\\ checks\n\t\\ command\n\t\\ dane\n\t\\ dkim\n\t\\ dnsbl\n\t\\ dnssec\n\t\\ dummy\n\t\\ extauth\n\t\\ external\n\t\\ file\n\t\\ identity\n\t\\ imap\n\t\\ imap_filters\n\t\\ imapsql\n\t\\ limits\n\t\\ lmtp\n\t\\ loader\n\t\\ local_policy\n\t\\ milter\n\t\\ modifiers\n\t\\ msgpipeline\n\t\\ mtasts\n\t\\ mx_auth\n\t\\ pam\n\t\\ pass_table\n\t\\ plain_separate\n\t\\ queue\n\t\\ regexp\n\t\\ remote\n\t\\ replace_rcpt\n\t\\ replace_sender\n\t\\ require_matching_rdns\n\t\\ require_mx_record\n\t\\ require_tls\n\t\\ rspamd\n\t\\ shadow\n\t\\ smtp\n\t\\ sql_query\n\t\\ sql_table\n\t\\ static\n\t\\ submission\n\nsyn keyword maddyDispatchDir\n\t\\ check\n\t\\ modify\n\t\\ default_source\n\t\\ source\n\t\\ default_destination\n\t\\ destination\n\t\\ reject\n\t\\ deliver_to\n\t\\ reroute\n\t\\ dmarc\n\n\" grep --no-file -E 'cfg..+\\(\".+\", ' **.go | sed -E 's/.+cfg..+\\(\"([^\"]+)\", .+/\\1/' | sort -u\nsyn keyword maddyModDir\n\t\\ add\n\t\\ add_header_action\n\t\\ allow_multiple_from\n\t\\ api_path\n\t\\ appendlimit\n\t\\ attempt_starttls\n\t\\ auth\n\t\\ autogenerated_msg_domain\n\t\\ body_canon\n\t\\ bounce\n\t\\ broken_sig_action\n\t\\ buffer\n\t\\ cache\n\t\\ case_insensitive\n\t\\ certs\n\t\\ check_early\n\t\\ client_ipv4\n\t\\ client_ipv6\n\t\\ compression\n\t\\ conn_max_idle_count\n\t\\ conn_max_idle_time\n\t\\ conn_reuse_limit\n\t\\ debug\n\t\\ defer_sender_reject\n\t\\ del\n\t\\ domains\n\t\\ driver\n\t\\ dsn\n\t\\ ehlo\n\t\\ endpoint\n\t\\ enforce_early\n\t\\ enforce_testing\n\t\\ entry\n\t\\ error_resp_action\n\t\\ expand_replaceholders\n\t\\ fail_action\n\t\\ fail_open\n\t\\ file\n\t\\ flags\n\t\\ force_ipv4\n\t\\ fs_dir\n\t\\ fsstore\n\t\\ full_match\n\t\\ hash\n\t\\ header_canon\n\t\\ helper\n\t\\ hostname\n\t\\ imap_filter\n\t\\ init\n\t\\ insecure_auth\n\t\\ io_debug\n\t\\ io_error_action\n\t\\ io_errors\n\t\\ junk_mailbox\n\t\\ key_column\n\t\\ key_path\n\t\\ keys\n\t\\ limits\n\t\\ list\n\t\\ local_ip\n\t\\ location\n\t\\ lookup\n\t\\ mailfrom\n\t\\ max_logged_rcpt_errors\n\t\\ max_message_size\n\t\\ max_parallelism\n\t\\ max_received\n\t\\ max_recipients\n\t\\ max_tries\n\t\\ min_mx_level\n\t\\ min_tls_level\n\t\\ mx_auth\n\t\\ neutral_action\n\t\\ newkey_algo\n\t\\ none_action\n\t\\ no_sig_action\n\t\\ oversign_fields\n\t\\ pass\n\t\\ perdomain\n\t\\ permerr_action\n\t\\ quarantine_threshold\n\t\\ read_timeout\n\t\\ reject_threshold\n\t\\ reject_action\n\t\\ relaxed_requiretls\n\t\\ required_fields\n\t\\ require_sender_match\n\t\\ require_tls\n\t\\ requiretls_override\n\t\\ responses\n\t\\ rewrite_subj_action\n\t\\ run_on\n\t\\ score\n\t\\ selector\n\t\\ set\n\t\\ settings_id\n\t\\ sig_expiry\n\t\\ sign_fields\n\t\\ sign_subdomains\n\t\\ soft_reject_action\n\t\\ softfail_action\n\t\\ SOME_action\n\t\\ source\n\t\\ sqlite3_busy_timeout\n\t\\ sqlite3_cache_size\n\t\\ sqlite3_exclusive_lock\n\t\\ storage\n\t\\ table\n\t\\ table_name\n\t\\ tag\n\t\\ target\n\t\\ targets\n\t\\ temperr_action\n\t\\ tls\n\t\\ tls_client\n\t\\ use_helper\n\t\\ user\n\t\\ value_column\n\t\\ write_timeout\n\nhi def link maddyModDir\t\tIdentifier\nhi def link maddyModule\t\tIdentifier\nhi def link maddyDispatchDir\t\tIdentifier\n\nlet b:current_syntax = \"maddy\"\n"
  },
  {
    "path": "docs/docker.md",
    "content": "# Docker\n\nOfficial Docker image is available from Docker Hub.\n\nIt expects configuration file to be available at /data/maddy.conf.\n\nIf /data is a Docker volume, then default configuration will be placed there\nautomatically. If it is used, then MADDY_HOSTNAME, MADDY_DOMAIN environment\nvariables control the host name and primary domain for the server. TLS\ncertificate should be placed in /data/tls/fullchain.pem, private key in\n/data/tls/privkey.pem\n\nDKIM keys are generated in /data/dkim_keys directory.\n\n## Image tags\n\n- `latest` - A latest stable release. May contain breaking changes.\n- `X.Y` - A specific feature branch, it is recommended to use these tags to\n  receive bugfixes without the risk of feature-related regressions or breaking\n  changes.\n- `X.Y.Z` - A specific stable release\n\n## Ports\n\nAll standard ports, as described in maddy docs.\n\n- `25` - SMTP inbound port.\n- `465`, `587` - SMTP Submission ports\n- `993`, `143` - IMAP4 ports\n\n## Volumes\n\n`/data` - maddy state directory. Databases, queues, etc are stored here. You\nmight want to mount a named volume there. The main configuration file is stored\nhere too (`/data/maddy.conf`).\n\n## Management utility\n\nTo run management commands, create a temporary container with the same\n/data directory and put the command after the image name, like this:\n\n```\ndocker run --rm -it -v maddydata:/data foxcpp/maddy:0.7 creds create foxcpp@maddy.test\ndocker run --rm -it -v maddydata:/data foxcpp/maddy:0.7 imap-acct create foxcpp@maddy.test\n```\n\nUse the same image version as the running server. Things may break badly\notherwise.\n\nNote that, if you modify messages using maddy subcommands while the server is running -\nyou must ensure that  /tmp from the server is accessible for the management\ncommand. One way to it is to run it using `docker exec` instead of `docker run`:\n```\ndocker exec -it container_name_here maddy creds create foxcpp@maddy.test\n```\n\n## Build Tags\n\nSome Maddy features (such as automatic certificate management via ACME with [a non-default libdns provider](../reference/tls-acme/#dns-providers)) require build tags to be passed to Maddy's `build.sh`, as this is run in the Dockerfile you must compile your own Docker image. Build tags can be set via the docker build argument `ADDITIONAL_BUILD_TAGS` e.g. `docker build --build-arg ADDITIONAL_BUILD_TAGS=\"libdns_acmedns libdns_route53\" -t yourorgname/maddy:yourtagname .`.\n\n\n## TL;DR\n\n```\ndocker volume create maddydata\ndocker run \\\n  --name maddy \\\n  -e MADDY_HOSTNAME=mx.maddy.test \\\n  -e MADDY_DOMAIN=maddy.test \\\n  -v maddydata:/data \\\n  -p 25:25 \\\n  -p 143:143 \\\n  -p 465:465 \\\n  -p 587:587 \\\n  -p 993:993 \\\n  foxcpp/maddy:0.7\n```\n\nIt will fail on first startup. Copy TLS certificate to /data/tls/fullchain.pem\nand key to /data/tls/privkey.pem. Run the server again. Finish DNS configuration\n(DKIM keys, etc) as described in [tutorials/setting-up/](../tutorials/setting-up/).\n"
  },
  {
    "path": "docs/faq.md",
    "content": "# Frequently Asked Questions\n\n## I configured maddy as recommended and gmail still puts my messages in spam\n\nUnfortunately, GMail policies are opaque so we cannot tell why this happens.\n\nVerify that you have a rDNS record set for the IP used\nby sender server. Also some IPs may just happen to\nhave bad reputation - check it with various DNSBLs. In this\ncase you do not have much of a choice but to replace it.\n\nAdditionally, you may try marking multiple messages sent from\nyour domain as \"not spam\" in GMail UI.\n\n## Message sending fails with `dial tcp X.X.X.X:25: connect: connection timed out` in log\n\nYour provider is blocking outbound SMTP traffic on port 25.\n\nYou either have to ask them to unblock it or forward\nall outbound messages via a \"smart-host\".\n\n## What is resource usage of maddy?\n\nFor a small personal server, you do not need much more than a\nsingle 1 GiB of RAM and disk space.\n\n## How to setup a catchall address?\n\nhttps://github.com/foxcpp/maddy/issues/243#issuecomment-655694512\n\n## maddy command prints a \"permission denied\" error\n\nRun maddy command under the same user as maddy itself.\nE.g.\n```\nsudo -u maddy maddy creds ...\n```\n\n## How maddy compares to MailCow or Mail-In-The-Box?\n\nMailCow and MIAB are bundles of well-known email-related software configured to\nwork together. maddy is a single piece of software implementing subset of what\nMailCow and MIAB offer.\n\nmaddy offers more uniform configuration system, more lightweight implementation\nand has no dependency on Docker or similar technologies for deployment.\n\nmaddy may have more bugs than 20 years old battle-tested software.\n\nIt is easier to get help with MailCow/MITB since underlying implementations\nare well-understood and have active community.\n\nmaddy has no Web interface for administration, that is currently done via CLI\nutility.\n\n## How maddy IMAP server compares to WildDuck?\n\nBoth are \"more secure by definition\": root access is not required,\nimplementation is in memory-safe language, etc.\n\nBoth support message compression.\n\nBoth have first-class Unicode/internationalization support.\n\nWildDuck may offer easier scalability options. maddy does not require you to\nsetup MongoDB and Redis servers, though. In fact, maddy in its default\nconfiguration has no dependencies besides libc.\n\nmaddy has less builtin authentication providers. This means no\napp-specific passwords and all that WildDuck lists under point 4 on their\nfeatures page.\n\nmaddy currently has no admin Web interface, all necessary DB changes are\nperformed via CLI utility.\n\n## How maddy SMTP server compares to ZoneMTA?\n\nmaddy SMTP server has a lot of similarities to ZoneMTA.\nBoth have powerful mechanisms for message routing (although designed\ndifferently).\n\nmaddy does not require MongoDB server for deployment.\n\nmaddy has no web interface for queue inspection. However, it can\neasily inspected by looking at files in /var/lib/maddy.\n\nZoneMTA has a number of features that may make it easier to integrate\nwith HTTP-based services. maddy speaks standard email protocols (SMTP,\nSubmission).\n\n## Is there a webmail?\n\nNo, at least currently.\n\nI suggest you to check out [alps](https://git.sr.ht/~migadu/alps) if you\nare fine with alpha-quality but extremely easy to deploy webmail.\n\n## Is there a content filter (spam filter)?\n\nNo. maddy moves email messages around, it does not classify\nthem as bad or good with the notable exception of sender policies.\n\nIt is possible to integrate rspamd using 'rspamd' module. Just add\n`rspamd` line to `checks` in `local_routing`, it should just work\nin most cases.\n\n## Is it production-ready?\n\nmaddy is considered \"beta\" quality. Several people use it for personal email.\n\n## Single process makes it unreliable. This is dumb!\n\nThis is a compromise between ease of management and reliability. Several\nmeasures are implemented in code base in attempt to reduce possible effect\nof bugs in one component.\n\nBesides, you are not required to use a single process, it is easy to launch\nmaddy with a non-default configuration path and connect multiple instances\ntogether using off-the-shelf protocols.\n"
  },
  {
    "path": "docs/index.md",
    "content": "> Composable all-in-one mail server.\n\nMaddy Mail Server implements all functionality required to run a e-mail\nserver. It can send messages via SMTP (works as MTA), accept messages via SMTP\n(works as MX) and store messages while providing access to them via IMAP.\nIn addition to that it implements auxiliary protocols that are mandatory\nto keep email reasonably secure (DKIM, SPF, DMARC, DANE, MTA-STS).\n\nIt replaces Postfix, Dovecot, OpenDKIM, OpenSPF, OpenDMARC and more with one\ndaemon with uniform configuration and minimal maintenance cost.\n\n**Note:** IMAP storage is \"beta\". If you are looking for stable and\nfeature-packed implementation you may want to use Dovecot instead. maddy still\ncan handle message delivery business.\n\n[![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)\n[![Issues tracker](https://img.shields.io/github/issues/foxcpp/maddy?style=flat-square)](https://github.com/foxcpp/maddy)\n\n* [Setup tutorial](https://maddy.email/tutorials/setting-up/)\n* [Documentation](https://maddy.email/)\n\n* [IRC channel](https://webchat.oftc.net/?channels=maddy&uio=MT11bmRlZmluZWQb1)\n* [Mailing list](https://lists.sr.ht/~foxcpp/maddy)\n"
  },
  {
    "path": "docs/internals/quirks.md",
    "content": "# Implementation quirks\n\nThis page documents unusual behavior of the maddy protocols implementations.\nSome of these problems break standards, some don't but still can hurt\ninteroperability.\n\n## SMTP\n\n- `for` field is never included in the `Received` header field.\n\n  This is allowed by [RFC 2821].\n\n## IMAP\n\n### `sql`\n\n- `\\Recent` flag is not reset in all cases.\n\n  This _does not_ break [RFC 3501]. Clients relying on it will work (much) less\n  efficiently.\n\n[RFC 2821]: https://tools.ietf.org/html/rfc2821\n[RFC 3501]: https://tools.ietf.org/html/rfc3501\n"
  },
  {
    "path": "docs/internals/specifications.md",
    "content": "# Followed specifications\n\nThis page lists Internet Standards and other specifications followed by\nmaddy along with any known deviations.\n\n\n## Message format\n\n- [RFC 2822] - Internet Message Format\n- [RFC 2045] - Multipurpose Internet Mail Extensions (MIME) (part 1)\n- [RFC 2046] - Multipurpose Internet Mail Extensions (MIME) (part 2)\n- [RFC 2047] - Multipurpose Internet Mail Extensions (MIME) (part 3)\n- [RFC 2048] - Multipurpose Internet Mail Extensions (MIME) (part 4)\n- [RFC 2049] - Multipurpose Internet Mail Extensions (MIME) (part 5)\n- [RFC 6532] - Internationalized Email Headers\n\n- [RFC 2183] - Communicating Presentation Information in Internet Messages: The\n  Content-Disposition Header Field\n\n## IMAP\n\n- [RFC 3501] - Internet Message Access Protocol - Version 4rev1\n    * **Partial**: `\\Recent` flag is not reset sometimes.\n- [RFC 2152] - UTF-7\n\n### Extensions\n\n- [RFC 2595] - Using TLS with IMAP, POP3 and ACAP\n- [RFC 7889] - The IMAP APPENDLIMIT Extension\n- [RFC 3348] - The Internet Message Action Protocol (IMAP4). Child Mailbox\n  Extension\n- [RFC 6851] - Internet Message Access Protocol (IMAP) - MOVE Extension\n- [RFC 6154] - IMAP LIST Extension for Special-Use Mailboxes\n    * **Partial**: Only SPECIAL-USE capability.\n- [RFC 5255] - Internet Message Access Protocol Internationalization\n    * **Partial**: Only I18NLEVEL=1 capability.\n- [RFC 4978] - The IMAP COMPRESS Extension\n- [RFC 3691] - Internet Message Access Protocol (IMAP) UNSELECT command\n- [RFC 2177] - IMAP4 IDLE command\n- [RFC 7888] - IMAP4 Non-Synchronizing Literals\n    * LITERAL+ capability.\n- [RFC 4959] - IMAP Extension for Simple Authentication and Security Layer\n  (SASL) Initial Client Response\n\n## SMTP\n\n- [RFC 2033] - Local Mail Transfer Protocol\n- [RFC 5321] - Simple Mail Transfer Protocol\n- [RFC 6409] - Message Submission for Mail\n\n### Extensions\n\n- [RFC 1870] - SMTP Service Extension for Message Size Declaration\n- [RFC 2920] - SMTP Service Extension for Command Pipelining\n    * Server support only, not used by SMTP client\n- [RFC 2034] - SMTP Service Extension for Returning Enhanced Error Codes\n- [RFC 3207] - SMTP Service Extension for Secure SMTP over Transport Layer\n  Security\n- [RFC 4954] - SMTP Service Extension for Authentication\n- [RFC 6152] - SMTP Extension for 8-bit MIME\n- [RFC 6531] - SMTP Extension for Internationalized Email\n\n### Misc\n\n- [RFC 6522] - The Multipart/Report Content Type for the Reporting of Mail\n  System Administrative Messages\n- [RFC 3464] - An Extensible Message Format for Delivery Status Notifications\n- [RFC 6533] - Internationalized Delivery Status and Disposition Notifications\n\n## Email security\n\n### User authentication\n\n- [RFC 4422] - Simple Authentication and Security Layer (SASL)\n- [RFC 4616] - The PLAIN Simple Authentication and Security Layer (SASL)\n  Mechanism\n\n### Sender authentication\n\n- [RFC 6376] - DomainKeys Identified Mail (DKIM) Signatures\n- [RFC 7001] - Message Header Field for Indicating Message Authentication Status\n- [RFC 7208] - Sender Policy Framework (SPF) for Authorizing Use of Domains in\n  Email, Version 1\n- [RFC 7372] - Email Authentication Status Codes\n- [RFC 7479] - Domain-based Message Authentication, Reporting, and Conformance\n  (DMARC)\n    * **Partial**: No report generation.\n- [RFC 8301] - Cryptographic Algorithm and Key Usage Update to DomainKeys\n  Identified Mail (DKIM)\n- [RFC 8463] - A New Cryptographic Signature Method for DomainKeys Identified\n  Mail (DKIM)\n- [RFC 8616] - Email Authentication for Internationalized Mail\n\n### Recipient authentication\n\n- [RFC 4033] - DNS Security Introduction and Requirements\n- [RFC 6698] - The DNS-Based Authentication of Named Entities (DANE) Transport\n  Layer Security (TLS) Protocol: TLSA\n- [RFC 7672] - SMTP Security via Opportunistic DNS-Based Authentication of\n  Named Entities (DANE) Transport Layer Security (TLS)\n- [RFC 8461] - SMTP MTA Strict Transport Security (MTA-STS)\n\n## Unicode, encodings, internationalization\n\n- [RFC 3492] - Punycode: A Bootstring encoding of Unicode for Internationalized\n  Domain Names in Applications (IDNA)\n- [RFC 3629] - UTF-8, a transformation format of ISO 10646\n- [RFC 5890] - Internationalized Domain Names for Applications (IDNA):\n  Definitions and Document Framework\n- [RFC 5891] - Internationalized Domain Names for Applications (IDNA): Protocol\n- [RFC 7616] - Preparation, Enforcement, and Comparison of Internationalized\n  Strings Representing Usernames and Passwords\n- [RFC 8264] - PRECIS Framework: Preparation, Enforcement, and Comparison of\n  Internationalized Strings in Application Protocols\n- [Unicode 11.0.0]\n    - [UAX #15] - Unicode Normalization Forms\n\nThere is a huge list of non-Unicode encodings supported by message parser used\nfor IMAP static cache and search.  See [Unicode support](unicode.md) page for\ndetails.\n\n## Misc\n\n- [RFC 5782] - DNS Blacklists and Whitelists\n\n\n[GH 188]: https://github.com/foxcpp/maddy/issues/188\n\n[RFC 2822]: https://tools.ietf.org/html/rfc2822\n[RFC 2045]: https://tools.ietf.org/html/rfc2045\n[RFC 2046]: https://tools.ietf.org/html/rfc2046\n[RFC 2047]: https://tools.ietf.org/html/rfc2047\n[RFC 2048]: https://tools.ietf.org/html/rfc2048\n[RFC 2049]: https://tools.ietf.org/html/rfc2049\n[RFC 6532]: https://tools.ietf.org/html/rfc6532\n[RFC 2183]: https://tools.ietf.org/html/rfc2183\n[RFC 3501]: https://tools.ietf.org/html/rfc3501\n[RFC 2152]: https://tools.ietf.org/html/rfc2152\n[RFC 2595]: https://tools.ietf.org/html/rfc2595\n[RFC 7889]: https://tools.ietf.org/html/rfc7889\n[RFC 3348]: https://tools.ietf.org/html/rfc3348\n[RFC 6851]: https://tools.ietf.org/html/rfc6851\n[RFC 6154]: https://tools.ietf.org/html/rfc6154\n[RFC 5255]: https://tools.ietf.org/html/rfc5255\n[RFC 4978]: https://tools.ietf.org/html/rfc4978\n[RFC 3691]: https://tools.ietf.org/html/rfc3691\n[RFC 2177]: https://tools.ietf.org/html/rfc2177\n[RFC 7888]: https://tools.ietf.org/html/rfc7888\n[RFC 4959]: https://tools.ietf.org/html/rfc4959\n[RFC 2033]: https://tools.ietf.org/html/rfc2033\n[RFC 5321]: https://tools.ietf.org/html/rfc5321\n[RFC 6409]: https://tools.ietf.org/html/rfc6409\n[RFC 1870]: https://tools.ietf.org/html/rfc1870\n[RFC 2920]: https://tools.ietf.org/html/rfc2920\n[RFC 2034]: https://tools.ietf.org/html/rfc2034\n[RFC 3207]: https://tools.ietf.org/html/rfc3207\n[RFC 4954]: https://tools.ietf.org/html/rfc4954\n[RFC 6152]: https://tools.ietf.org/html/rfc6152\n[RFC 6531]: https://tools.ietf.org/html/rfc6531\n[RFC 6522]: https://tools.ietf.org/html/rfc6522\n[RFC 3464]: https://tools.ietf.org/html/rfc3464\n[RFC 6533]: https://tools.ietf.org/html/rfc6533\n[RFC 4422]: https://tools.ietf.org/html/rfc4422\n[RFC 4616]: https://tools.ietf.org/html/rfc4616\n[RFC 6376]: https://tools.ietf.org/html/rfc6376\n[RFC 7001]: https://tools.ietf.org/html/rfc7001\n[RFC 7208]: https://tools.ietf.org/html/rfc7208\n[RFC 7372]: https://tools.ietf.org/html/rfc7372\n[RFC 7479]: https://tools.ietf.org/html/rfc7479\n[RFC 8301]: https://tools.ietf.org/html/rfc8301\n[RFC 8463]: https://tools.ietf.org/html/rfc8463\n[RFC 8616]: https://tools.ietf.org/html/rfc8616\n[RFC 4033]: https://tools.ietf.org/html/rfc4033\n[RFC 6698]: https://tools.ietf.org/html/rfc6698\n[RFC 7672]: https://tools.ietf.org/html/rfc7672\n[RFC 8461]: https://tools.ietf.org/html/rfc8461\n[RFC 3492]: https://tools.ietf.org/html/rfc3492\n[RFC 3629]: https://tools.ietf.org/html/rfc3629\n[RFC 5890]: https://tools.ietf.org/html/rfc5890\n[RFC 5891]: https://tools.ietf.org/html/rfc5891\n[RFC 7616]: https://tools.ietf.org/html/rfc7616\n[RFC 8264]: https://tools.ietf.org/html/rfc8264\n[RFC 5782]: https://tools.ietf.org/html/rfc5782\n[RFC 2822]: https://tools.ietf.org/html/rfc2822\n[RFC 2045]: https://tools.ietf.org/html/rfc2045\n[RFC 2046]: https://tools.ietf.org/html/rfc2046\n[RFC 2047]: https://tools.ietf.org/html/rfc2047\n[RFC 2048]: https://tools.ietf.org/html/rfc2048\n[RFC 2049]: https://tools.ietf.org/html/rfc2049\n[RFC 6532]: https://tools.ietf.org/html/rfc6532\n[RFC 3501]: https://tools.ietf.org/html/rfc3501\n[RFC 2595]: https://tools.ietf.org/html/rfc2595\n[RFC 7889]: https://tools.ietf.org/html/rfc7889\n[RFC 3348]: https://tools.ietf.org/html/rfc3348\n[RFC 6851]: https://tools.ietf.org/html/rfc6851\n[RFC 6154]: https://tools.ietf.org/html/rfc6154\n[RFC 5255]: https://tools.ietf.org/html/rfc5255\n[RFC 4978]: https://tools.ietf.org/html/rfc4978\n[RFC 3691]: https://tools.ietf.org/html/rfc3691\n[RFC 2177]: https://tools.ietf.org/html/rfc2177\n[RFC 7888]: https://tools.ietf.org/html/rfc7888\n[RFC 4959]: https://tools.ietf.org/html/rfc4959\n[RFC 2033]: https://tools.ietf.org/html/rfc2033\n[RFC 5321]: https://tools.ietf.org/html/rfc5321\n[RFC 6409]: https://tools.ietf.org/html/rfc6409\n[RFC 1870]: https://tools.ietf.org/html/rfc1870\n[RFC 2920]: https://tools.ietf.org/html/rfc2920\n[RFC 2034]: https://tools.ietf.org/html/rfc2034\n[RFC 3207]: https://tools.ietf.org/html/rfc3207\n[RFC 4954]: https://tools.ietf.org/html/rfc4954\n[RFC 6152]: https://tools.ietf.org/html/rfc6152\n[RFC 6531]: https://tools.ietf.org/html/rfc6531\n[RFC 6522]: https://tools.ietf.org/html/rfc6522\n[RFC 3464]: https://tools.ietf.org/html/rfc3464\n[RFC 6533]: https://tools.ietf.org/html/rfc6533\n[RFC 4422]: https://tools.ietf.org/html/rfc4422\n[RFC 4616]: https://tools.ietf.org/html/rfc4616\n[RFC 6376]: https://tools.ietf.org/html/rfc6376\n[RFC 7001]: https://tools.ietf.org/html/rfc7001\n[RFC 7208]: https://tools.ietf.org/html/rfc7208\n[RFC 7372]: https://tools.ietf.org/html/rfc7372\n[RFC 7479]: https://tools.ietf.org/html/rfc7479\n[RFC 8301]: https://tools.ietf.org/html/rfc8301\n[RFC 8463]: https://tools.ietf.org/html/rfc8463\n[RFC 8616]: https://tools.ietf.org/html/rfc8616\n[RFC 4033]: https://tools.ietf.org/html/rfc4033\n[RFC 6698]: https://tools.ietf.org/html/rfc6698\n[RFC 7672]: https://tools.ietf.org/html/rfc7672\n[RFC 8461]: https://tools.ietf.org/html/rfc8461\n[RFC 3492]: https://tools.ietf.org/html/rfc3492\n[RFC 3629]: https://tools.ietf.org/html/rfc3629\n[RFC 5890]: https://tools.ietf.org/html/rfc5890\n[RFC 5891]: https://tools.ietf.org/html/rfc5891\n[RFC 7616]: https://tools.ietf.org/html/rfc7616\n[RFC 8264]: https://tools.ietf.org/html/rfc8264\n[RFC 5782]: https://tools.ietf.org/html/rfc5782\n[RFC 2822]: https://tools.ietf.org/html/rfc2822\n[RFC 2045]: https://tools.ietf.org/html/rfc2045\n[RFC 2046]: https://tools.ietf.org/html/rfc2046\n[RFC 2047]: https://tools.ietf.org/html/rfc2047\n[RFC 2048]: https://tools.ietf.org/html/rfc2048\n[RFC 2049]: https://tools.ietf.org/html/rfc2049\n[RFC 6532]: https://tools.ietf.org/html/rfc6532\n[RFC 3501]: https://tools.ietf.org/html/rfc3501\n[RFC 2595]: https://tools.ietf.org/html/rfc2595\n[RFC 7889]: https://tools.ietf.org/html/rfc7889\n[RFC 3348]: https://tools.ietf.org/html/rfc3348\n[RFC 6851]: https://tools.ietf.org/html/rfc6851\n[RFC 6154]: https://tools.ietf.org/html/rfc6154\n[RFC 5255]: https://tools.ietf.org/html/rfc5255\n[RFC 4978]: https://tools.ietf.org/html/rfc4978\n[RFC 3691]: https://tools.ietf.org/html/rfc3691\n[RFC 2177]: https://tools.ietf.org/html/rfc2177\n[RFC 7888]: https://tools.ietf.org/html/rfc7888\n[RFC 4959]: https://tools.ietf.org/html/rfc4959\n[RFC 2033]: https://tools.ietf.org/html/rfc2033\n[RFC 5321]: https://tools.ietf.org/html/rfc5321\n[RFC 6409]: https://tools.ietf.org/html/rfc6409\n[RFC 1870]: https://tools.ietf.org/html/rfc1870\n[RFC 2920]: https://tools.ietf.org/html/rfc2920\n[RFC 2034]: https://tools.ietf.org/html/rfc2034\n[RFC 3207]: https://tools.ietf.org/html/rfc3207\n[RFC 4954]: https://tools.ietf.org/html/rfc4954\n[RFC 6152]: https://tools.ietf.org/html/rfc6152\n[RFC 6531]: https://tools.ietf.org/html/rfc6531\n[RFC 6522]: https://tools.ietf.org/html/rfc6522\n[RFC 3464]: https://tools.ietf.org/html/rfc3464\n[RFC 6533]: https://tools.ietf.org/html/rfc6533\n[RFC 4422]: https://tools.ietf.org/html/rfc4422\n[RFC 4616]: https://tools.ietf.org/html/rfc4616\n[RFC 6376]: https://tools.ietf.org/html/rfc6376\n[RFC 8301]: https://tools.ietf.org/html/rfc8301\n[RFC 8463]: https://tools.ietf.org/html/rfc8463\n[RFC 7208]: https://tools.ietf.org/html/rfc7208\n[RFC 7372]: https://tools.ietf.org/html/rfc7372\n[RFC 7479]: https://tools.ietf.org/html/rfc7479\n[RFC 8616]: https://tools.ietf.org/html/rfc8616\n[RFC 4033]: https://tools.ietf.org/html/rfc4033\n[RFC 6698]: https://tools.ietf.org/html/rfc6698\n[RFC 7672]: https://tools.ietf.org/html/rfc7672\n[RFC 8461]: https://tools.ietf.org/html/rfc8461\n[RFC 3492]: https://tools.ietf.org/html/rfc3492\n[RFC 3629]: https://tools.ietf.org/html/rfc3629\n[RFC 5890]: https://tools.ietf.org/html/rfc5890\n[RFC 5891]: https://tools.ietf.org/html/rfc5891\n[RFC 7616]: https://tools.ietf.org/html/rfc7616\n[RFC 8264]: https://tools.ietf.org/html/rfc8264\n[RFC 5782]: https://tools.ietf.org/html/rfc5782\n\n[Unicode 11.0.0]: https://www.unicode.org/versions/components-11.0.0.html\n[UAX #15]: https://unicode.org/reports/tr15/\n"
  },
  {
    "path": "docs/internals/sqlite.md",
    "content": "# maddy & SQLite\n\nSQLite is a perfect choice for small deployments because no additional\nconfiguration is required to get started. It is recommended for cases when you\nhave less than 10 mail accounts.\n\n**Note: SQLite requires DB-wide locking for writing, it means that multiple\nmessages can't be accepted in parallel. This is not the case for server-based\nRDBMS where maddy can accept multiple messages in parallel even for a single\nmailbox.**\n\n## WAL mode\n\nmaddy forces WAL journal mode for SQLite. This makes things reasonably fast and\nreduces locking contention which may be important for a typical mail server.\n\nmaddy uses increased WAL autocheckpoint interval. This means that while\nmaintaining a high write throughput, maddy will have to stop for a bit (0.5-1\nsecond) every time 78 MiB is written to the DB (with default configuration it\nis 15 MiB).\n\nNote that when moving the database around you need to move WAL journal (`-wal`)\nand shared memory (`-shm`) files as well, otherwise some changes to the DB will\nbe lost.\n\n## Query planner statistics\n\nmaddy updates query planner statistics on shutdown and every 5 hours. It\nprovides query planner with information to access the database in more\nefficient way because go-imap-sql schema does use a few so called \"low-quality\nindexes\".\n\n## Auto-vacuum\n\nmaddy turns on SQLite auto-vacuum feature. This means that database file size\nwill shrink when data is removed (compared to default when it remains unused).\n\n## Manual vacuuming\n\nAuto-vacuuming can lead to database fragmentation and thus reduce the read\nperformance.  To do manual vacuum operation to repack and defragment database\nfile, install the SQLite3 console utility and run the following commands:\n```\nsqlite3 -cmd 'vacuum' database_file_path_here.db\nsqlite3 -cmd 'pragma wal_checkpoint(truncate)' database_file_path_here.db\n```\n\nIt will take some time to complete, you can close the utility when the\n`sqlite>` prompt appears.\n"
  },
  {
    "path": "docs/internals/unicode.md",
    "content": "# Unicode support\n\nmaddy has the first-class Unicode support in all components (modules). You do\nnot have to take any actions to make it work with internationalized domains,\nmailbox names or non-ASCII message headers.\n\nInternally, all text fields in maddy are represented in UTF-8 and handled using\nUnicode-aware operations for comparisons, case-folding and so on.\n\n## Non-ASCII data in message headers and bodies\n\nmaddy SMTP implementation does not care about encodings used in MIME headers or\nin `Content-Type text/*` charset field.\n\nHowever, local IMAP storage implementation needs to perform certain operations\non header contents. This is mostly about SEARCH functionality. For IMAP search\nto work correctly, the message body and headers should use one of the following\nencodings:\n\n- ASCII\n- UTF-8\n- ISO-8859-1, 2, 3, 4, 9, 10, 13, 14, 15 or 16\n- Windows-1250, 1251 or 1252 (aka Code Page 1250 and so on)\n- KOI8-R\n- ~~HZGB2312~~, GB18030\n- GBK (aka Code Page 936)\n- Shift JIS (aka Code Page 932 or Windows-31J)\n- Big-5 (aka Code Page 950)\n- EUC-JP\n- ISO-2022-JP\n\n_Support for HZGB2312 is currently disabled due to bugs with security\nimplications._\n\nIf mailbox includes a message with any encoding not listed here, it will not\nbe returned in search results for any request.\n\nBehavior regarding handling of non-Unicode encodings is not considered stable\nand may change between versions (including removal of supported encodings). If\nyou need your stuff to work correctly - start using UTF-8.\n\n## Configuration files\n\nmaddy configuration files are assumed to be encoded in UTF-8. Use of any other\nencoding will break stuff, do not do it.\n\nDomain names (e.g. in hostname directive or pipeline rules) can be represented\nusing the ACE form (aka Punycode). They will be converted to the Unicode form\ninternally.\n\n## Local credentials\n\n'sql' storage backend and authentication provider enforce a number of additional\nconstraints on used account names.\n\nPRECIS UsernameCaseMapped profile is enforced for local email addresses.\nIt limits the use of control and Bidi characters to make sure the used value\ncan be represented consistently in a variety of contexts. On top of that, the\naddress is case-folded and normalized to the NFC form for consistent internal\nhandling.\n\nPRECIS OpaqueString profile is enforced for passwords. Less strict rules are\napplied here. Runs of Unicode whitespace characters are replaced with a single\nASCII space. NFC normalization is applied afterwards. If the resulting string\nis empty - the password is not accepted.\n\nBoth profiles are defined in RFC 8265, consult it for details.\n\n## Protocol support\n\n### SMTPUTF8 extension\n\nmaddy SMTP implementation includes support for the SMTPUTF8 extension as\ndefined in RFC 6531.\n\nThis means maddy can handle internationalized mailbox and domain names in MAIL\nFROM, RCPT TO commands both for outbound and inbound delivery.\n\nmaddy will not accept messages with non-ASCII envelope addresses unless\nSMTPUTF8 support is requested. If a message with SMTPUTF8 flag set is forwarded\nto a server without SMTPUTF8 support, delivery will fail unless it is possible\nto represent envelope addresses in the ASCII form (only domains use Unicode and\nthey can be converted to Punycode). Contents of message body (and header) are\nnot considered and always accepted and sent as-is, no automatic downgrading or\nreencoding is done.\n\n### IMAP UTF8, I18NLEVEL extensions\n\nCurrently, maddy does not include support for UTF8 and I18NLEVEL IMAP\nextensions. However, it is not a problem that can prevent it from correctly\nhandling UTF-8 messages (or even messages in other non-ASCII encodings\nmentioned above).\n\nClients that want to implement proper handling for Unicode strings may assume\nmaddy does not handle them properly in e.g. SEARCH commands and so such clients\nmay download messages and process them locally.\n"
  },
  {
    "path": "docs/man/.gitignore",
    "content": "_generated_*.md\n"
  },
  {
    "path": "docs/man/README.md",
    "content": "maddy manual pages\n-------------------\n\nThe reference documentation is maintained in the scdoc format and is compiled\ninto a set of Unix man pages viewable using the standard `man` utility.\n\nSee https://git.sr.ht/~sircmpwn/scdoc for information about the tool used to\nbuild pages.\nIt can be used as follows:\n```\nscdoc < maddy-filters.5.scd > maddy-filters.5\nman ./maddy-filters.5\n```\n\nbuild.sh script in the repo root compiles and installs man pages if the scdoc\nutility is installed in the system.\n"
  },
  {
    "path": "docs/man/maddy.1.scd",
    "content": "maddy(1) \"maddy mail server\" \"maddy reference documentation\"\n\n; TITLE Command line arguments\n\n# Name\n\nmaddy - Composable all-in-one mail server.\n\n# Synopsis\n\n*maddy* [options...]\n\n# Description\n\nMaddy is Mail Transfer agent (MTA), Mail Delivery Agent (MDA), Mail Submission\nAgent (MSA), IMAP server and a set of other essential protocols/schemes\nnecessary to run secure email server implemented in one executable.\n\n# Command line arguments\n\n*-h, -help*\n\tShow help message and exit.\n\n*-config* _path_\n\tPath to the configuration file. Default is /etc/maddy/maddy.conf.\n\n*-libexec* _path_\n\tPath to the libexec directory. Helper executables will be searched here.\n\tDefault is /usr/lib/maddy.\n\n*-log* _targets..._\n\tComma-separated list of logging targets. Valid values are the same as the\n\t'log' config directive. Affects logging before configuration parsing\n\tcompletes and after it, if the different value is not specified in the\n\tconfiguration.\n\n*-debug*\n\tEnable debug log. You want to use it when reporting bugs.\n\n*-v*\n\tPrint version & build metadata.\n"
  },
  {
    "path": "docs/man/prepare_md.py",
    "content": "#!/usr/bin/python3\n\n\"\"\"\nThis script does all necessary pre-processing to convert scdoc format into\nMarkdown.\n\nUsage:\n    prepare_md.py < in > out\n    prepare_md.py file1 file2 file3\n        Converts into _generated_file1.md, etc.\n\"\"\"\n\nimport sys\nimport re\n\nanchor_escape = str.maketrans(r' #()./\\+-_', '__________')\n\ndef prepare(r, w):\n    new_lines = list()\n    title = str()\n    previous_h1_anchor = ''\n\n    inside_literal = False\n\n    for line in r:\n        if not inside_literal:\n            if line.startswith('; TITLE ') and title == '':\n                title = line[8:]\n            if line[0] == ';':\n                continue\n            # turn *page*(1) into [**page(1)**](../_generated_page.1)\n            line = re.sub(r'\\*(.+?)\\*\\(([0-9])\\)', r'[*\\1(\\2)*](../_generated_\\1.\\2)', line)\n            # *aaa* => **aaa**\n            line = re.sub(r'\\*(.+?)\\*', r'**\\1**', line)\n            # remove ++ from line endings\n            line = re.sub(r'\\+\\+$', '<br>', line)\n            # turn whatever looks like a link into one\n            line = re.sub(r'(https://[^ \\)\\(\\\\]+[a-z0-9_\\-])', r'[\\1](\\1)', line)\n            # escape underscores inside words\n            line = re.sub(r'([^ ])_([^ ])', r'\\1\\\\_\\2', line)\n\n        if line.startswith('```'):\n            inside_literal = not inside_literal\n\n        new_lines.append(line)\n\n    if title != '':\n        print('#', title, file=w)\n\n    print(''.join(new_lines[1:]), file=w)\n\nif len(sys.argv) == 1:\n    prepare(sys.stdin, sys.stdout)\nelse:\n    for f in sys.argv[1:]:\n        new_name = '_generated_' + f[:-4] + '.md'\n        prepare(open(f, 'r'), open(new_name, 'w'))\n"
  },
  {
    "path": "docs/multiple-domains.md",
    "content": "# Multiple domains configuration\n\nBy default, maddy uses email addresses as account identifiers for both\nauthentication and storage purposes. Therefore, account named `user@example.org`\nis completely independent from `user@example.com`. They must be created\nseparately, may have different credentials and have separate IMAP mailboxes.\n\nThis makes it extremely easy to setup maddy to manage multiple otherwise\nindependent domains.\n\nDefault configuration file contains two macros - `$(primary_domain)` and\n`$(local_domains)`. They are used to used in several places thorough the\nfile to configure message routing, security checks, etc.\n\nIn general, you should just add all domains you want maddy to manage to\n`$(local_domains)`, like this:\n```\n$(primary_domain) = example.org\n$(local_domains) = $(primary_domain) example.com\n```\nNote that you need to pick one domain as a \"primary\" for use in\nauto-generated messages.\n\nWith that done, you can create accounts using both domains in the name, send\nand receive messages and so on.  Do not forget to configure corresponding SPF,\nDMARC and MTA-STS records as was recommended in\nthe [introduction tutorial](tutorials/setting-up.md).\n\nAlso note that you do not really need a separate TLS certificate for each\nmanaged domain. You can have one hostname e.g. mail.example.org set as an MX\nrecord for multiple domains.\n\n**If you want multiple domains to share username namespace**, you should change\nseveral more options.\n\nYou can make \"user@example.org\" and \"user@example.com\" users share the same\ncredentials of user \"user\" but have different IMAP mailboxes (\"user@example.org\"\nand \"user@example.com\" correspondingly). For that, it is enough to set `auth_map`\nglobally to use `email_localpart` table:\n```\nauth_map email_localpart\n```\nThis way, when user logs in as \"user@example.org\", \"user\" will be passed\nto the authentication provider, but \"user@example.org\" will be passed to the\nstorage backend. You should create accounts like this:\n```\nmaddy creds create user\nmaddy imap-acct create user@example.org\nmaddy imap-acct create user@example.com\n```\n\n**If you want accounts to also share the same IMAP storage of account named\n\"user\"**, you can set `storage_map` in IMAP endpoint and `delivery_map` in\nstorage backend to use `email_locapart`:\n```\nstorage.imapsql local_mailboxes {\n   ...\n   delivery_map email_localpart # deliver \"user@*\" to \"user\"\n}\nimap tls://0.0.0.0:993 {\n   ...\n   storage &local_mailboxes\n   ...\n   storage_map email_localpart # \"user@*\" accesses \"user\" mailbox\n}\n```\n\nYou also might want to make it possible to log in without\nspecifying a domain at all. In this case, use `email_localpart_optional` for\nboth `auth_map` and `storage_map`.\n\nYou also need to make `authorize_sender` check (used in `submission` endpoint)\naccept non-email usernames:\n```\nauthorize_sender {\n  ...\n  user_to_email chain {\n    step email_localpart_optional           # remove domain from username if present\n    step email_with_domain $(local_domains) # expand username with all allowed domains\n  }\n}\n```\n\n## TL;DR\n\nYour options:\n\n**\"user@example.org\" and \"user@example.com\" have distinct credentials and\ndistinct mailboxes.**\n\n```\n$(primary_domain) = example.org\n$(local_domains) = example.org example.com\n```\n\nCreate accounts as:\n\n```shell\nmaddy creds create user@example.org\nmaddy imap-acct create user@example.org\nmaddy creds create user@example.com\nmaddy imap-acct create user@example.com\n```\n\n**\"user@example.org\" and \"user@example.com\" have same credentials but\ndistinct mailboxes.**\n\n```\n$(primary_domain) = example.org\n$(local_domains) = example.org example.com\nauth_map email_localpart\n```\n\nCreate accounts as:\n```shell\nmaddy creds create user\nmaddy imap-acct create user@example.org\nmaddy imap-acct create user@example.com\n```\n\n**\"user@example.org\", \"user@example.com\", \"user\" have same credentials and same\nmailboxes.**\n\n```\n   $(primary_domain) = example.org\n   $(local_domains) = example.org example.com\n   auth_map email_localpart_optional # authenticating as \"user@*\" checks credentials for \"user\"\n\n   storage.imapsql local_mailboxes {\n      ...\n      delivery_map email_localpart_optional # deliver \"user@*\" to \"user\" mailbox\n   }\n\n   imap tls://0.0.0.0:993 {\n      ...\n      storage_map email_localpart_optional # authenticating as \"user@*\" accesses \"user\" mailboxes\n   }\n\n   submission tls://0.0.0.0:465 {\n      check {\n        authorize_sender {\n          ...\n          user_to_email chain {\n            step email_localpart_optional           # remove domain from username if present\n            step email_with_domain $(local_domains) # expand username with all allowed domains\n          }\n        }\n      }\n      ...\n   }\n```\n\nCreate accounts as:\n```shell\nmaddy creds create user\nmaddy imap-acct create user\n```\n"
  },
  {
    "path": "docs/reference/auth/dovecot_sasl.md",
    "content": "# Dovecot SASL\n\nThe 'auth.dovecot_sasl' module implements the client side of the Dovecot\nauthentication protocol, allowing maddy to use it as a credentials source.\n\nCurrently SASL mechanisms support is limited to mechanisms supported by maddy\nso you cannot get e.g. SCRAM-MD5 this way.\n\n```\nauth.dovecot_sasl {\n\tendpoint unix://socket_path\n}\n\ndovecot_sasl unix://socket_path\n```\n\n## Configuration directives\n\n### endpoint _schema://address_\nDefault: not set\n\nSet the address to use to contact Dovecot SASL server in the standard endpoint\nformat.\n\n`tcp://10.0.0.1:2222` for TCP, `unix:///var/lib/dovecot/auth.sock` for Unix\ndomain sockets.\n"
  },
  {
    "path": "docs/reference/auth/external.md",
    "content": "# System command\n\nauth.external module for authentication using external helper binary. It looks for binary\nnamed `maddy-auth-helper` in $PATH and libexecdir and uses it for authentication\nusing username/password pair.\n\nThe protocol is very simple:\nProgram is launched for each authentication. Username and password are written\nto stdin, adding \\n to the end. If binary exits with 0 status code -\nauthentication is considered successful. If the status code is 1 -\nauthentication is failed. If the status code is 2 - another unrelated error has\nhappened. Additional information should be written to stderr.\n\n```\nauth.external {\n    helper /usr/bin/ldap-helper\n    perdomain no\n    domains example.org\n}\n```\n\n## Configuration directives\n\n### helper _file_path_\n\n**Required.** <br>\nLocation of the helper binary. \n\n---\n\n### perdomain _boolean_\nDefault: `no`\n\nDon't remove domain part of username when authenticating and require it to be\npresent. Can be used if you want user@domain1 and user@domain2 to be different\naccounts.\n\n---\n\n### domains _domains..._\nDefault: not specified\n\nDomains that should be allowed in username during authentication.\n\nFor example, if 'domains' is set to \"domain1 domain2\", then\nusername, username@domain1 and username@domain2 will be accepted as valid login\nname in addition to just username.\n\nIf used without 'perdomain', domain part will be removed from login before\ncheck with underlying auth. mechanism. If 'perdomain' is set, then\ndomains must be also set and domain part **will not** be removed before check.\n\n"
  },
  {
    "path": "docs/reference/auth/ldap.md",
    "content": "# LDAP BindDN\n\nmaddy supports authentication via LDAP using DN binding. Passwords are verified\nby the LDAP server.\n\nmaddy needs to know the DN to use for binding. It can be obtained either by\ndirectory search or template .\n\nNote that storage backends conventionally use email addresses, if you use\nnon-email identifiers as usernames then you should map them onto\nemails on delivery by using `auth_map` (see documentation page for used storage backend).\n\nauth.ldap also can be a used as a table module. This way you can check\nwhether the account exists. It works only if DN template is not used.\n\n```\nauth.ldap {\n    urls ldap://maddy.test:389\n\n    # Specify initial bind credentials. Not required ('bind off')\n    # if DN template is used.\n    bind plain \"cn=maddy,ou=people,dc=maddy,dc=test\" \"123456\"\n\n    # Specify DN template to skip lookup.\n    dn_template \"cn={username},ou=people,dc=maddy,dc=test\"\n\n    # Specify base_dn and filter to lookup DN.\n    base_dn \"ou=people,dc=maddy,dc=test\"\n    filter \"(&(objectClass=posixAccount)(uid={username}))\"\n\n    tls_client { ... }\n    starttls off\n    debug off\n    connect_timeout 1m\n}\n```\n```\nauth.ldap ldap://maddy.test.389 {\n    ...\n}\n```\n\n## Configuration directives\n\n### urls _servers..._\n\n**Required.**\n\nURLs of the directory servers to use. First available server\nis used - no load-balancing is done.\n\nURLs should use `ldap://`, `ldaps://`, `ldapi://` schemes.\n\n---\n\n### bind `off` | `unauth` | `external` | `plain` _username_ _password_\n\nDefault: `off`\n\nCredentials to use for initial binding. Required if DN lookup is used.\n\n`unauth` performs unauthenticated bind. `external` performs external binding\nwhich is useful for Unix socket connections (`ldapi://`) or TLS client certificate\nauthentication (cert. is set using tls_client directive). `plain` performs a\nsimple bind using provided credentials.\n\n---\n\n### dn_template _template_\n\nDN template to use for binding. `{username}` is replaced with the\nusername specified by the user.\n\n---\n\n### base_dn _dn_\n\nBase DN to use for lookup.\n\n---\n\n### filter _str_\n\nDN lookup filter. `{username}` is replaced with the username specified\nby the user.\n\nExample:\n\n```\n(&(objectClass=posixAccount)(uid={username}))\n```\n\nExample (using ActiveDirectory):\n\n```\n(&(objectCategory=Person)(memberOf=CN=user-group,OU=example,DC=example,DC=org)(sAMAccountName={username})(!(UserAccountControl:1.2.840.113556.1.4.803:=2)))\n```\n\nExample:\n\n```\n(&(objectClass=Person)(mail={username}))\n```\n\n---\n\n### starttls _bool_\nDefault: `off`\n\nWhether to upgrade connection to TLS using STARTTLS.\n\n---\n\n### tls_client { ... }\n\nAdvanced TLS client configuration. See [TLS configuration / Client](/reference/tls/#client) for details.\n\n---\n\n### connect_timeout _duration_\nDefault: `1m`\n\nTimeout for initial connection to the directory server.\n\n---\n\n### request_timeout _duration_\nDefault: `1m`\n\nTimeout for each request (binding, lookup).\n"
  },
  {
    "path": "docs/reference/auth/netauth.md",
    "content": "# Native NetAuth\n\nmaddy supports authentication via NetAuth using direct entity\nauthentication checks.  Passwords are verified by the NetAuth server.\n\nmaddy needs to know the Entity ID to use for authentication.  It must\nmatch the string the user provides for the Local Atom part of their\nmail address.\n\nNote that storage backends conventionally use email addresses.  Since NetAuth\nrecommends *nix compatible usernames. You will need to either map email\nidentifiers specified by user to NetAuth Entity IDs using `auth_map` in\nendpoint.smtp/imap configuration (recommended) or you would need to use\n`storage_map` in storage backend configuration to map NetAuth Entity ID\nspecified by user back to appropriate storage backend account names.\n\nauth.netauth also can be used as a table module.  This way you can\ncheck whether the account exists.\n\nNote that the configuration fragment provided below is very sparse.\nThis is because NetAuth expects to read most of its common\nconfiguration values from the system NetAuth config file located at\n`/etc/netauth/config.toml`.\n\n```\nauth.netauth {\n  require_group \"maddy-users\"\n  debug off\n}\n```\n\n```\nauth.netauth {}\n```\n\n## Configuration directives\n\n### require_group _group_\n\nOptional.\n\nGroup that entities must possess to be able to use maddy services.\nThis can be used to provide email to just a subset of the entities\npresent in NetAuth.\n\n---\n\n### debug `on` | `off`\n\nDefault: `off`\n"
  },
  {
    "path": "docs/reference/auth/pam.md",
    "content": "# PAM\n\nauth.pam module implements authentication using libpam. Alternatively it can be configured to\nuse helper binary like auth.external module does.\n\nmaddy should be built with libpam build tag to use this module without\n'use_helper' directive.\n\n```\ngo get -tags 'libpam' ...\n```\n\n```\nauth.pam {\n    debug no\n    use_helper no\n}\n```\n\n## Configuration directives\n\n### debug _boolean_ \nDefault: `no`\n\nEnable verbose logging for all modules. You don't need that unless you are\nreporting a bug.\n\n---\n\n### use_helper _boolean_\nDefault: `no`\n\nUse `LibexecDirectory/maddy-pam-helper` instead of directly calling libpam.\nYou need to use that if:\n\n1. maddy is not compiled with libpam, but `maddy-pam-helper` is built separately.\n2. maddy is running as an unprivileged user and used PAM configuration requires additional privileges (e.g. when using system accounts).\n\nFor 2, you need to make `maddy-pam-helper` binary setuid, see\nREADME.md in source tree for details.\n\nTL;DR (assuming you have the maddy group):\n\n```\nchown root:maddy /usr/lib/maddy/maddy-pam-helper\nchmod u+xs,g+x,o-x /usr/lib/maddy/maddy-pam-helper\n```\n\n"
  },
  {
    "path": "docs/reference/auth/pass_table.md",
    "content": "# Password table\n\nauth.pass_table module implements username:password authentication by looking up the\npassword hash using a table module (maddy-tables(5)). It can be used\nto load user credentials from text file (via table.file module) or SQL query\n(via table.sql_table module).\n\n\nDefinition:\n```\nauth.pass_table [block name] {\n\ttable <table config>\n\n}\n```\nShortened variant for inline use:\n```\npass_table <table> [table arguments] {\n\t[additional table config]\n}\n```\n\nExample, read username:password pair from the text file:\n```\nsmtp tcp://0.0.0.0:587 {\n\tauth pass_table file /etc/maddy/smtp_passwd\n\t...\n}\n```\n\n## Password hashes\n\npass_table expects the used table to contain certain structured values with\nhash algorithm name, salt and other necessary parameters.\n\nYou should use `maddy hash` command to generate suitable values.\nSee `maddy hash --help` for details.\n\n## maddy creds\n\nIf the underlying table is a \"mutable\" table (see maddy-tables(5)) then\nthe `maddy creds` command can be used to modify the underlying tables\nvia pass_table module. It will act on a \"local credentials store\" and will write\nappropriate hash values to the table.\n"
  },
  {
    "path": "docs/reference/auth/plain_separate.md",
    "content": "# Separate username and password lookup\n\nauth.plain_separate module implements authentication using username:password pairs but can\nuse zero or more \"table modules\" (maddy-tables(5)) and one or more\nauthentication providers to verify credentials.\n\n```\nauth.plain_separate {\n\tuser ...\n\tuser ...\n\t...\n\tpass ...\n\tpass ...\n\t...\n}\n```\n\nHow it works:\n- Initial username input is normalized using PRECIS UsernameCaseMapped profile.\n- Each table specified with the 'user' directive looked up using normalized\n  username. If match is not found in any table, authentication fails.\n- Each authentication provider specified with the 'pass' directive is tried.\n  If authentication with all providers fails - an error is returned.\n\n## Configuration directives\n\n### user _table-module_\n\nConfiguration block for any module from maddy-tables(5) can be used here.\n\nExample:\n\n```\nuser file /etc/maddy/allowed_users\n```\n\n---\n\n### pass _auth-provider_\n\nConfiguration block for any auth. provider module can be used here, even\n'plain_split' itself.\n\nThe used auth. provider must provide username:password pair-based\nauthentication.\n"
  },
  {
    "path": "docs/reference/auth/shadow.md",
    "content": "# /etc/shadow\n\nauth.shadow module implements authentication by reading /etc/shadow. Alternatively it can be\nconfigured to use helper binary like auth.external does.\n\n```\nauth.shadow {\n    debug no\n    use_helper no\n}\n```\n\n## Configuration directives\n\n### debug _boolean_\n\nDefault: `no`\n\nEnable verbose logging for all modules. You don't need that unless you are\nreporting a bug.\n\n---\n\n### use_helper _boolean_\nDefault: `no`\n\nUse `LibexecDirectory/maddy-shadow-helper` instead of directly reading `/etc/shadow`.\nYou need to use that if maddy is running as an unprivileged user\nprivileges (e.g. when using system accounts).\n\nYou need to make `maddy-shadow-helper` binary setuid, see\ncmd/maddy-shadow-helper/README.md in source tree for details.\n\nTL;DR (assuming you have maddy group):\n\n```\nchown root:maddy /usr/lib/maddy/maddy-shadow-helper\nchmod u+xs,g+x,o-x /usr/lib/maddy/maddy-shadow-helper\n```\n\n"
  },
  {
    "path": "docs/reference/blob/fs.md",
    "content": "# Filesystem\n\nThis module stores message bodies in a file system directory.\n\n```\nstorage.blob.fs {\n    root <directory>\n}\n```\n\n```\nstorage.blob.fs <directory>\n```\n\n## Configuration directives\n\n### root _path_\nDefault: not set\n\nPath to the FS directory. Must be readable and writable by the server process.\nIf it does not exist - it will be created (parent directory should be writable\nfor this). Relative paths are interpreted relatively to server state directory.\n\n"
  },
  {
    "path": "docs/reference/blob/s3.md",
    "content": "# Amazon S3\n\nstorage.blob.s3 module stores messages bodies in a bucket on S3-compatible storage.\n\n```\nstorage.blob.s3 {\n    endpoint play.min.io\n    secure yes\n    access_key \"Q3AM3UQ867SPQQA43P2F\"\n    secret_key \"zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG\"\n    bucket maddy-test\n\n    # optional\n    region eu-central-1\n    object_prefix maddy/\n    creds access_key\n}\n```\n\nExample:\n\n```\nstorage.imapsql local_mailboxes {\n    ...\n    msg_store s3 {\n        endpoint s3.amazonaws.com\n        access_key \"...\"\n        secret_key \"...\"\n        bucket maddy-messages\n        region us-west-2\n        creds access_key\n    }\n}\n```\n\n## Configuration directives\n\n### endpoint _address:port_\n\n**Required**.\n\nRoot S3 endpoint. e.g. `s3.amazonaws.com`\n\n---\n\n### secure _boolean_\nDefault: `yes`\n\nWhether TLS should be used.\n\n---\n\n### access_key _string_<br>secret_key _string_\n\n**Required**.\n\nStatic S3 credentials.\n\n---\n\n### bucket _name_\n\n**Required**.\n\nS3 bucket name. The bucket must exist and\nbe read-writable.\n\n---\n\n### region _string_\nDefault: not set\n\nS3 bucket location. May be called \"endpoint\" in some manuals.\n\n---\n\n### object_prefix _string_\nDefault: empty string\n\nString to add to all keys stored by maddy.\n\nCan be useful when S3 is used as a file system.\n\n---\n\n### creds `access_key` | `file_minio` | `file_aws` | `iam`\nDefault: `access_key`\n\nCredentials to use for accessing the S3 Bucket.\n\nCredential Types:\n\n - `access_key`: use AWS access key and secret access key \n - `file_minio`: use credentials for Minio present at ~/.mc/config.json\n - `file_aws`: use credentials for AWS S3 present at ~/.aws/credentials\n - `iam`: use AWS IAM instance profile for credentials.\n\nBy default, access_key is used with the access key and secret access key present in the config.\n"
  },
  {
    "path": "docs/reference/checks/actions.md",
    "content": "# Check actions\n\nWhen a certain check module thinks the message is \"bad\", it takes some actions\ndepending on its configuration. Most checks follow the same configuration\nstructure and allow following actions to be taken on check failure:\n\n- Do nothing (`action ignore`)\n\nUseful for testing deployment of new checks. Check failures are still logged\nbut they have no effect on message delivery.\n\n- Reject the message (`action reject`)\n\nReject the message at connection time. No bounce is generated locally.\n\n- Quarantine the message (`action quarantine`)\n\nMark message as 'quarantined'. If message is then delivered to the local\nstorage, the storage backend can place the message in the 'Junk' mailbox.\nAnother thing to keep in mind that 'target.remote' module\nwill refuse to send quarantined messages."
  },
  {
    "path": "docs/reference/checks/authorize_sender.md",
    "content": "# MAIL FROM and From authorization\n\nModule check.authorize_sender verifies that envelope and header sender addresses belong\nto the authenticated user. Address ownership is established via table\nthat maps each user account to a email address it is allowed to use.\nThere are some special cases, see `user_to_email` description below.\n\n```\ncheck.authorize_sender {\n    prepare_email identity\n    user_to_email identity\n    check_header yes\n\n    unauth_action reject\n    no_match_action reject\n    malformed_action reject\n    err_action reject\n\n    auth_normalize auto\n    from_normalize auto\n}\n```\n```\ncheck {\n    authorize_sender { ... }\n}\n```\n\n## Configuration directives\n\n### user_to_email _table_\nDefault: `identity`\n\nTable that maps authorization username to the list of sender emails\nthe user is allowed to use.\n\nIn additional to email addresses, the table can contain domain names or\nspecial string \"\\*\" as a value. If the value is a domain - user\nwill be allowed to use any mailbox within it as a sender address.\nIf it is \"\\*\" - user will be allowed to use any address.\n\nBy default, table.identity is used, meaning that username should\nbe equal to the sender email.\n\nBefore username is looked up via the table, normalization algorithm\ndefined by auth_normalize is applied to it.\n\n---\n\n### prepare_email _table_\nDefault: `identity`\n\nTable that is used to translate email addresses before they\nare matched against user_to_email values.\n\nTypically used to allow users to use their aliases as sender\naddresses - prepare_email in this case should translate\naliases to \"canonical\" addresses. This is how it is\ndone in default configuration.\n\nIf table does not contain any mapping for the used sender\naddress, it will be used as is.\n\n---\n\n### check_header _boolean_\nDefault: `yes`\n\nWhether to verify header sender in addition to envelope.\n\nEither Sender or From field value should match the\nauthorization identity.\n\n---\n\n### unauth_action _action_\nDefault: `reject`\n\nWhat to do if the user is not authenticated at all.\n\n---\n\n### no_match_action _action_\nDefault: `reject`\n\nWhat to do if user is not allowed to use the sender address specified.\n\n---\n\n### malformed_action _action_\nDefault: `reject`\n\nWhat to do if From or Sender header fields contain malformed values.\n\n---\n\n### err_action _action_\nDefault: `reject`\n\nWhat to do if error happens during prepare_email or user_to_email lookup.\n\n---\n\n### auth_normalize _action_\nDefault: `auto`\n\nNormalization function to apply to authorization username before\nfurther processing.\n\nAvailable options:\n\n- `auto`                    `precis_casefold_email` for valid emails, `precis_casefold` otherwise.\n- `precis_casefold_email`   PRECIS UsernameCaseMapped profile + U-labels form for domain\n- `precis_casefold`         PRECIS UsernameCaseMapped profile for the entire string\n- `precis_email`            PRECIS UsernameCasePreserved profile + U-labels form for domain\n- `precis`                  PRECIS UsernameCasePreserved profile for the entire string\n- `casefold`                Convert to lower case\n- `noop`                    Nothing\n\nPRECIS profiles are defined by RFC 8265. In short, they make sure\nthat Unicode strings that look the same will be compared as if they were\nthe same. CaseMapped profiles also convert strings to lower case.\n\n---\n\n### from_normalize _action_\nDefault: `auto`\n\nNormalization function to apply to email addresses before\nfurther processing.\n\nAvailable options are same as for `auth_normalize`.\n"
  },
  {
    "path": "docs/reference/checks/command.md",
    "content": "# System command filter\n\nThis module executes an arbitrary system command during a specified stage of\nchecks execution.\n\n```\ncommand executable_name arg0 arg1 ... {\n\trun_on body\n\n\tcode 1 reject\n\tcode 2 quarantine\n}\n```\n\n## Arguments\n\nThe module arguments specify the command to run. If the first argument is not\nan absolute path, it is looked up in the Libexec Directory (/usr/lib/maddy on\nLinux) and in $PATH (in that ordering). Note that no additional handling\nof arguments is done, especially, the command is executed directly, not via the\nsystem shell.\n\nThere is a set of special strings that are replaced with the corresponding\nmessage-specific values:\n\n- `{source_ip}` – IPv4/IPv6 address of the sending MTA.\n- `{source_host}` – Hostname of the sending MTA, from the HELO/EHLO command.\n- `{source_rdns}` – PTR record of the sending MTA IP address.\n- `{msg_id}` – Internal message identifier. Unique for each delivery.\n- `{auth_user}` – Client username, if authenticated using SASL PLAIN\n- `{sender}` – Message sender address, as specified in the MAIL FROM SMTP command.\n- `{rcpts}` – List of accepted recipient addresses, including the currently handled\n  one.\n- `{address}` – Currently handled address. This is a recipient address if the command\n  is called during RCPT TO command handling (`run_on rcpt`) or a sender\n  address if the command is called during MAIL FROM command handling (`run_on\n  sender`).\n\nIf value is undefined (e.g. `{source_ip}` for a message accepted over a Unix\nsocket) or unavailable (the command is executed too early), the placeholder\nis replaced with an empty string. Note that it can not remove the argument.\nE.g. `-i {source_ip}` will not become just `-i`, it will be `-i \"\"`\n\nUndefined placeholders are not replaced.\n\n## Command stdout\n\nThe command stdout must be either empty or contain a valid RFC 5322 header.\nIf it contains a byte stream that does not look a valid header, the message\nwill be rejected with a temporary error.\n\nThe header from stdout will be **prepended** to the message header.\n\n## Configuration directives\n\n### run_on `conn` | `sender` | `rcpt` | `body`\nDefault: `body`\n\nWhen to run the command. This directive also affects the information visible\nfor the message.\n\n- `conn`<br>\n    Run before the sender address (MAIL FROM) is handled.<br>\n    **Stdin**: Empty <br>\n    **Available placeholders**: {source_ip}, {source_host}, {msg_id}, {auth_user}.\n\n- `sender`<br>\n    Run during sender address (MAIL FROM) handling.<br>\n    **Stdin**: Empty <br>\n    **Available placeholders**: conn placeholders + {sender}, {address}.\n    The {address} placeholder contains the MAIL FROM address.\n\n- `rcpt`<br>\n    Run during recipient address (RCPT TO) handling. The command is executed\n    once for each RCPT TO command, even if the same recipient is specified\n    multiple times.<br>\n    **Stdin**: Empty <br>\n    **Available placeholders**: sender placeholders + {rcpts}.\n    The {address} placeholder contains the recipient address.\n\n- `body`<br>\n    Run during message body handling.<br>\n    **Stdin**: The message header + body <br>\n    **Available placeholders**: all except for {address}.\n\n---\n\n### code _integer_ ignore <br>code _integer_ quarantine <br>code _integer_ reject _smtp-code_ _smtp-enhanced-code_ _smtp-message_\n\nThis directive specifies the mapping from the command exit code _integer_ to\nthe message pipeline action.\n\nTwo codes are defined implicitly, exit code 1 causes the message to be rejected\nwith a permanent error, exit code 2 causes the message to be quarantined. Both\nactions can be overridden using the 'code' directive.\n\n"
  },
  {
    "path": "docs/reference/checks/dkim.md",
    "content": "# DKIM\n\nThis is the check module that performs verification of the DKIM signatures\npresent on the incoming messages.\n\n## Configuration directives\n\n```\ncheck.dkim {\n    debug no\n    required_fields From Subject\n    allow_body_subset no\n    no_sig_action ignore\n    broken_sig_action ignore\n\tfail_open no\n}\n```\n\n### debug _boolean_\nDefault: global directive value\n\nLog both successful and unsuccessful check executions instead of just\nunsuccessful.\n\n---\n\n### required_fields _string..._\nDefault: `From Subject`\n\nHeader fields that should be included in each signature. If signature\nlacks any field listed in that directive, it will be considered invalid.\n\nNote that From is always required to be signed, even if it is not included in\nthis directive.\n\n---\n\n### no_sig_action _action_\nDefault: `ignore` (recommended by RFC 6376)\n\nAction to take when message without any signature is received.\n\nNote that DMARC policy of the sender domain can request more strict handling of\nmissing DKIM signatures.\n\n---\n\n### broken_sig_action _action_\nDefault: `ignore` (recommended by RFC 6376)\n\nAction to take when there are not valid signatures in a message.\n\nNote that DMARC policy of the sender domain can request more strict handling of\nbroken DKIM signatures.\n\n---\n\n### fail_open _boolean_\nDefault: `no`\n\nWhether to accept the message if a temporary error occurs during DKIM\nverification. Rejecting the message with a 4xx code will require the sender\nto resend it later in a hope that the problem will be resolved.\n"
  },
  {
    "path": "docs/reference/checks/dnsbl.md",
    "content": "# DNSBL lookup\n\nThe check.dnsbl module implements checking of source IP and hostnames against a set\nof DNS-based Blackhole lists (DNSBLs).\n\nIts configuration consists of module configuration directives and a set\nof blocks specifying lists to use and kind of lookups to perform on them.\n\n```\ncheck.dnsbl {\n    debug no\n    check_early no\n\n    quarantine_threshold 1\n    reject_threshold 1\n\n    # Lists configuration example.\n    dnsbl.example.org {\n        client_ipv4 yes\n        client_ipv6 no\n        ehlo no\n        mailfrom no\n        score 1\n    }\n    hsrbl.example.org {\n        client_ipv4 no\n        client_ipv6 no\n        ehlo yes\n        mailfrom yes\n        score 1\n    }\n    \n    # Example with per-response-code scoring (new in 0.8)\n    zen.spamhaus.org {\n        client_ipv4 yes\n        client_ipv6 yes\n        \n        # SBL - Spamhaus Block List (known spam sources)\n        response 127.0.0.2 127.0.0.3 {\n            score 10\n            message \"Listed in Spamhaus SBL. See https://check.spamhaus.org/\"\n        }\n        \n        # XBL - Exploits Block List (compromised hosts)\n        response 127.0.0.4 127.0.0.5 127.0.0.6 127.0.0.7 {\n            score 10\n            message \"Listed in Spamhaus XBL. See https://check.spamhaus.org/\"\n        }\n        \n        # PBL - Policy Block List (dynamic IPs)\n        response 127.0.0.10 127.0.0.11 {\n            score 5\n            message \"Listed in Spamhaus PBL. See https://check.spamhaus.org/\"\n        }\n    }\n}\n```\n\n## Arguments\n\nArguments specify the list of IP-based BLs to use.\n\nThe following configurations are equivalent.\n\n```\ncheck {\n    dnsbl dnsbl.example.org dnsbl2.example.org\n}\n```\n\n```\ncheck {\n    dnsbl {\n        dnsbl.example.org dnsbl2.example.org {\n            client_ipv4 yes\n            client_ipv6 no\n            ehlo no\n            mailfrom no\n            score 1\n        }\n    }\n}\n```\n\n## Configuration directives\n\n### debug _boolean_\nDefault: global directive value\n\nEnable verbose logging.\n\n---\n\n### check_early _boolean_\nDefault: `no`\n\nCheck BLs before mail delivery starts and silently reject blacklisted clients.\n\nFor this to work correctly, check should not be used in source/destination\npipeline block.\n\nIn particular, this means:\n\n- No logging is done for rejected messages.\n- No action is taken if `quarantine_threshold` is hit, only `reject_threshold`\n  applies.\n- `defer_sender_reject` from SMTP configuration takes no effect.\n- MAIL FROM is not checked, even if specified.\n\nIf you often get hit by spam attacks, it is recommended to enable this\nsetting to save server resources.\n\n---\n\n### quarantine_threshold _integer_\nDefault: `1`\n\nDNSBL score needed (equals-or-higher) to quarantine the message.\n\n---\n\n### reject_threshold _integer_\nDefault: `9999`\n\nDNSBL score needed (equals-or-higher) to reject the message.\n\n## List configuration\n\n```\ndnsbl.example.org dnsbl.example.com {\n    client_ipv4 yes\n    client_ipv6 no\n    ehlo no\n    mailfrom no\n    responses 127.0.0.1/24\n\tscore 1\n}\n```\n\nDirective name and arguments specify the actual DNS zone to query when checking\nthe list. Using multiple arguments is equivalent to specifying the same\nconfiguration separately for each list.\n\n### client_ipv4 _boolean_\nDefault: `yes`\n\nWhether to check address of the IPv4 clients against the list.\n\n---\n\n### client_ipv6 _boolean_\nDefault: `yes`\n\nWhether to check address of the IPv6 clients against the list.\n\n---\n\n### ehlo _boolean_\nDefault: `no`\n\nWhether to check hostname specified n the HELO/EHLO command\nagainst the list.\n\nThis works correctly only with domain-based DNSBLs.\n\n---\n\n### mailfrom _boolean_\nDefault: `no`\n\nWhether to check domain part of the MAIL FROM address against the list.\n\nThis works correctly only with domain-based DNSBLs.\n\n---\n\n### responses _cidr_ | _ip..._\nDefault: `127.0.0.1/24`\n\nIP networks (in CIDR notation) or addresses to permit in list lookup results.\nAddresses not matching any entry in this directives will be ignored.\n\n---\n\n### score _integer_\nDefault: `1`\n\nScore value to add for the message if it is listed.\n\nIf sum of list scores is equals or higher than `quarantine_threshold`, the\nmessage will be quarantined.\n\nIf sum of list scores is equals or higher than `rejected_threshold`, the message\nwill be rejected.\n\nIt is possible to specify a negative value to make list act like a whitelist\nand override results of other blocklists.\n\n**Note:** When using `response` blocks (see below), the score from matching response\nrules is used instead of this flat score value.\n\n---\n\n### response _ip..._\n\nDefines per-response-code rules for scoring and custom messages. This is useful\nfor combined DNSBLs like Spamhaus ZEN that return different codes for different\nlisting types.\n\nThis works for both IP-based lookups (client_ipv4, client_ipv6) and domain-based\nlookups (ehlo, mailfrom).\n\nEach `response` block takes one or more IP addresses or CIDR ranges as arguments\nand contains the following directives:\n\n#### score _integer_\n**Required**\n\nScore to add when this response code is returned. If multiple response codes\nare returned by the DNSBL, and they match different rules, the scores from\nall matched rules are summed together. Each rule is counted only once, even\nif multiple returned IPs match networks within that rule.\n\n#### message _string_\n**Optional**\n\nCustom rejection or quarantine message to include when this response code\nmatches. This message is shown to the client or logged when the threshold\nis reached.\n\n**Example:**\n\n```\nzen.spamhaus.org {\n    client_ipv4 yes\n    \n    # High severity - known spam sources\n    response 127.0.0.2 127.0.0.3 {\n        score 10\n        message \"Listed in Spamhaus SBL\"\n    }\n    \n    # Lower severity - dynamic IPs\n    response 127.0.0.10 127.0.0.11 {\n        score 5\n        message \"Listed in Spamhaus PBL\"\n    }\n}\n```\n\n**Scoring behavior:**\n- If DNSBL returns `127.0.0.2` only → Score: 10 (matches first rule)\n- If DNSBL returns `127.0.0.11` only → Score: 5 (matches second rule)\n- If DNSBL returns both `127.0.0.2` and `127.0.0.11` → Score: 15 (both rules match, scores sum)\n- If DNSBL returns both `127.0.0.2` and `127.0.0.3` → Score: 10 (same rule matches, counted once)\n\n**Backwards compatibility:** When `response` blocks are not used, the legacy\n`responses` and `score` directives work as before.\n"
  },
  {
    "path": "docs/reference/checks/milter.md",
    "content": "# Milter client\n\nThe 'milter' implements subset of Sendmail's milter protocol that can be used\nto integrate external software with maddy.\nmaddy implements version 6 of the protocol, older versions are\nnot supported.\n\nNotable limitations of protocol implementation in maddy include:\n1. Changes of envelope sender address are not supported\n2. Removal and addition of envelope recipients is not supported\n3. Removal and replacement of header fields is not supported\n4. Headers fields can be inserted only on top\n5. Milter does not receive some \"macros\" provided by sendmail.\n\nRestrictions 1 and 2 are inherent to the maddy checks interface and cannot be\nremoved without major changes to it. Restrictions 3, 4 and 5 are temporary due to\nincomplete implementation.\n\n```\ncheck.milter {\n\tendpoint <endpoint>\n\tfail_open false\n}\n\nmilter <endpoint>\n```\n\n## Arguments\n\nWhen defined inline, the first argument specifies endpoint to access milter\nvia. See below.\n\n## Configuration directives\n\n### endpoint _scheme://path_\nDefault: not set\n\nSpecifies milter protocol endpoint to use.\nThe endpoit is specified in standard URL-like format:\n`tcp://127.0.0.1:6669` or `unix:///var/lib/milter/filter.sock`\n\n---\n\n### fail_open _boolean_\nDefault: `false`\n\nToggles behavior on milter I/O errors. If false (\"fail closed\") - message is\nrejected with temporary error code. If true (\"fail open\") - check is skipped.\n\n"
  },
  {
    "path": "docs/reference/checks/misc.md",
    "content": "# Misc checks\n\n## Configuration directives\n\nFollowing directives are defined for all modules listed below.\n\n### fail_action `ignore` | `reject` | `quarantine`\nDefault: `quarantine`\n\nAction to take when check fails. See [Check actions](../actions/) for details.\n\n---\n\n### debug _boolean_\nDefault: global directive value\n\nLog both successful and unsuccessful check executions instead of just\nunsuccessful.\n\n---\n\n### require_mx_record\n\nCheck that domain in MAIL FROM command does have a MX record and none of them\nare \"null\" (contain a single dot as the host).\n\nBy default, quarantines messages coming from servers missing MX records,\nuse `fail_action` directive to change that.\n\n---\n\n### require_matching_rdns\n\nCheck that source server IP does have a PTR record point to the domain\nspecified in EHLO/HELO command.\n\nBy default, quarantines messages coming from servers with mismatched or missing\nPTR record, use `fail_action` directive to change that.\n\n---\n\n### require_tls\n\nCheck that the source server is connected via TLS; either directly, or by using\nthe STARTTLS command.\n\nBy default, rejects messages coming from unencrypted servers. Use the\n`fail_action` directive to change that."
  },
  {
    "path": "docs/reference/checks/rspamd.md",
    "content": "# rspamd\n\nThe 'rspamd' module implements message filtering by contacting the rspamd\nserver via HTTP API.\n\n```\ncheck.rspamd {\n\ttls_client { ... }\n\tapi_path http://127.0.0.1:11333\n\tsettings_id whatever\n\ttag maddy\n\thostname mx.example.org\n\tio_error_action ignore\n\terror_resp_action ignore\n\tadd_header_action quarantine\n\trewrite_subj_action quarantine\n\treject_action reject\n\tsoft_reject_action reject\n\tflags pass_all\n}\n\nrspamd http://127.0.0.1:11333\n```\n\n## Configuration directives\n\n### tls_client { ... }\nDefault: not set\n\nConfigure TLS client if HTTPS is used. See [TLS configuration / Client](/reference/tls/#client) for details.\n\n---\n\n### api_path _url_\nDefault: `http://127.0.0.1:11333`\n\nURL of HTTP API endpoint. Supports both HTTP and HTTPS and can include\npath element.\n\n---\n\n### settings_id _string_\nDefault: not set\n\nSettings ID to pass to the server.\n\n---\n\n### tag _string_\nDefault: `maddy`\n\nValue to send in MTA-Tag header field.\n\n---\n\n### hostname _string_ <br>\nDefault: value of global directive\n\nValue to send in MTA-Name header field.\n\n---\n\n### io_error_action _action_\nDefault: `ignore`\n\nAction to take in case of inability to contact the rspamd server.\n\n---\n\n### error_resp_action _action_\nDefault: `ignore`\n\nAction to take in case of 5xx or 4xx response received from the rspamd server.\n\n---\n\n### add_header_action _action_\nDefault: `quarantine`\n\nAction to take when rspamd requests to \"add header\".\n\nX-Spam-Flag and X-Spam-Score are added to the header irregardless of value.\n\n---\n\n### rewrite_subj_action _action_\nDefault: `quarantine`\n\nAction to take when rspamd requests to \"rewrite subject\".\n\nX-Spam-Flag and X-Spam-Score are added to the header irregardless of value.\n\n---\n\n### reject_action _action_\nDefault: `reject`\n\nAction to take when rspamd requests to \"reject\".\n\n---\n\n### soft_reject_action _action_\nDefault: `reject`\n\nAction to take when rspamd requests to \"soft reject\".\n\n---\n\n### flags _string-list..._\nDefault: `pass_all`\n\nFlags to pass to the rspamd server.\nSee [https://rspamd.com/doc/architecture/protocol.html](https://rspamd.com/doc/architecture/protocol.html) for details.\n"
  },
  {
    "path": "docs/reference/checks/spf.md",
    "content": "# SPF\n\ncheck.spf the check module that verifies whether IP address of the client is\nauthorized to send messages for domain in MAIL FROM address.\n\nSPF statuses are mapped to maddy check actions in a way\nspecified by \\*_action directives. By default, SPF failure \nresults in the message being quarantined and errors (both permanent and \ntemporary) cause message to be rejected.\nAuthentication-Results field is generated irregardless of status.\n\n## DMARC override\n\nIt is recommended by the DMARC standard to don't fail delivery based solely on\nSPF policy and always check DMARC policy and take action based on it.\n\nIf `enforce_early` is `no`, check.spf module will not take any action on SPF\npolicy failure if sender domain does have a DMARC record with 'quarantine' or\n'reject' policy. Instead it will rely on DMARC support to take necesary\nactions using SPF results as an input.\n\nDisabling `enforce_early` without enabling DMARC support will make SPF policies\nno-op and is considered insecure.\n\n## Configuration directives\n\n```\ncheck.spf {\n    debug no\n    enforce_early no\n    fail_action quarantine\n    softfail_action ignore\n    permerr_action reject\n    temperr_action reject\n}\n```\n\n### debug _boolean_\nDefault: global directive value\n\nEnable verbose logging for check.spf.\n\n---\n\n### enforce_early _boolean_\nDefault: `no`\n\nMake policy decision on MAIL FROM stage (before the message body is received).\nThis makes it impossible to apply DMARC override (see above).\n\n---\n\n### none_action `reject` | `quarantine` | `ignore`\nDefault: `ignore`\n\nAction to take when SPF policy evaluates to a 'none' result.\n\nSee [https://tools.ietf.org/html/rfc7208#section-2.6](https://tools.ietf.org/html/rfc7208#section-2.6) for meaning of\nSPF results.\n\n---\n\n### neutral_action `reject` | `quarantine` | `ignore`\nDefault: `ignore`\n\nAction to take when SPF policy evaluates to a 'neutral' result.\n\nSee [https://tools.ietf.org/html/rfc7208#section-2.6](https://tools.ietf.org/html/rfc7208#section-2.6) for meaning of\nSPF results.\n\n---\n\n### fail_action `reject` | `quarantine` | `ignore`\nDefault: `quarantine`\n\nAction to take when SPF policy evaluates to a 'fail' result.\n\n---\n\n### softfail_action `reject` | `quarantine` | `ignore`\nDefault: `ignore`\n\nAction to take when SPF policy evaluates to a 'softfail' result.\n\n---\n\n### permerr_action `reject` | `quarantine` | `ignore`\nDefault: `reject`\n\nAction to take when SPF policy evaluates to a 'permerror' result.\n\n---\n\n### temperr_action `reject` | `quarantine` | `ignore`\nDefault: `reject`\n\nAction to take when SPF policy evaluates to a 'temperror' result.\n"
  },
  {
    "path": "docs/reference/config-syntax.md",
    "content": "# Configuration files syntax\n\n**Note:** This file is a technical document describing how\nmaddy parses configuration files.\n\nConfiguration consists of newline-delimited \"directives\". Each directive can\nhave zero or more arguments.\n\n```\ndirective0\ndirective1 arg0 arg1\n```\n\nAny line starting with # is ignored. Empty lines are ignored too.\n\n## Quoting\n\nStrings with whitespace should be wrapped into double quotes to make sure they\nwill be interpreted as a single argument.\n\n```\ndirective0 two arguments\ndirective1 \"one argument\"\n```\n\nString wrapped in quotes may contain newlines and they will not be interpreted\nas a directive separator.\n\n```\ndirective0 \"one long big\nargument for directive0\"\n```\n\nQuotes and only quotes can be escaped inside literals: \\\\\"\n\nBackslash can be used at the end of line to continue the directve on the next\nline.\n\n## Blocks\n\nA directive may have several subdirectives. They are written in a {-enclosed\nblock like this:\n```\ndirective0 arg0 arg1 {\n    subdirective0 arg0 arg1\n    subdirective1 etc\n}\n```\n\nSubdirectives can have blocks too.\n\n```\ndirective0 {\n    subdirective0 {\n        subdirective2 {\n            a\n            b\n            c\n        }\n    }\n    subdirective1 { }\n}\n```\n\nLevel of nesting is limited, but you should never hit the limit with correct\nconfiguration.\n\nIn most cases, an empty block is equivalent to no block:\n```\ndirective { }\ndirective2 # same as above\n```\n\n## Environment variables\n\nEnvironment variables can be referenced in the configuration using either\n{env:VARIABLENAME} syntax.\n\nNon-existent variables are expanded to empty strings and not removed from\nthe arguments list.  In the following example, directive0 will have one argument\nindependently of whether VAR is defined.\n\n```\ndirective0 {env:VAR}\n```\n\nParse is forgiving and incomplete variable placeholder (e.g. '{env:VAR') will\nbe left as-is. Variables are expanded inside quotes too.\n\n## Snippets & imports\n\nYou can reuse blocks of configuration by defining them as \"snippets\". Snippet\nis just a directive with a block, declared tp top level (not inside any blocks)\nand with a directive name wrapped in curly braces.\n\n```\n(snippetname) {\n    a\n    b\n    c\n}\n```\n\nThe snippet can then be referenced using 'import' meta-directive.\n\n```\nunrelated0\nunrelated1\nimport snippetname\n```\n\nThe above example will be expanded into the following configuration:\n\n```\nunrelated0\nunrelated1\na\nb\nc\n```\n\nImport statement also can be used to include content from other files. It works\nexactly the same way as with snippets but the file path should be used instead.\nThe path can be either relative to the location of the currently processed\nconfiguration file or absolute. If there are both snippet and file with the\nsame name - snippet will be used.\n\n```\n# /etc/maddy/tls.conf\ntls long_path_to_certificate long_path_to_private_key\n\n# /etc/maddy/maddy.conf\nsmtp tcp://0.0.0.0:25 {\n    import tls.conf\n}\n```\n\n```\n# Expanded into:\nsmtp tcp://0.0.0.0:25 {\n    tls long_path_to_certificate long_path_to_private_key\n}\n```\n\nThe imported file can introduce new snippets and they can be referenced in any\nprocessed configuration file.\n\n## Duration values\n\nDirectives that accept duration use the following format: A sequence of decimal\ndigits with an optional fraction and unit suffix (zero can be specified without\na suffix). If multiple values are specified, they will be added.\n\nValid unit suffixes: \"h\" (hours), \"m\" (minutes), \"s\" (seconds), \"ms\" (milliseconds).\nImplementation also accepts us and ns for microseconds and nanoseconds, but these\nvalues are useless in practice.\n\nExamples:\n```\n1h\n1h 5m\n1h5m\n0\n```\n\n## Data size values\n\nSimilar to duration values, but fractions are not allowed and suffixes are different.\n\nValid unit suffixes: \"G\" (gibibyte, 1024^3 bytes), \"M\" (mebibyte, 1024^2 bytes),\n\"K\" (kibibyte, 1024 bytes), \"B\" or \"b\" (byte).\n\nExamples:\n```\n32M\n3M 5K\n5b\n```\n\nAlso note that the following is not valid, unlike Duration values syntax:\n```\n32M5K\n```\n\n## Address Definitions\n\nMaddy configuration uses URL-like syntax to specify network addresses.\n\n- `unix://file_path` – Unix domain socket. Relative paths are relative to runtime directory (`/run/maddy`).\n- `tcp://ADDRESS:PORT` – TCP/IP socket.\n- `tls://ADDRESS:PORT` – TCP/IP socket using TLS.\n\n## Dummy Module\n\nNo-op module. It doesn't need to be configured explicitly and can be referenced\nusing \"dummy\" name. It can act as a delivery target or auth.\nprovider. In the latter case, it will accept any credentials, allowing any\nclient to authenticate using any username and password (use with care!).\n\n\n"
  },
  {
    "path": "docs/reference/endpoints/imap.md",
    "content": "# IMAP4rev1 endpoint\n\nModule 'imap' is a listener that implements IMAP4rev1 protocol and provides\naccess to local messages storage specified by 'storage' directive.\n\nIn most cases, local storage modules will auto-create accounts when they are\naccessed via IMAP. This relies on authentication provider used by IMAP endpoint\nto provide what essentially is access control. There is a caveat, however: this\nauto-creation will not happen when delivering incoming messages via SMTP as\nthere is no authentication to confirm that this account should indeed be\ncreated.\n\n## Configuration directives\n\n```\nimap tcp://0.0.0.0:143 tls://0.0.0.0:993 {\n    tls /etc/ssl/private/cert.pem /etc/ssl/private/pkey.key\n    io_debug no\n    debug no\n    insecure_auth no\n    sasl_login no\n    auth pam\n    storage &local_mailboxes\n    auth_map identity\n    auth_map_normalize auto\n    storage_map identity\n    storage_map_normalize auto\n}\n```\n\n### tls _certificate-path_ _key-path_ { ... }\nDefault: global directive value\n\nTLS certificate & key to use. Fine-tuning of other TLS properties is possible\nby specifying a configuration block and options inside it:\n\n```\ntls cert.crt key.key {\n    protocols tls1.2 tls1.3\n}\n```\n\nSee [TLS configuration / Server](/reference/tls/#server-side) for details.\n\n---\n\n### proxy_protocol _trusted ips..._ { ... }\nDefault: not enabled\n\nEnable use of HAProxy PROXY protocol. Supports both v1 and v2 protocols.\nIf a list of trusted IP addresses or subnets is provided, only connections\nfrom those will be trusted.\n\nTLS for the channel between the proxies and maddy can be configured\nusing a 'tls' directive:\n```\nproxy_protocol {\n    trust 127.0.0.1 ::1 192.168.0.1/24\n    tls &proxy_tls\n}\n```\nNote that the top-level 'tls' directive is not inherited here. If you\nneed TLS on top of the PROXY protocol, securing the protocol header,\nyou must declare TLS explicitly.\n\n---\n\n### io_debug _boolean_\nDefault: `no`\n\nWrite all commands and responses to stderr.\n\n---\n\n### io_errors _boolean_\nDefault: `no`\n\nLog I/O errors.\n\n---\n\n### debug _boolean_\nDefault: global directive value\n\nEnable verbose logging.\n\n---\n\n### insecure_auth _boolean_\nDefault: `no` (`yes` if TLS is disabled)\n\nAllow plain-text authentication over unencrypted connections.\n\n---\n\n### sasl_login _boolean_\nDefault: `no`\n\nEnable support for SASL LOGIN authentication mechanism used by\nsome outdated clients.\n\n---\n\n### auth _module-reference_\n**Required.**\n\nUse the specified module for authentication.\n\n---\n\n### storage _module-reference_\n**Required.**\n\nUse the specified module for message storage.\n\n---\n\n### storage_map _module-reference_\nDefault: `identity`\n\nUse the specified table to map SASL usernames to storage account names.\n\nBefore username is looked up, it is normalized using function defined by\n`storage_map_normalize`.\n\nThis directive is useful if you want users user@example.org and user@example.com\nto share the same storage account named \"user\". In this case, use\n\n```\n    storage_map email_localpart\n```\n\nNote that `storage_map` does not affect the username passed to the\nauthentication provider.\n\nIt also does not affect how message delivery is handled, you should specify\n`delivery_map` in storage module to define how to map email addresses\nto storage accounts. E.g.\n\n```\n    storage.imapsql local_mailboxes {\n        ...\n        delivery_map email_localpart # deliver \"user@*\" to mailbox for \"user\"\n    }\n```\n\n---\n\n### storage_map_normalize _function_\nDefault: `auto`\n\nSame as `auth_map_normalize` but for `storage_map`.\n\n---\n\n### auth_map_normalize _function_\nDefault: `auto`\n\nOverrides global `auth_map_normalize` value for this endpoint.\n\nSee [Global configuration](/reference/global-config) for details.\n\n\n\n"
  },
  {
    "path": "docs/reference/endpoints/openmetrics.md",
    "content": "# OpenMetrics/Prometheus telemetry\n\nVarious server statistics are provided in OpenMetrics format by the\n\"openmetrics\" module.\n\nTo enable it, add the following line to the server config:\n\n```\nopenmetrics tcp://127.0.0.1:9749 { }\n```\n\nScrape endpoint would be `http://127.0.0.1:9749/metrics`.\n\n## Metrics\n\n```\n# AUTH command failures due to invalid credentials.\nmaddy_smtp_failed_logins{module}\n# Failed SMTP transaction commands (MAIL, RCPT, DATA).\nmaddy_smtp_failed_commands{module, command, smtp_code, smtp_enchcode}\n# Messages rejected with 4xx code due to ratelimiting.\nmaddy_smtp_ratelimit_deferred{module}\n# Amount of started SMTP transactions started.\nmaddy_smtp_started_transactions{module}\n# Amount of aborted SMTP transactions started.\nmaddy_smtp_aborted_transactions{module}\n# Amount of completed SMTP transactions.\nmaddy_smtp_completed_transactions{module}\n# Number of times a check returned 'reject' result (may be more than processed\n# messages if check does so on per-recipient basis).\nmaddy_check_reject{check}\n# Number of times a check returned 'quarantine' result (may be more than\n# processed messages if check does so on per-recipient basis).\nmaddy_check_quarantined{check}\n# Amount of queued messages.\nmaddy_queue_length{module, location}\n# Outbound connections established with specific TLS security level.\nmaddy_remote_conns_tls_level{module, level}\n# Outbound connections established with specific MX security level.\nmaddy_remote_conns_mx_level{module, level}\n```\n"
  },
  {
    "path": "docs/reference/endpoints/smtp.md",
    "content": "# SMTP/LMTP/Submission endpoint\n\nModule 'smtp' is a listener that implements ESMTP protocol with optional\nauthentication, LMTP and Submission support. Incoming messages are processed in\naccordance with pipeline rules (explained in Message pipeline section below).\n\n```\nsmtp tcp://0.0.0.0:25 {\n    hostname example.org\n    tls /etc/ssl/private/cert.pem /etc/ssl/private/pkey.key\n    io_debug no\n    debug no\n    insecure_auth no\n    sasl_login no\n    read_timeout 10m\n    write_timeout 1m\n    shutdown_timeout 3m\n    max_message_size 32M\n    max_header_size 1M\n    auth pam\n    defer_sender_reject yes\n    dmarc yes\n    smtp_max_line_length 4000\n    limits {\n        endpoint rate 10\n        endpoint concurrency 500\n    }\n\n    # Example pipeline configuration.\n    destination example.org {\n        deliver_to &local_mailboxes\n    }\n    default_destination {\n        reject\n    }\n}\n```\n\n## Configuration directives\n\n### hostname _string_\nDefault: global directive value\n\nServer name to use in SMTP banner.\n\n```\n220 example.org ESMTP Service Ready\n```\n\n---\n\n### tls _certificate-path_ _key-path_ { ... }\nDefault: global directive value\n\nTLS certificate & key to use. Fine-tuning of other TLS properties is possible\nby specifying a configuration block and options inside it:\n\n```\ntls cert.crt key.key {\n    protocols tls1.2 tls1.3\n}\n```\n\nSee [TLS configuration / Server](/reference/tls/#server-side) for details.\n\n---\n\n### proxy_protocol _trusted ips..._ { ... } <br>\nDefault: not enabled\n\nEnable use of HAProxy PROXY protocol. Supports both v1 and v2 protocols.\nIf a list of trusted IP addresses or subnets is provided, only connections\nfrom those will be trusted.\n\nTLS for the channel between the proxies and maddy can be configured\nusing a 'tls' directive:\n```\nproxy_protocol {\n    trust 127.0.0.1 ::1 192.168.0.1/24\n    tls &proxy_tls\n}\n```\n\n---\n\n### io_debug _boolean_\nDefault: `no`\n\nWrite all commands and responses to stderr.\n\n---\n\n### debug _boolean_\nDefault: global directive value\n\nEnable verbose logging.\n\n---\n\n### insecure_auth _boolean_\nDefault: `no` (`yes` if TLS is disabled)\n\nAllow plain-text authentication over unencrypted connections. Not recommended!\n\n---\n\n### sasl_login _boolean_\nDefault: `no`\n\nEnable support for SASL LOGIN authentication mechanism used by\nsome outdated clients.\n\n---\n\n### read_timeout _duration_\nDefault: `10m`\n\nI/O read timeout.\n\n---\n\n### write_timeout _duration_\nDefault: `1m`\n\nI/O write timeout.\n\n---\n\n### shutdown_timeout _duration_\nDefault: `3m`\n\nTime to wait until forcibly closing connections on server shutdown\nor configuration reload.\n\n---\n\n### max_message_size _size_\nDefault: `32M`\n\nLimit the size of incoming messages to 'size'.\n\n---\n\n### max_header_size _size_\nDefault: `1M`\n\nLimit the size of incoming message headers to 'size'.\n\n---\n\n### auth _module-reference_\nDefault: not specified\n\nUse the specified module for authentication.\n\n---\n\n### defer_sender_reject _boolean_\nDefault: `yes`\n\nApply sender-based checks and routing logic when first RCPT TO command\nis received. This allows maddy to log recipient address of the rejected\nmessage and also improves interoperability with (improperly implemented)\nclients that don't expect an error early in session.\n\n---\n\n### max_logged_rcpt_errors _integer_\nDefault: `5`\n\nAmount of RCPT-time errors that should be logged. Further errors will be\nhandled silently. This is to prevent log flooding during email dictionary\nattacks (address probing).\n\n---\n\n### max_received _integer_\nDefault: `50`\n\nMax. amount of Received header fields in the message header. If the incoming\nmessage has more fields than this number, it will be rejected with the permanent error\n5.4.6 (\"Routing loop detected\").\n\n---\n\n### buffer `ram`<br>buffer `fs` _path_ <br>buffer `auto` _max-size_ _path_\nDefault: `auto 1M StateDirectory/buffer`\n\nTemporary storage to use for the body of accepted messages.\n\n- `ram` – Store the body in RAM.\n- `fs` – Write out the message to the FS and read it back as needed.\n_path_ can be omitted and defaults to StateDirectory/buffer.\n- `auto` – Store message bodies smaller than `_max_size_` entirely in RAM,\notherwise write them out to the FS. _path_ can be omitted and defaults to `StateDirectory/buffer`.\n\n---\n\n### smtp_max_line_length _integer_\nDefault: `4000`\n\nThe maximum line length allowed in the SMTP input stream. If client sends a\nlonger line - connection will be closed and message (if any) will be rejected\nwith a permanent error.\n\nRFC 5321 has the recommended limit of 998 bytes. Servers are not required\nto handle longer lines correctly but some senders may produce them.\n\nUnless BDAT extension is used by the sender, this limitation also applies to\nthe message body.\n\n---\n\n### dmarc _boolean_\nDefault: `yes`\n\nEnforce sender's DMARC policy. Due to implementation limitations, it is not a\ncheck module.\n\n**Note**: Report generation is not implemented now.\n\n**Note**: DMARC needs SPF and DKIM checks to function correctly.\nWithout these, DMARC check will not run.\n\n---\n\n## Rate & concurrency limiting\n\n### limits { ... }\nDefault: no limits\n\nThis allows configuring a set of message flow restrictions including\nmax. concurrency and rate per-endpoint, per-source, per-destination.\n\nLimits are specified as directives inside the block:\n\n```\nlimits {\n\tall rate 20\n\tdestination concurrency 5\n}\n```\n\nSupported limits:\n\n### _scope_ rate _burst_ _period_\n\nRate limit. Restrict the amount of messages processed in _period_ to\n_burst_ messages. If period is not specified, 1 second is used.\n\n### _scope_ concurrency _max_\nConcurrency limit. Restrict the amount of messages processed in parallel\nto _max_.\n\nFor each supported limitation, _scope_ determines whether it should be applied\nfor all messages (\"all\"), per-sender IP (\"ip\"), per-sender domain (\"source\") or\nper-recipient domain (\"destination\"). Having a scope other than \"all\" means\nthat the restriction will be enforced independently for each group determined\nby scope. E.g.  \"ip rate 20\" means that the same IP cannot send more than 20\nmessages per second. \"destination concurrency 5\" means that no more than 5\nmessages can be sent in parallel to a single domain.\n\n**Note**: At the moment, SMTP endpoint on its own does not support per-recipient\nlimits.  They will be no-op. If you want to enforce a per-recipient restriction\non outbound messages, do so using 'limits' directive for the 'table.remote' module\n\nIt is possible to share limit counters between multiple endpoints (or any other\nmodules). To do so define a top-level configuration block for module \"limits\"\nand reference it where needed using standard & syntax. E.g.\n\n```\nlimits inbound_limits {\n\tall rate 20\n}\n\nsmtp smtp://0.0.0.0:25 {\n\tlimits &inbound_limits\n\t...\n}\n\nsubmission tls://0.0.0.0:465 {\n\tlimits &inbound_limits\n\t...\n}\n```\n\nUsing an \"all rate\" restriction in such way means that no more than 20\nmessages can enter the server through both endpoints in one second.\n\n# Submission module (submission)\n\nModule 'submission' implements all functionality of the 'smtp' module and adds\ncertain message preprocessing on top of it, additionally authentication is\nalways required.\n\n'submission' module checks whether addresses in header fields From, Sender, To,\nCc, Bcc, Reply-To are correct and adds Message-ID and Date if it is missing.\n\n```\nsubmission tcp://0.0.0.0:587 tls://0.0.0.0:465 {\n    # ... same as smtp ...\n}\n```\n\n# LMTP module (lmtp)\n\nModule 'lmtp' implements all functionality of the 'smtp' module but uses\nLMTP (RFC 2033) protocol.\n\n```\nlmtp unix://lmtp.sock {\n    # ... same as smtp ...\n}\n```\n\n## Limitations of LMTP implementation\n\n- Can't be used with TCP.\n- Delivery to 'sql' module storage is always atomic, either all recipients will\n  succeed or none of them will.\n\n"
  },
  {
    "path": "docs/reference/global-config.md",
    "content": "# Global configuration directives\n\nThese directives can be specified outside of any\nconfiguration blocks and they are applied to all modules.\n\nSome directives can be overridden on per-module basis (e.g. hostname).\n\n### state_dir _path_\nDefault: `/var/lib/maddy`\n\nThe path to the state directory. This directory will be used to store all\npersistent data and should be writable.\n\n---\n\n### runtime_dir _path_\nDefault: `/run/maddy`\n\nThe path to the runtime directory. Used for Unix sockets and other temporary\nobjects. Should be writable.\n\n---\n\n### hostname _domain_ \nDefault: not specified\n\nInternet hostname of this mail server. Typicall FQDN is used. It is recommended\nto make sure domain specified here resolved to the public IP of the server.\n\n---\n\n### auth_map _module-reference_\nDefault: `identity`\n\nUse the specified table to translate SASL usernames before passing it to the\nauthentication provider.\n\nBefore username is looked up, it is normalized using function defined by\n`auth_map_normalize`.\n\nNote that `auth_map` does not affect the storage account name used. You probably\nshould also use `storage_map` in IMAP config block to handle this.\n\nThis directive is useful if used authentication provider does not support\nusing emails as usernames but you still want users to have separate mailboxes\non separate domains. In this case, use it with `email_localpart` table:\n\n```\n    auth_map email_localpart\n```\n\nWith this configuration, `user@example.org` and `user@example.com` will use\n`user` credentials when authenticating, but will access `user@example.org` and\n`user@example.com` mailboxes correspondingly. If you want to also accept\n`user` as a username, use `auth_map email_localpart_optional`.\n\nIf you want `user@example.org` and `user@example.com` to have the same mailbox,\nalso set `storage_map` in IMAP config block to use `email_localpart`\n(or `email_localpart_optional` if you want to also accept just \"user\"):\n\n```\n    storage_map email_localpart\n```\n\nIn this case you will need to create storage accounts without domain part in\nthe name:\n\n```\nmaddy imap-acct create user # instead of user@example.org\n```\n\n---\n\n### auth_map_normalize _function_\nDefault: `auto`\n\nNormalization function to apply to SASL usernames before mapping\nthem to storage accounts.\n\nAvailable options:\n\n- `auto`                    `precis_casefold_email` for valid emails, `precis_casefold` otherwise.\n- `precis_casefold_email`   PRECIS UsernameCaseMapped profile + U-labels form for domain\n- `precis_casefold`         PRECIS UsernameCaseMapped profile for the entire string\n- `precis_email`            PRECIS UsernameCasePreserved profile + U-labels form for domain\n- `precis`                  PRECIS UsernameCasePreserved profile for the entire string\n- `casefold`                Convert to lower case\n- `noop`                    Nothing\n\n---\n\n### autogenerated_msg_domain _domain_\nDefault: not specified\n\nDomain that is used in From field for auto-generated messages (such as Delivery\nStatus Notifications).\n\n---\n\n### tls `file` _cert-file_ _pkey-file_ | _module-reference_ | `off`\nDefault: not specified\n\nDefault TLS certificate to use for all endpoints.\n\nMust be present in either all endpoint modules configuration blocks or as\nglobal directive.\n\nYou can also specify other configuration options such as cipher suites and TLS\nversion. See maddy-tls(5) for details. maddy uses reasonable\ncipher suites and TLS versions by default so you generally don't have to worry\nabout it.\n\n---\n\n### tls_client { ... }\nDefault: not specified\n\nThis is optional block that specifies various TLS-related options to use when\nmaking outbound connections. See TLS client configuration for details on\ndirectives that can be used in it. maddy uses reasonable cipher suites and TLS\nversions by default so you generally don't have to worry about it.\n\n---\n\n### log _targets..._ | `off`\nDefault: `stderr`\n\nWrite log to one of more \"targets\".\n\nThe target can be one or the following:\n\n- `stderr` –  Write logs to stderr.\n- `stderr_ts` – Write logs to stderr with timestamps.\n- `syslog` – Send logs to the local syslog daemon.\n- _file path_ – Write (append) logs to file.\n\nExample:\n\n```\nlog syslog /var/log/maddy.log\n```\n\n**Note:** Maddy does not perform log files rotation, this is the job of the\nlogrotate daemon. Send SIGUSR1 to maddy process to make it reopen log files.\n\n---\n\n### debug _boolean_ \nDefault: `no`\n\nEnable verbose logging for all modules. You don't need that unless you are\nreporting a bug.\n\n"
  },
  {
    "path": "docs/reference/modifiers/dkim.md",
    "content": "# DKIM signing\n\nmodify.dkim module is a modifier that signs messages using DKIM\nprotocol (RFC 6376).\n\nEach configuration block specifies a single selector\nand one or more domains.\n\nA key will be generated or read for each domain, the key to use\nfor each message will be selected based on the SMTP envelope sender. Exception\nfor that is that for domain-less postmaster address and null address, the\nkey for the first domain will be used. If domain in envelope sender\ndoes not match any of loaded keys, message will not be signed.\nAdditionally, for each messages From header is checked to \nmatch MAIL FROM and authorization identity (username sender is logged in as).\nThis can be controlled using require_sender_match directive.\n\nGenerated private keys are stored in unencrypted PKCS#8 format\nin state_directory/dkim_keys (`/var/lib/maddy/dkim_keys`).\nIn the same directory .dns files are generated that contain\npublic key for each domain formatted in the form of a DNS record.\n\n## Arguments\n\ndomains and selector can be specified in arguments, so actual modify.dkim use can\nbe shortened to the following:\n\n```\nmodify {\n    dkim example.org selector\n}\n```\n\n## Configuration directives\n\n```\nmodify.dkim {\n    debug no\n    domains example.org example.com\n    selector default\n    key_path dkim-keys/{domain}-{selector}.key\n    oversign_fields ...\n    sign_fields ...\n    header_canon relaxed\n    body_canon relaxed\n    sig_expiry 120h # 5 days\n    hash sha256\n    newkey_algo rsa2048\n}\n```\n\n### debug _boolean_\nDefault: global directive value\n\nEnable verbose logging.\n\n---\n\n### domains _string-list_\n**Required**. <br>\nDefault: not specified\n\n\nADministrative Management Domains (ADMDs) taking responsibility for messages.\n\nShould be specified either as a directive or as an argument.\n\n---\n\n### selector _string_\n**Required**. <br>\nDefault: not specified\n\nIdentifier of used key within the ADMD.\nShould be specified either as a directive or as an argument.\n\n---\n\n### key_path _string_\nDefault: `dkim_keys/{domain}_{selector}.key`\n\nPath to private key. It should be in PKCS#8 format wrapped in PAM encoding.\nIf key does not exist, it will be generated using algorithm specified\nin newkey_algo.\n\nPlaceholders '{domain}' and '{selector}' will be replaced with corresponding\nvalues from domain and selector directives.\n\nAdditionally, keys in PKCS#1 (\"RSA PRIVATE KEY\") and\nRFC 5915 (\"EC PRIVATE KEY\") can be read by modify.dkim. Note, however that\nnewly generated keys are always in PKCS#8.\n\n---\n\n### oversign_fields _list..._\nDefault: see below\n\nHeader fields that should be signed n+1 times where n is times they are\npresent in the message. This makes it impossible to replace field\nvalue by prepending another field with the same name to the message.\n\nFields specified here don't have to be also specified in `sign_fields`.\n\nDefault set of oversigned fields:\n\n- Subject\n- To\n- From\n- Date\n- MIME-Version\n- Content-Type\n- Content-Transfer-Encoding\n- Reply-To\n- Message-Id\n- References\n- Autocrypt\n- Openpgp\n\n---\n\n### sign_fields _list..._\nDefault: see below\n\nHeader fields that should be signed n times where n is times they are\npresent in the message. For these fields, additional values can be prepended\nby intermediate relays, but existing values can't be changed.\n\nDefault set of signed fields:\n\n- List-Id\n- List-Help\n- List-Unsubscribe\n- List-Post\n- List-Owner\n- List-Archive\n- Resent-To\n- Resent-Sender\n- Resent-Message-Id\n- Resent-Date\n- Resent-From\n- Resent-Cc\n\n---\n\n### header_canon `relaxed` | `simple`\nDefault: `relaxed`\n\nCanonicalization algorithm to use for header fields. With `relaxed`, whitespace within\nfields can be modified without breaking the signature, with `simple` no\nmodifications are allowed.\n\n---\n\n### body_canon `relaxed` | `simple`\nDefault: `relaxed`\n\nCanonicalization algorithm to use for message body. With `relaxed`, whitespace within\ncan be modified without breaking the signature, with `simple` no\nmodifications are allowed.\n\n---\n\n### sig_expiry _duration_\nDefault: `120h`\n\nTime for which signature should be considered valid. Mainly used to prevent\nunauthorized resending of old messages.\n\n---\n\n### hash _hash_\nDefault: `sha256`\n\nHash algorithm to use when computing body hash.\n\nsha256 is the only supported algorithm now.\n\n---\n\n### newkey_algo `rsa4096` | `rsa2048` | `ed25519`\nDefault: `rsa2048`\n\nAlgorithm to use when generating a new key.\n\nCurrently ed25519 is **not** supported by most platforms.\n\n---\n\n### require_sender_match _ids..._\nDefault: `envelope auth`\n\nRequire specified identifiers to match From header field and key domain,\notherwise - don't sign the message.\n\nIf From field contains multiple addresses, message will not be\nsigned unless `allow_multiple_from` is also specified. In that\ncase only first address will be compared.\n\nMatching is done in a case-insensitive way.\n\nValid values:\n\n- `off` – Disable check, always sign.\n- `envelope` – Require MAIL FROM address to match From header.\n- `auth` – If authorization identity contains @ - then require it to\n  fully match From header. Otherwise, check only local-part\n  (username).\n\n---\n\n### allow_multiple_from _boolean_\nDefault: `no`\n\nAllow multiple addresses in From header field for purposes of\n`require_sender_match` checks. Only first address will be checked, however.\n\n---\n\n### sign_subdomains _boolean_\nDefault: `no`\n\nSign emails from subdomains using a top domain key.\n\nAllows only one domain to be specified (can be worked around by using `modify.dkim`\nmultiple times).\n"
  },
  {
    "path": "docs/reference/modifiers/envelope.md",
    "content": "# Envelope sender / recipient rewriting\n\n`replace_sender` and `replace_rcpt` modules replace SMTP envelope addresses\nbased on the mapping defined by the table module (maddy-tables(5)). It is possible\nto specify 1:N mappings. This allows, for example, implementing mailing lists.\n\nThe address is normalized before lookup (Punycode in domain-part is decoded,\nUnicode is normalized to NFC, the whole string is case-folded).\n\nFirst, the whole address is looked up. If there is no replacement, local-part\nof the address is looked up separately and is replaced in the address while\nkeeping the domain part intact. Replacements are not applied recursively, that\nis, lookup is not repeated for the replacement.\n\nRecipients are not deduplicated after expansion, so message may be delivered\nmultiple times to a single recipient. However, used delivery target can apply\nsuch deduplication (imapsql storage does it).\n\nDefinition:\n\n```\nreplace_rcpt <table> [table arguments] {\n\t[extended table config]\n}\nreplace_sender <table> [table arguments] {\n\t[extended table config]\n}\n```\n\nUse examples:\n\n```\nmodify {\n\treplace_rcpt file /etc/maddy/aliases\n\treplace_rcpt static {\n\t\tentry a@example.org b@example.org\n\t\tentry c@example.org c1@example.org c2@example.org\n\t}\n\treplace_rcpt regexp \"(.+)@example.net\" \"$1@example.org\"\n\treplace_rcpt regexp \"(.+)@example.net\" \"$1@example.org\" \"$1@example.com\"\n}\n```\n\nPossible contents of /etc/maddy/aliases in the example above:\n\n```\n# Replace 'cat' with any domain to 'dog'.\n# E.g. cat@example.net -> dog@example.net\ncat: dog\n\n# Replace cat@example.org with cat@example.com.\n# Takes priority over the previous line.\ncat@example.org: cat@example.com\n\n# Using aliases in multiple lines\ncat2: dog\ncat2: mouse\ncat2@example.org: cat@example.com\ncat2@example.org: cat@example.net\n# Comma-separated aliases in multiple lines\ncat3: dog , mouse\ncat3@example.org: cat@example.com , cat@example.net\n```"
  },
  {
    "path": "docs/reference/modules.md",
    "content": "# Modules introduction\n\nmaddy is built of many small components called \"modules\". Each module does one\ncertain well-defined task. Modules can be connected to each other in arbitrary\nways to achieve wanted functionality. Default configuration file defines\nset of modules that together implement typical email server stack.\n\nTo specify the module that should be used by another module for something, look\nfor configuration directives with \"module reference\" argument. Then\nput the module name as an argument for it. Optionally, if referenced module\nneeds that, put additional arguments after the name. You can also put a\nconfiguration block with additional directives specifing the module\nconfiguration.\n\nHere are some examples:\n\n```\nsmtp ... {\n    # Deliver messages to the 'dummy' module with the default configuration.\n    deliver_to dummy\n\n    # Deliver messages to the 'target.smtp' module with\n    # 'tcp://127.0.0.1:1125' argument as a configuration.\n    deliver_to smtp tcp://127.0.0.1:1125\n\n    # Deliver messages to the 'queue' module with the specified configuration.\n    deliver_to queue {\n        target ...\n        max_tries 10\n    }\n}\n```\n\nAdditionally, module configuration can be placed in a separate named block\nat the top-level and referenced by its name where it is needed.\n\nHere is the example:\n```\nstorage.imapsql local_mailboxes {\n    driver sqlite3\n    dsn all.db\n}\n\nsmtp ... {\n    deliver_to &local_mailboxes\n}\n```\n\nIt is recommended to use this syntax for modules that are 'expensive' to\ninitialize such as storage backends and authentication providers.\n\nFor top-level configuration block definition, syntax is as follows:\n```\nnamespace.module_name config_block_name... {\n    module_configuration\n}\n```\nIf config\\_block\\_name is omitted, it will be the same as module\\_name. Multiple\nnames can be specified. All names must be unique.\n\nNote the \"storage.\" prefix. This is the actual module name and includes\n\"namespace\". It is a little cheating to make more concise names and can\nbe omitted when you reference the module where it is used since it can\nbe implied (e.g. putting module reference in \"check{}\" likely means you want\nsomething with \"check.\" prefix)\n\nUsual module arguments can't be specified when using this syntax, however,\nmodules usually provide explicit directives that allow to specify the needed\nvalues. For example 'sql sqlite3 all.db' is equivalent to\n```\nstorage.imapsql {\n    driver sqlite3\n    dsn all.db\n}\n```\n\n"
  },
  {
    "path": "docs/reference/smtp-pipeline.md",
    "content": "# SMTP message routing (pipeline)\n\n# Message pipeline\n\nA message pipeline is a set of module references and associated rules that\ndescribe how to handle messages.\n\nThe pipeline is responsible for\n\n- Running message filters (called \"checks\"), (e.g. DKIM signature verification,\n  DNSBL lookup, and so on).\n- Running message modifiers (e.g. DKIM signature creation).\n- Associating each message recipient with one or more delivery targets.\n  Delivery target is a module that does the final processing (delivery) of the\n  message.\n\nMessage handling flow is as follows:\n\n- Execute checks referenced in top-level `check` blocks (if any)\n- Execute modifiers referenced in top-level `modify` blocks (if any)\n- If there are `source` blocks - select one that matches the message sender (as\n  specified in MAIL FROM). If there are no `source` blocks - the entire\n  configuration is assumed to be the `default_source` block.\n- Execute checks referenced in `check` blocks inside the selected `source` block\n  (if any).\n- Execute modifiers referenced in `modify` blocks inside selected `source`\n  block (if any).\n\nThen, for each recipient:\n\n- Select the `destination` block that matches it. If there are\n  no `destination` blocks - the entire used `source` block is interpreted as if it\n  was a `default_destination` block.\n- Execute checks referenced in the `check` block inside the selected `destination`\n  block (if any).\n- Execute modifiers referenced in `modify` block inside the selected `destination`\n  block (if any).\n- If the used block contains the `reject` directive - reject the recipient with\n  the specified SMTP status code.\n- If the used block contains the `deliver_to` directive - pass the message to the\n  specified target module. Only recipients that are handled\n  by the used block are visible to the target.\n\nEach recipient is handled only by a single `destination` block, in case of\noverlapping `destination` - the first one takes priority.\n\n```\ndestination example.org {\n    deliver_to targetA\n}\ndestination example.org { # ambiguous and thus not allowed\n    deliver_to targetB\n}\n```\n\nSame goes for `source` blocks, each message is handled only by a single block.\n\nEach recipient block should contain at least one `deliver_to` directive or\n`reject` directive. If `destination` blocks are used, then\n`default_destination` block should also be used to specify behavior for\nunmatched recipients.  Same goes for source blocks, `default_source` should be\nused if `source` is used.\n\nThat is, pipeline configuration should explicitly specify behavior for each\npossible sender/recipient combination.\n\nAdditionally, directives that specify final handling decision (`deliver_to`,\n`reject`) can't be used at the same level as source/destination rules.\nConsider example:\n\n```\ndestination example.org {\n    deliver_to local_mboxes\n}\nreject\n```\n\nIt is not obvious whether `reject` applies to all recipients or\njust for non-example.org ones, hence this is not allowed.\n\nComplete configuration example using all of the mentioned directives:\n\n```\ncheck {\n    # Run a check to make sure source SMTP server identification\n    # is legit.\n    spf\n}\n\n# Messages coming from senders at example.org will be handled in\n# accordance with the following configuration block.\nsource example.org {\n    # We are example.com, so deliver all messages with recipients\n    # at example.com to our local mailboxes.\n    destination example.com {\n        deliver_to &local_mailboxes\n    }\n\n    # We don't do anything with recipients at different domains\n    # because we are not an open relay, thus we reject them.\n    default_destination {\n        reject 521 5.0.0 \"User not local\"\n    }\n}\n\n# We do our business only with example.org, so reject all\n# other senders.\ndefault_source {\n    reject\n}\n```\n\n## Directives\n\n\n### check _block name_ { ... }\nContext: pipeline configuration, source block, destination block\n\nList of the module references for checks that should be executed on\nmessages handled by block where 'check' is placed in.\n\nNote that message body checks placed in destination block are currently\nignored. Due to the way SMTP protocol is defined, they would cause message to\nbe rejected for all recipients which is not what you usually want when using\nsuch configurations.\n\nExample:\n\n```\ncheck {\n    # Reference implicitly defined default configuration for check.\n    spf\n\n    # Inline definition of custom config.\n    spf {\n         # Configuration for spf goes here.\n         permerr_action reject\n    }\n}\n```\n\nIt is also possible to define the block of checks at the top level\nas \"checks\" module and reference it using & syntax. Example:\n\n```\nchecks inbound_checks {\n\tspf\n\tdkim\n}\n\n# ... somewhere else ...\n{\n\t...\n\tcheck &inbound_checks\n}\n```\n\n---\n\n### modify { ... }\nDefault: not specified<br>\nContext: pipeline configuration, source block, destination block\n\nList of the module references for modifiers that should be executed on\nmessages handled by block where 'modify' is placed in.\n\nMessage modifiers are similar to checks with the difference in that checks\npurpose is to verify whether the message is legitimate and valid per local\npolicy, while modifier purpose is to post-process message and its metadata\nbefore final delivery.\n\nFor example, modifier can replace recipient address to make message delivered\nto the different mailbox or it can cryptographically sign outgoing message\n(e.g. using DKIM). Some modifier can perform multiple unrelated modifications\non the message.\n\n**Note**: Modifiers that affect source address can be used only globally or on\nper-source basis, they will be no-op inside destination blocks. Modifiers that\naffect the message header will affect it for all recipients.\n\nIt is also possible to define the block of modifiers at the top level\nas \"modiifers\" module and reference it using & syntax. Example:\n\n```\nmodifiers local_modifiers {\n\treplace_rcpt file /etc/maddy/aliases\n}\n\n# ... somewhere else ...\n{\n\t...\n\tmodify &local_modifiers\n}\n```\n\n---\n\n### reject _smtp-code_ _smtp-enhanced-code_ _error-description_ <br>reject _smtp-code_ _smtp-enhanced-code_ <br>reject _smtp-code_ <br>reject\nContext: destination block\n\nMessages handled by the configuration block with this directive will be\nrejected with the specified SMTP error.\n\nIf you aren't sure which codes to use, use 541 and 5.4.0 with your message or\njust leave all arguments out, the error description will say \"message is\nrejected due to policy reasons\" which is usually what you want to mean.\n\n`reject` can't be used in the same block with `deliver_to` or\n`destination`/`source` directives.\n\nExample:\n\n```\nreject 541 5.4.0 \"We don't like example.org, go away\"\n```\n\n---\n\n### deliver_to _target-config-block_\nContext: pipeline configuration, source block, destination block\n\nDeliver the message to the referenced delivery target. What happens next is\ndefined solely by used target. If `deliver_to` is used inside `destination`\nblock, only matching recipients will be passed to the target.\n\n---\n\n### source_in _table-reference_ { ... }\nContext: pipeline configuration\n\nHandle messages with envelope senders present in the specified table in\naccordance with the specified configuration block.\n\nTakes precedence over all `sender` directives.\n\nExample:\n\n```\nsource_in file /etc/maddy/banned_addrs {\n\treject 550 5.7.0 \"You are not welcome here\"\n}\nsource example.org {\n\t...\n}\n...\n```\n\nSee `destination_in` documentation for note about table configuration.\n\n---\n\n### source _rules..._ { ... }\nContext: pipeline configuration\n\nHandle messages with MAIL FROM value (sender address) matching any of the rules\nin accordance with the specified configuration block.\n\n\"Rule\" is either a domain or a complete address. In case of overlapping\n'rules', first one takes priority. Matching is case-insensitive.\n\nExample:\n\n```\n# All messages coming from example.org domain will be delivered\n# to local_mailboxes.\nsource example.org {\n    deliver_to &local_mailboxes\n}\n# Messages coming from different domains will be rejected.\ndefault_source {\n    reject 521 5.0.0 \"You were not invited\"\n}\n```\n\n---\n\n### reroute { ... }\nContext: pipeline configuration, source block, destination block\n\nThis directive allows to make message routing decisions based on the\nresult of modifiers. The block can contain all pipeline directives and they\nwill be handled the same with the exception that source and destination rules\nwill use the final recipient and sender values (e.g. after all modifiers are\napplied).\n\nHere is the concrete example how it can be useful:\n\n```\ndestination example.org {\n    modify {\n        replace_rcpt file /etc/maddy/aliases\n    }\n    reroute {\n        destination example.org {\n            deliver_to &local_mailboxes\n        }\n        default_destination {\n            deliver_to &remote_queue\n        }\n    }\n}\n```\n\nThis configuration allows to specify alias local addresses to remote ones\nwithout being an open relay, since remote_queue can be used only if remote\naddress was introduced as a result of rewrite of local address.\n\n**Warning**: If you have DMARC enabled (default), results generated by SPF\nand DKIM checks inside a reroute block **will not** be considered in DMARC\nevaluation.\n\n---\n\n### destination_in _table-reference_ { ... }\nContext: pipeline configuration, source block\n\nHandle messages with envelope recipients present in the specified table in\naccordance with the specified configuration block.\n\nTakes precedence over all 'destination' directives.\n\nExample:\n\n```\ndestination_in file /etc/maddy/remote_addrs {\n\tdeliver_to smtp tcp://10.0.0.7:25\n}\ndestination example.com {\n\tdeliver_to &local_mailboxes\n}\n...\n```\n\nNote that due to the syntax restrictions, it is not possible to specify\nextended configuration for table module. E.g. this is not valid:\n\n```\ndestination_in sql_table {\n\tdsn ...\n\tdriver ...\n} {\n\tdeliver_to whatever\n}\n```\n\nIn this case, configuration should be specified separately and be referneced\nusing '&' syntax:\n\n```\ntable.sql_table remote_addrs {\n\tdsn ...\n\tdriver ...\n}\n\nwhatever {\n\tdestination_in &remote_addrs {\n\t\tdeliver_to whatever\n\t}\n}\n```\n\n---\n\n### destination _rule..._ { ... }\nContext: pipeline configuration, source block\n\nHandle messages with RCPT TO value (recipient address) matching any of the\nrules in accordance with the specified configuration block.\n\n\"Rule\" is either a domain or a complete address. Duplicate rules are not\nallowed. Matching is case-insensitive.\n\nNote that messages with multiple recipients are split into multiple messages if\nthey have recipients matched by multiple blocks. Each block will see the\nmessage only with recipients matched by its rules.\n\nExample:\n\n```\n# Messages with recipients at example.com domain will be\n# delivered to local_mailboxes target.\ndestination example.com {\n    deliver_to &local_mailboxes\n}\n\n# Messages with other recipients will be rejected.\ndefault_destination {\n    rejected 541 5.0.0 \"User not local\"\n}\n```\n\n## Reusable pipeline snippets (msgpipeline module)\n\nThe message pipeline can be used independently of the SMTP module in other\ncontexts that require a delivery target via `msgpipeline` module.\n\nExample:\n\n```\nmsgpipeline local_routing {\n    destination whatever.com {\n        deliver_to dummy\n    }\n}\n\n# ... somewhere else ...\ndeliver_to &local_routing\n```"
  },
  {
    "path": "docs/reference/storage/imap-filters.md",
    "content": "# IMAP filters\n\nMost storage backends support application of custom code late in delivery\nprocess. As opposed to using SMTP pipeline modifiers or checks, it allows\nmodifying IMAP-specific message attributes. In particular, it allows\ncode to change target folder and add IMAP flags (keywords) to the message.\n\nThere is no way to reject message using IMAP filters, this should be done\nearlier in SMTP pipeline logic. Quarantined messages are not processed\nby IMAP filters and are unconditionally delivered to Junk folder (or other\nfolder with \\Junk special-use attribute).\n\nTo use an IMAP filter, specify it in the 'imap\\_filter' directive for the\nused storage backend, like this:\n```\nstorage.imapsql local_mailboxes {\n   ...\n   \n   imap_filter {\n       command /etc/maddy/sieve.sh {account_name}\n   }\n}\n```\n\n## System command filter (imap.filter.command)\n\nThis filter is similar to check.command module\nand runs a system command to obtain necessary information.\n\nUsage:\n```\ncommand executable_name args... { }\n```\n\nSame as check.command, following placeholders are supported for command\narguments: {source\\_ip}, {source\\_host}, {source\\_rdns}, {msg\\_id}, {auth\\_user},\n{sender}. Note: placeholders\nin command name are not processed to avoid possible command injection attacks.\n\nAdditionally, for imap.filter.command, {account\\_name} placeholder is replaced\nwith effective IMAP account name, {rcpt_to}, {original_rcpt_to} provide\naccess to the SMTP envelope recipient (before and after any rewrites),\n{subject} is replaced with the Subject header, if it is present.\n\nNote that if you use provided systemd units on Linux, maddy executable is\nsandboxed - all commands will be executed with heavily restricted filesystem\naccess and other privileges. Notably, /tmp is isolated and all directories\nexcept for /var/lib/maddy and /run/maddy are read-only. You will need to modify\nsystemd unit if your command needs more privileges.\n\nCommand output should consist of zero or more lines. First one, if non-empty, overrides\ndestination folder. All other lines contain additional IMAP flags to add\nto the message. If command wants to add flags without changing folder - first\nline should be empty.\n\nIt is valid for command to not write anything to stdout. In this case its\nexecution will have no effect on delivery.\n\nOutput example:\n```\nJunk\n```\nIn this case, message will be placed in the Junk folder.\n\n```\n\n$Label1\n```\nIn this case, message will be placed in inbox and will have\n'$Label1' added.\n"
  },
  {
    "path": "docs/reference/storage/imapsql.md",
    "content": "# SQL-indexed storage\n\nThe imapsql module implements database for IMAP index and message\nmetadata using SQL-based relational database.\n\nMessage contents are stored in an \"blob store\" defined by msg_store\ndirective. By default this is a file system directory under /var/lib/maddy.\n\nSupported RDBMS:\n- SQLite 3.25.0\n- PostgreSQL 9.6 or newer\n- CockroachDB 20.1.5 or newer\n\nAccount names are required to have the form of a email address (unless configured otherwise)\nand are case-insensitive. UTF-8 names are supported with restrictions defined in the\nPRECIS UsernameCaseMapped profile.\n\n```\nstorage.imapsql {\n\tdriver sqlite3\n\tdsn imapsql.db\n\tmsg_store fs messages/\n}\n```\n\nimapsql module also can be used as a lookup table.\nIt returns empty string values for existing usernames. This might be useful\nwith `destination_in` directive e.g. to implement catch-all\naddresses (this is a bad idea to do so, this is just an example):\n```\ndestination_in &local_mailboxes {\n\tdeliver_to &local_mailboxes\n}\ndestination example.org {\n\tmodify {\n\t\treplace_rcpt regexp \".*\" \"catchall@example.org\"\n\t}\n\tdeliver_to &local_mailboxes\n}\n```\n\n\n## Arguments\n\nSpecify the driver and DSN.\n\n## Configuration directives\n\n### driver _string_\n**Required.**<br>\nDefault: not specified\n\nUse a specified driver to communicate with the database. Supported values:\nsqlite3, postgres.\n\nShould be specified either via an argument or via this directive.\n\n---\n\n### dsn _string_\n**Required.**<br>\nDefault: not specified\n\nData Source Name, the driver-specific value that specifies the database to use.\n\nFor SQLite3 this is just a file path.\nFor PostgreSQL: [https://godoc.org/github.com/lib/pq#hdr-Connection\\_String\\_Parameters](https://godoc.org/github.com/lib/pq#hdr-Connection\\_String\\_Parameters)\n\nShould be specified either via an argument or via this directive.\n\n---\n\n### msg_store _store_\nDefault: `fs messages/`\n\nModule to use for message bodies storage.\n\nSee \"Blob storage\" section for what you can use here.\n\n---\n\n### compression `off`<br>compression _algorithm_<br>compression _algorithm_ _level_\nDefault: `off`\n\nApply compression to message contents.\nSupported algorithms: `lz4`, `zstd`.\n\n---\n\n### appendlimit _size_\nDefault: `32M`\n\nDon't allow users to add new messages larger than 'size'.\n\nThis does not affect messages added when using module as a delivery target.\nUse `max_message_size` directive in SMTP endpoint module to restrict it too.\n\n---\n\n### debug _boolean_\nDefault: global directive value\n\nEnable verbose logging.\n\n---\n\n### junk_mailbox _name_\nDefault: `Junk`\n\nThe folder to put quarantined messages in. Thishis setting is not used if user\ndoes have a folder with \"Junk\" special-use attribute.\n\n---\n\n### disable_recent _boolean_\nDefault: `true`\n\nDisable RFC 3501-conforming handling of \\Recent flag.\n\nThis significantly improves storage performance when SQLite3 or CockroackDB is\nused at the cost of confusing clients that use this flag.\n\n---\n\n### sqlite_cache_size _integer_\nDefault: defined by SQLite\n\nSQLite page cache size. If positive - specifies amount of pages (1 page - 4\nKiB) to keep in cache. If negative - specifies approximate upper bound\nof cache size in KiB.\n\n---\n\n### sqlite_busy_timeout _integer_\nDefault: `5000000`\n\nSQLite-specific performance tuning option. Amount of milliseconds to wait\nbefore giving up on DB lock.\n\n---\n\n### imap_filter { ... }\nDefault: not set\n\nSpecifies IMAP filters to apply for messages delivered from SMTP pipeline.\n\nEx.\n\n```\nimap_filter {\n\tcommand /etc/maddy/sieve.sh {account_name}\n}\n```\n\n---\n\n### delivery_map _table_\nDefault: `identity`\n\nUse specified table module to map recipient\naddresses from incoming messages to mailbox names.\n\nNormalization algorithm specified in `delivery_normalize` is appied before\n`delivery_map`.\n\n---\n\n### delivery_normalize _name_\nDefault: `precis_casefold_email`\n\nNormalization function to apply to email addresses before mapping them\nto mailboxes.\n\nSee `auth_normalize`.\n\n---\n\n### auth_map _table_\n**Deprecated:** Use `storage_map` in imap config instead.<br>\nDefault: `identity`\n\nUse specified table module to map authentication\nusernames to mailbox names.\n\nNormalization algorithm specified in auth_normalize is applied before\nauth_map.\n\n---\n\n### auth_normalize _name_\n**Deprecated:** Use `storage_map_normalize` in imap config instead.<br>\n**Default**: `precis_casefold_email`\n\nNormalization function to apply to authentication usernames before mapping\nthem to mailboxes.\n\nAvailable options:\n\n- `precis_casefold_email`   PRECIS UsernameCaseMapped profile + U-labels form for domain\n- `precis_casefold`         PRECIS UsernameCaseMapped profile for the entire string\n- `precis_email`            PRECIS UsernameCasePreserved profile + U-labels form for domain\n- `precis`                  PRECIS UsernameCasePreserved profile for the entire string\n- `casefold`                Convert to lower case\n- `noop`                    Nothing\n\nNote: On message delivery, recipient address is unconditionally normalized\nusing `precis_casefold_email` function.\n\n"
  },
  {
    "path": "docs/reference/table/auth.md",
    "content": "# Authentication providers\n\nMost authentication providers are also usable as a table\nthat contains all usernames known to the module. Exceptions are auth.external and\npam as underlying interfaces do not define a way to check credentials\nexistence.\n"
  },
  {
    "path": "docs/reference/table/chain.md",
    "content": "# Table chaining\n\nThe table.chain module allows chaining together multiple table modules\nby using value returned by a previous table as an input for the second\ntable.\n\nExample:\n```\ntable.chain {\n\tstep regexp \"(.+)(\\\\+[^+\"@]+)?@example.org\" \"$1@example.org\"\n\tstep file /etc/maddy/emails\n}\n```\nThis will strip +prefix from mailbox before looking it up\nin /etc/maddy/emails list.\n\n## Configuration directives\n\n### step _table_\n\nAdds a table module to the chain. If input value is not in the table\n(e.g. file) - return \"not exists\" error.\n\n---\n\n### optional_step _table_\n\nSame as step but if input value is not in the table - it is passed to the\nnext step without changes.\n\nExample:\nSomething like this can be used to map emails to usernames\nafter translating them via aliases map:\n\n```\ntable.chain {\n    optional_step file /etc/maddy/aliases\n    step regexp \"(.+)@(.+)\" \"$1\"\n}\n```\n\n"
  },
  {
    "path": "docs/reference/table/email_localpart.md",
    "content": "# Email local part\n\nThe module `table.email_localpart` extracts and unescapes local (\"username\") part\nof the email address.\n\nE.g.\n\n* `test@example.org` => `test`\n* `\"test @ a\"@example.org` => `test @ a`\n\nMappings for invalid emails are not defined (will be treated as non-existing\nvalues).\n\n```\ntable.email_localpart { }\n```\n\n`table.email_localpart_optional` works the same, but returns non-email strings\nas is. This can be used if you want to accept both `user@example.org` and\n`user` somewhere and treat it the same.\n"
  },
  {
    "path": "docs/reference/table/email_with_domain.md",
    "content": "# Email with domain\n\nThe table module `table.email_with_domain` appends one or more\ndomains (allowing 1:N expansion) to the specified value.\n\n```\ntable.email_with_domain DOMAIN DOMAIN... { }\n```\n\nIt can be used to implement domain-level expansion for aliases if used together\nwith `table.chain`. Example:\n\n```\nmodify {\n    replace_rcpt chain {\n        step email_local_part\n        step email_with_domain example.org example.com\n    }\n}\n```\n\nThis configuration will alias `anything@anydomain` to `anything@example.org`\nand `anything@example.com`.\n\nIt is also useful with `authorize_sender` to authorize sending using multiple\naddresses under different domains if non-email usernames are used for\nauthentication:\n\n```\ncheck.authorize_sender {\n   ...\n   user_to_email email_with_domain example.org example.com\n}\n```\n\nThis way, user authenticated as `user` will be allowed to use\n`user@example.org` or `user@example.com` as a sender address.\n"
  },
  {
    "path": "docs/reference/table/file.md",
    "content": "# File \n\ntable.file module builds string-string mapping from a text file.\n\nFile is reloaded every 15 seconds if there are any changes (detected using\nmodification time). No changes are applied if file contains syntax errors.\n\nDefinition:\n```\nfile <file path>\n```\nor\n```\nfile {\n\tfile <file path>\n}\n```\n\nUsage example:\n```\n# Resolve SMTP address aliases using text file mapping.\nmodify {\n\treplace_rcpt file /etc/maddy/aliases\n}\n```\n\n## Syntax\n\nBetter demonstrated by examples:\n\n```\n# Lines starting with # are ignored.\n\n# And so are lines only with whitespace.\n\n# Whenever 'aaa' is looked up, return 'bbb'\naaa: bbb\n\n\t# Trailing and leading whitespace is ignored.\n\tccc: ddd\n\n# If there is no colon, the string is translated into \"\"\n# That is, the following line is equivalent to\n#\taaa:\naaa\n\n# If the same key is used multiple times - table.file will return\n# multiple values when queries.\nddd: firstvalue\nddd: secondvalue\n\n# Alternatively, multiple values can be specified\n# using a comma. There is no support for escaping\n# so you would have to use a different format if you require\n# comma-separated values.\nddd: firstvalue, secondvalue\n```\n\n"
  },
  {
    "path": "docs/reference/table/regexp.md",
    "content": "# Regexp rewrite table\n\nThe 'regexp' module implements table lookups by applying a regular expression\nto the key value. If it matches - 'replacement' value is returned with $N\nplaceholders being replaced with corresponding capture groups from the match.\nOtherwise, no value is returned.\n\nThe regular expression syntax is the subset of PCRE. See\n[https://golang.org/pkg/regexp/syntax](https://golang.org/pkg/regexp/syntax)/ for details.\n\n```\ntable.regexp <regexp> [replacement] {\n\tfull_match yes\n\tcase_insensitive yes\n\texpand_placeholders yes\n}\n```\n\nNote that [replacement] is optional. If it is not included - table.regexp\nwill return the original string, therefore acting as a regexp match check.\nThis can be useful in combination in `destination_in` for\nadvanced matching:\n\n```\ndestination_in regexp \".*-bounce+.*@example.com\" {\n\t...\n}\n```\n\n## Configuration directives\n\n### full_match _boolean_\nDefault: `yes`\n\nWhether to implicitly add start/end anchors to the regular expression.\nThat is, if `full_match` is `yes`, then the provided regular expression should\nmatch the whole string. With `no` - partial match is enough.\n\n---\n\n### case_insensitive _boolean_\nDefault: `yes`\n\nWhether to make matching case-insensitive.\n\n---\n\n### expand_placeholders _boolean_\nDefault: `yes`\n\nReplace '$name' and '${name}' in the replacement string with contents of\ncorresponding capture groups from the match.\n\nTo insert a literal $ in the output, use $$ in the template.\n\n## Identity table (table.identity)\n\nThe module 'identity' is a table module that just returns the key looked up.\n\n```\ntable.identity { }\n```\n\n"
  },
  {
    "path": "docs/reference/table/sql_query.md",
    "content": "# SQL query mapping\n\nThe table.sql_query module implements table interface using SQL queries.\n\nDefinition:\n\n```\ntable.sql_query {\n\tdriver <driver name>\n\tdsn <data source name>\n\tlookup <lookup query>\n\n\t# Optional:\n\tinit <init query list>\n\tlist <list query>\n\tadd <add query>\n\tdel <del query>\n\tset <set query>\n}\n```\n\nUsage example:\n\n```\n# Resolve SMTP address aliases using PostgreSQL DB.\nmodify {\n\treplace_rcpt sql_query {\n\t\tdriver postgres\n\t\tdsn \"dbname=maddy user=maddy\"\n\t\tlookup \"SELECT alias FROM aliases WHERE address = $1\"\n\t}\n}\n```\n\n## Configuration directives\n\n### driver _driver name_ \n**Required.**\n\nDriver to use to access the database.\n\nSupported drivers: `postgres`, `sqlite3` (if compiled with C support)\n\n---\n\n### dsn _data source name_\n**Required.**\n\nData Source Name to pass to the driver. For SQLite3 this is just a path to DB\nfile. For Postgres, see\n[https://pkg.go.dev/github.com/lib/pq?tab=doc#hdr-Connection\\_String\\_Parameters](https://pkg.go.dev/github.com/lib/pq?tab=doc#hdr-Connection\\_String\\_Parameters)\n\n---\n\n### lookup _query_\n**Required.**\n\nSQL query to use to obtain the lookup result.\n\nIt will get one named argument containing the lookup key. Use :key\nplaceholder to access it in SQL. The result row set should contain one row, one\ncolumn with the string that will be used as a lookup result. If there are more\nrows, they will be ignored. If there are more columns, lookup will fail.  If\nthere are no rows, lookup returns \"no results\". If there are any error - lookup\nwill fail.\n\n---\n\n### init _queries..._\nDefault: empty\n\nList of queries to execute on initialization. Can be used to configure RDBMS.\n\nExample, to improve SQLite3 performance:\n\n```\ntable.sql_query {\n\tdriver sqlite3\n\tdsn whatever.db\n\tinit \"PRAGMA journal_mode=WAL\" \\\n\t\t\"PRAGMA synchronous=NORMAL\"\n\tlookup \"SELECT alias FROM aliases WHERE address = $1\"\n}\n```\n\n---\n\n### named_args _boolean_\nDefault: `yes`\n\nWhether to use named parameters binding when executing SQL queries\nor not.\n\nNote that maddy's PostgreSQL driver does not support named parameters and\nSQLite3 driver has issues handling numbered parameters:\n[https://github.com/mattn/go-sqlite3/issues/472](https://github.com/mattn/go-sqlite3/issues/472)\n\n---\n\n### add _query_<br>list _query_<br>set _query_ <br>del _query_\nDefault: none\n\nIf queries are set to implement corresponding table operations - table becomes\n\"mutable\" and can be used in contexts that require writable key-value store.\n\n'add' query gets :key, :value named arguments - key and value strings to store.\nThey should be added to the store. The query **should** not add multiple values\nfor the same key and **should** fail if the key already exists.\n\n'list' query gets no arguments and should return a column with all keys in\nthe store.\n\n'set' query gets :key, :value named arguments - key and value and should replace the existing\nentry in the database.\n\n'del' query gets :key argument - key and should remove it from the database.\n\nIf `named_args` is set to `no` - key is passed as the first numbered parameter\n($1), value is passed as the second numbered parameter ($2).\n\n"
  },
  {
    "path": "docs/reference/table/static.md",
    "content": "# Static table\n\nThe 'static' module implements table lookups using key-value pairs in its\nconfiguration.\n\n```\ntable.static {\n\tentry KEY1 VALUE1\n\tentry KEY2 VALUE2\n\t...\n}\n```\n\n## Configuration directives\n\n### entry _key_ _value_\n\nAdd an entry to the table.\n\nIf the same key is used multiple times, the last one takes effect.\n\n"
  },
  {
    "path": "docs/reference/targets/queue.md",
    "content": "# Local queue\n\nQueue module buffers messages on disk and retries delivery multiple times to\nanother target to ensure reliable delivery.\n\nIt is also responsible for generation of DSN messages\nin case of delivery failures.\n\n## Arguments\n\nFirst argument specifies directory to use for storage.\nRelative paths are relative to the StateDirectory.\n\n## Configuration directives\n\n```\ntarget.queue {\n    target remote\n    location ...\n    max_parallelism 16\n    max_tries 4\n\tbounce {\n\t    destination example.org {\n\t        deliver_to &local_mailboxes\n\t    }\n\t    default_destination {\n\t        reject\n\t    }\n\t}\n\n    autogenerated_msg_domain example.org\n    debug no\n}\n```\n\n### target _block_name_\n**Required.** <br>\nDefault: not specified\n\nDelivery target to use for final delivery.\n\n---\n\n### location _directory_\nDefault: `StateDirectory/configuration_block_name`\n\nFile system directory to use to store queued messages.\nRelative paths are relative to the StateDirectory.\n\n---\n\n### max_parallelism _integer_\nDefault: `16`\n\nStart up to _integer_ goroutines for message processing. Basically, this option\nlimits amount of messages tried to be delivered concurrently.\n\n---\n\n### max_tries _integer_\nDefault: `20`\n\nAttempt delivery up to _integer_ times. Note that no more attempts will be done\nis permanent error occurred during previous attempt.\n\nDelay before the next attempt will be increased exponentially using the\nfollowing formula: 15mins * 1.2 ^ (n - 1) where n is the attempt number.\nThis gives you approximately the following sequence of delays:\n18mins, 21mins, 25mins, 31mins, 37mins, 44mins, 53mins, 64mins, ...\n\n---\n\n### bounce { ... }\nDefault: not specified\n\nThis configuration contains pipeline configuration to be used for generated DSN\n(Delivery Status Notification) messages.\n\nIf this is block is not present in configuration, DSNs will not be generated.\nNote, however, this is not what you want most of the time.\n\n---\n\n### autogenerated_msg_domain _domain_\nDefault: global directive value\n\nDomain to use in sender address for DSNs. Should be specified too if 'bounce'\nblock is specified.\n\n---\n\n### debug _boolean_\nDefault: `no`\n\nEnable verbose logging."
  },
  {
    "path": "docs/reference/targets/remote.md",
    "content": "# Remote MX delivery\n\nModule that implements message delivery to remote MTAs discovered via DNS MX\nrecords. You probably want to use it with queue module for reliability.\n\nIf a message check marks a message as 'quarantined', remote module\nwill refuse to deliver it.\n\n## Configuration directives\n\n```\ntarget.remote {\n    hostname mx.example.org\n    debug no\n}\n```\n\n### hostname _domain_\nDefault: global directive value\n\nHostname to use client greeting (EHLO/HELO command). Some servers require it to\nbe FQDN, SPF-capable servers check whether it corresponds to the server IP\naddress, so it is better to set it to a domain that resolves to the server IP.\n\n---\n\n### limits { ... }\nDefault: no limits\n\nSee ['limits' directive for SMTP endpoint](/reference/endpoints/smtp/#rate-concurrency-limiting).\nIt works the same except for address domains used for\nper-source/per-destination are as observed when message exits the server.\n\n---\n\n### local_ip _ip-address_\nDefault: empty\n\nChoose the local IP to bind for outbound SMTP connections.\n\n---\n\n### force_ipv4 _boolean_\nDefault: `false`\n\nForce resolving outbound SMTP domains to IPv4 addresses. Some server providers\ndo not offer a way to properly set reverse PTR domains for IPv6 addresses; this\noption makes maddy only connect to IPv4 addresses so that its public IPv4 address\nis used to connect to that server, and thus reverse PTR checks are made against\nits IPv4 address.\n\nWarning: this may break sending outgoing mail to IPv6-only SMTP servers.\n\n---\n\n### connect_timeout _duration_\nDefault: `5m`\n\nTimeout for TCP connection establishment.\n\nRFC 5321 recommends 5 minutes for \"initial greeting\" that includes TCP\nhandshake. maddy uses two separate timers - one for \"dialing\" (DNS A/AAAA\nlookup + TCP handshake) and another for \"initial greeting\". This directive\nconfigures the former. The latter is not configurable and is hardcoded to be\n5 minutes.\n\n---\n\n### command_timeout _duration_\nDefault: `5m`\n\nTimeout for any SMTP command (EHLO, MAIL, RCPT, DATA, etc).\n\nIf STARTTLS is used this timeout also applies to TLS handshake.\n\nRFC 5321 recommends 5 minutes for MAIL/RCPT and 3 minutes for\nDATA.\n\n---\n\n### submission_timeout _duration_\nDefault: `12m`\n\nTime to wait after the entire message is sent (after \"final dot\").\n\nRFC 5321 recommends 10 minutes.\n\n---\n\n### debug _boolean_\nDefault: global directive value\n\nEnable verbose logging.\n\n---\n\n### requiretls_override _boolean_\nDefault: `true`\n\nAllow local security policy to be disabled using 'TLS-Required' header field in\nsent messages. Note that the field has no effect if transparent forwarding is\nused, message body should be processed before outbound delivery starts for it\nto take effect (e.g. message should be queued using 'queue' module).\n\n---\n\n### relaxed_requiretls _boolean_\nDefault: `true`\n\nThis option disables strict conformance with REQUIRETLS specification and\nallows forwarding of messages 'tagged' with REQUIRETLS to MXes that are not\nadvertising REQUIRETLS support. It is meant to allow REQUIRETLS use without the\nneed to have support from all servers. It is based on the assumption that\nserver referenced by MX record is likely the final destination and therefore\nthere is only need to secure communication towards it and not beyond.\n\n---\n\n### conn_reuse_limit _integer_\nDefault: `10`\n\nAmount of times the same SMTP connection can be used.\nConnections are never reused if the previous DATA command failed.\n\n---\n\n### conn_max_idle_count _integer_\nDefault: `10`\n\nMax. amount of idle connections per recipient domains to keep in cache.\n\n---\n\n### conn_max_idle_time _integer_\nDefault: `150` (2.5 min)\n\nAmount of time the idle connection is still considered potentially usable.\n\n---\n\n## Security policies\n\n### mx_auth { ... }\nDefault: no policies\n\n'remote' module implements a number of of schemes and protocols necessary to\nensure security of message delivery. Most of these schemes are concerned with\nauthentication of recipient server and TLS enforcement.\n\nTo enable mechanism, specify its name in the `mx_auth` directive block:\n\n```\nmx_auth {\n\tdane\n\tmtasts\n}\n```\n\nAdditional configuration is possible if supported by the mechanism by\nspecifying additional options as a block for the corresponding mechanism.\nE.g.\n\n```\nmtasts {\n\tcache ram\n}\n```\n\nIf the `mx_auth` directive is not specified, no mechanisms are enabled. Note\nthat, however, this makes outbound SMTP vulnerable to a numerous downgrade\nattacks and hence not recommended.\n\nIt is possible to share the same set of policies for multiple 'remote' module\ninstances by defining it at the top-level using `mx_auth` module and then\nreferencing it using standard & syntax:\n\n```\nmx_auth outbound_policy {\n\tdane\n\tmtasts {\n\t\tcache ram\n\t}\n}\n\n# ... somewhere else ...\n\ndeliver_to remote {\n\tmx_auth &outbound_policy\n}\n\n# ... somewhere else ...\n\ndeliver_to remote {\n\tmx_auth &outbound_policy\n\ttls_client { ... }\n}\n```\n\n---\n\n### MTA-STS\n\nChecks MTA-STS policy of the recipient domain. Provides proper authentication\nand TLS enforcement for delivery, but partially vulnerable to persistent active\nattacks.\n\nSets MX level to \"mtasts\" if the used MX matches MTA-STS policy even if it is\nnot set to \"enforce\" mode.\n\n```\nmtasts {\n\tcache fs\n\tfs_dir StateDirectory/mtasts_cache\n}\n```\n\n### cache `fs` | `ram`\nDefault: `fs`\n\nStorage to use for MTA-STS cache. 'fs' is to use a filesystem directory, 'ram'\nto store the cache in memory.\n\nIt is recommended to use 'fs' since that will not discard the cache (and thus\ncause MTA-STS security to disappear) on server restart. However, using the RAM\ncache can make sense for high-load configurations with good uptime.\n\n### fs_dir _directory_\nDefault: `StateDirectory/mtasts_cache`\n\nFilesystem directory to use for policies caching if 'cache' is set to 'fs'.\n\n---\n\n### DNSSEC\n\nChecks whether MX records are signed. Sets MX level to \"dnssec\" is they are.\n\nmaddy does not validate DNSSEC signatures on its own. Instead it relies on\nthe upstream resolver to do so by causing lookup to fail when verification\nfails and setting the AD flag for signed and verified zones. As a safety\nmeasure, if the resolver is not 127.0.0.1 or ::1, the AD flag is ignored.\n\nDNSSEC is currently not supported on Windows and other platforms that do not\nhave the /etc/resolv.conf file in the standard format.\n\n```\ndnssec { }\n```\n\n---\n\n### DANE\n\nChecks TLSA records for the recipient MX. Provides downgrade-resistant TLS\nenforcement.\n\nSets TLS level to \"authenticated\" if a valid and matching TLSA record uses\nDANE-EE or DANE-TA usage type.\n\nSee above for notes on DNSSEC. DNSSEC support is required for DANE to work.\n\n```\ndane { }\n```\n\n---\n\n### Local policy\n\nChecks effective TLS and MX levels (as set by other policies) against local\nconfiguration.\n\n```\nlocal_policy {\n\tmin_tls_level none\n\tmin_mx_level none\n}\n```\n\nUsing `local_policy off` is equivalent to setting both directives to `none`.\n\n### min_tls_level `none` | `encrypted` | `authenticated`\nDefault: `encrypted`\n\nSet the minimal TLS security level required for all outbound messages.\n\nSee [Security levels](/seclevels) page for details.\n\n### min_mx_level `none` | `mtasts` | `dnssec`\nDefault: `none`\n\nSet the minimal MX security level required for all outbound messages.\n\nSee [Security levels](/seclevels) page for details.\n\n"
  },
  {
    "path": "docs/reference/targets/smtp.md",
    "content": "# SMTP & LMTP transparent forwarding\n\nModule that implements transparent forwarding of messages over SMTP.\n\nUse in pipeline configuration:\n\n```\ndeliver_to smtp tcp://127.0.0.1:5353\n# or\ndeliver_to smtp tcp://127.0.0.1:5353 {\n  # Other settings, see below.\n}\n```\n\ntarget.lmtp can be used instead of target.smtp to\nuse LMTP protocol.\n\nEndpoint addresses use format described in [Configuration files syntax / Address definitions](/reference/config-syntax/#address-definitions).\n\n## Configuration directives\n\n```\ntarget.smtp {\n    debug no\n    tls_client {\n        ...\n    }\n    attempt_starttls yes\n    require_tls no\n    auth off\n    targets tcp://127.0.0.1:2525\n    connect_timeout 5m\n    command_timeout 5m\n    submission_timeout 12m\n}\n```\n\n### debug _boolean_\nDefault: global directive value\n\nEnable verbose logging.\n\n---\n\n### tls_client { ... }\nDefault: not specified\n\nAdvanced TLS client configuration options. See [TLS configuration / Client](/reference/tls/#client) for details.\n\n---\n\n### starttls _boolean_\nDefault: `yes` (`no` for `target.lmtp`)\n\nUse STARTTLS to enable TLS encryption. If STARTTLS is not supported\nby the remote server - connection will fail.\n\nmaddy will use `localhost` as HELO hostname before STARTTLS\nand will only send its actual hostname after STARTTLS.\n\n### attempt_starttls _boolean_\nDefault: `yes` (`no` for `target.lmtp`)\n\nDEPRECATED: Equivalent to `starttls`. Plaintext fallback is no longer\nsupported.\n\n---\n\n### require_tls _boolean_\nDefault: `no`\n\nDEPRECATED: Ignored. Set `starttls yes` to use STARTLS.\n\n---\n\n### auth `off` | `plain` _username_ _password_ | `forward`  | `external`\nDefault: `off`\n\nSpecify the way to authenticate to the remote server.\nValid values:\n\n- `off` – No authentication.\n- `plain` – Authenticate using specified username-password pair.\n  **Don't use** this without enforced TLS (`require_tls`).\n- `forward` – Forward credentials specified by the client.\n  **Don't use** this without enforced TLS (`require_tls`).\n- `external` – Request \"external\" SASL authentication. This is usually used for\n  authentication using TLS client certificates. See [TLS configuration / Client](/reference/tls/#client) for details.\n\n---\n\n### targets _endpoints..._\n**Required.**<br>\nDefault: not specified\n\nList of remote server addresses to use. See [Address definitions](/reference/config-syntax/#address-definitions)\nfor syntax to use.  Basically, it is `tcp://ADDRESS:PORT`\nfor plain SMTP and `tls://ADDRESS:PORT` for SMTPS (aka SMTP with Implicit\nTLS).\n\nMultiple addresses can be specified, they will be tried in order until connection to\none succeeds (including TLS handshake if TLS is required).\n\n---\n\n### connect_timeout _duration_\nDefault: `5m`\n\nSame as for target.remote.\n\n---\n\n### command_timeout _duration_\nDefault: `5m`\n\nSame as for target.remote.\n\n---\n\n### submission_timeout _duration_\nDefault: `12m`\n\nSame as for target.remote.\n"
  },
  {
    "path": "docs/reference/tls-acme.md",
    "content": "# Automatic certificate management via ACME\n\nMaddy supports obtaining certificates using ACME protocol.\n\nTo use it, create a configuration name for `tls.loader.acme`\nand reference it from endpoints that should use automatically\nconfigured certificates:\n\n```\ntls.loader.acme local_tls {\n    email put-your-email-here@example.org\n    agreed # indicate your agreement with Let's Encrypt ToS\n    challenge dns-01\n}\n\nsmtp tcp://127.0.0.1:25 {\n    tls &local_tls\n    ...\n}\n```\n\nYou can also use a global `tls` directive to use automatically\nobtained certificates for all endpoints:\n\n```\ntls {\n    loader acme {\n        email maddy-acme@example.org\n        agreed\n        challenge dns-01\n    }\n}\n```\n\nNote: `tls &local_tls` as a global directive won't work because\nglobal directives are initialized before other configuration blocks.\n\nCurrently the only supported challenge is `dns-01` one therefore\nyou also need to configure the DNS provider:\n\n```\ntls.loader.acme local_tls {\n    email maddy-acme@example.org\n    agreed\n    challenge dns-01\n    dns PROVIDER_NAME {\n        ...\n    }\n}\n```\n\nSee below for supported providers and necessary configuration\nfor each.\n\n## Configuration directives\n\n```\ntls.loader.acme {\n    debug off\n    hostname example.maddy.invalid\n    store_path /var/lib/maddy/acme\n    ca https://acme-v02.api.letsencrypt.org/directory\n    test_ca https://acme-staging-v02.api.letsencrypt.org/directory\n    email test@maddy.invalid\n    agreed off\n    challenge dns-01\n    dns ...\n}\n```\n\n### debug _boolean_\nDefault: global directive value\n\nEnable debug logging.\n\n---\n\n### hostname _str_\n**Required.**<br>\nDefault: global directive value\n\nDomain name to issue certificate for.\n\n---\n\n### store_path _path_\nDefault: `state_dir/acme`\n\nWhere to store issued certificates and associated metadata.\nCurrently only filesystem-based store is supported.\n\n---\n\n### ca _url_\nDefault: Let's Encrypt production CA\n\nURL of ACME directory to use.\n\n---\n\n### test_ca _url_\nDefault: Let's Encrypt staging CA\n\nURL of ACME directory to use for retries should\nprimary CA fail.\n\nmaddy will keep attempting to issues certificates\nusing `test_ca` until it succeeds then it will switch\nback to the one configured via 'ca' option.\n\nThis avoids rate limit issues with production CA.\n\n---\n\n### override_domain _domain_\nDefault: not set\n\nOverride the domain to set the TXT record on for DNS-01 challenge.\nThis is to delegate the challenge to a different domain.\n\nSee https://www.eff.org/deeplinks/2018/02/technical-deep-dive-securing-automation-acme-dns-challenge-validation\nfor explanation why this might be useful.\n\n---\n\n### email _str_\nDefault: not set\n\nEmail to pass while registering an ACME account.\n\n---\n\n### agreed _boolean_\nDefault: false\n\nWhether you agreed to ToS of the CA service you are using.\n\n---\n\n### challenge `dns-01`\nDefault: not set\n\nChallenge(s) to use while performing domain verification.\n\n## DNS providers\n\nSupport for some providers is not provided by standard builds.\nTo be able to use these, you need to compile maddy\nwith \"libdns_PROVIDER\" build tag.\nE.g.\n```\n./build.sh --tags 'libdns_googleclouddns'\n```\n\n- gandi\n\n```\ndns gandi {\n    api_token \"token\"\n}\n```\n\n- digitalocean\n\n```\ndns digitalocean {\n    api_token \"...\"\n}\n```\n\n- cloudflare\n\nSee [https://github.com/libdns/cloudflare#authenticating](https://github.com/libdns/cloudflare#authenticating)\n\n```\ndns cloudflare {\n    api_token \"...\"\n}\n```\n\n- vultr\n\n```\ndns vultr {\n    api_token \"...\"\n}\n```\n\n- hetzner\n\n```\ndns hetzner {\n    api_token \"...\"\n}\n```\n\n- namecheap\n\n```\ndns namecheap {\n    api_key \"...\"\n    api_username \"...\"\n\n    # optional: API endpoint, production one is used if not set.\n    endpoint \"https://api.namecheap.com/xml.response\"\n\n    # optional: your public IP, discovered using icanhazip.com if not set\n    client_ip 1.2.3.4\n}\n```\n\n- googleclouddns (non-default)\n\n```\ndns googleclouddns {\n    project \"project_id\"\n    service_account_json \"path\"\n}\n```\n\n- route53 (non-default)\n\n```\ndns route53 {\n    secret_access_key \"...\"\n    access_key_id \"...\"\n    # or use environment variables: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY\n}\n```\n\n- leaseweb (non-default)\n\n```\ndns leaseweb {\n    api_key \"key\"\n}\n```\n\n- metaname (non-default)\n\n```\ndns metaname {\n    api_key \"key\"\n    account_ref \"reference\"\n}\n```\n\n- alidns (non-default)\n\n```\ndns alidns {\n    key_id \"...\"\n    key_secret \"...\"\n}\n```\n\n- namedotcom (non-default)\n\n```\ndns namedotcom {\n    user \"...\"\n    token \"...\"\n}\n```\n\n- rfc2136 (non-default)\n\n```\ndns rfc2136 {\n    key_name \"...\"\n    # Secret\n    key \"...\"\n    # HMAC algorithm used to generate the key, lowercase, e.g. hmac-sha512\n    key_alg \"...\"\n    # server to which the dynamic update will be sent, e.g. 127.0.0.1\n    # you can also specify the port: 127.0.0.1:53\n    server \"...\"\n}\n```\n\n- acmedns (non-default)\n\n```\ndns acmedns {\n    username \"...\"\n    password \"...\"\n    subdomain \"...\"\n    server_url \"...\"\n}\n```\n"
  },
  {
    "path": "docs/reference/tls.md",
    "content": "# TLS configuration\n\n## Server-side\n\nTLS certificates are obtained by modules called \"certificate loaders\". 'tls' directive\narguments specify name of loader to use and arguments. Due to syntax limitations\nadvanced configuration for loader should be specified using 'loader' directive, see\nbelow.\n\n```\ntls file cert.pem key.pem {\n\tprotocols tls1.2 tls1.3\n\tcurves X25519\n\tciphers ...\n}\n\ntls {\n\tloader file cert.pem key.pem {\n\t\t# Options for loader go here.\n\t}\n\tprotocols tls1.2 tls1.3\n\tcurves X25519\n\tciphers ...\n}\n```\n\n### Available certificate loaders\n\n- `file` – Accepts argument pairs specifying certificate and then key.\n  E.g. `tls file certA.pem keyA.pem certB.pem keyB.pem`.\n  If multiple certificates are listed, SNI will be used.\n- `acme` – Automatically obtains a certificate using ACME protocol (Let's Encrypt)\n- `off` – Not really a loader but a special value for tls directive, \n  explicitly  disables TLS for endpoint(s).\n\n## Advanced TLS configuration\n\n**Note: maddy uses secure defaults and TLS handshake is resistant to active downgrade attacks. There is no need to change anything in most cases.**\n\n---\n\n### protocols _min-version_ _max-version_ | _version_\nDefault: `tls1.0 tls1.3`\n\nMinimum/maximum accepted TLS version. If only one value is specified, it will\nbe the only one usable version.\n\nValid values are: `tls1.0`, `tls1.1`, `tls1.2`, `tls1.3`\n\n---\n\n### ciphers _ciphers..._ \nDefault: Go version-defined set of 'secure ciphers', ordered by hardware\nperformance\n\nList of supported cipher suites, in preference order. Not used with TLS 1.3.\n\nValid values:\n\n- `RSA-WITH-RC4128-SHA`\n- `RSA-WITH-3DES-EDE-CBC-SHA`\n- `RSA-WITH-AES128-CBC-SHA`\n- `RSA-WITH-AES256-CBC-SHA`\n- `RSA-WITH-AES128-CBC-SHA256`\n- `RSA-WITH-AES128-GCM-SHA256`\n- `RSA-WITH-AES256-GCM-SHA384`\n- `ECDHE-ECDSA-WITH-RC4128-SHA`\n- `ECDHE-ECDSA-WITH-AES128-CBC-SHA`\n- `ECDHE-ECDSA-WITH-AES256-CBC-SHA`\n- `ECDHE-RSA-WITH-RC4128-SHA`\n- `ECDHE-RSA-WITH-3DES-EDE-CBC-SHA`\n- `ECDHE-RSA-WITH-AES128-CBC-SHA`\n- `ECDHE-RSA-WITH-AES256-CBC-SHA`\n- `ECDHE-ECDSA-WITH-AES128-CBC-SHA256`\n- `ECDHE-RSA-WITH-AES128-CBC-SHA256`\n- `ECDHE-RSA-WITH-AES128-GCM-SHA256`\n- `ECDHE-ECDSA-WITH-AES128-GCM-SHA256`\n- `ECDHE-RSA-WITH-AES256-GCM-SHA384`\n- `ECDHE-ECDSA-WITH-AES256-GCM-SHA384`\n- `ECDHE-RSA-WITH-CHACHA20-POLY1305`\n- `ECDHE-ECDSA-WITH-CHACHA20-POLY1305`\n\n---\n\n### curves _curves..._\nDefault: defined by Go version\n\nThe elliptic curves that will be used in an ECDHE handshake, in preference\norder.\n\nValid values: `p256`, `p384`, `p521`, `X25519`.\n\n## Client\n\n`tls_client` directive allows to customize behavior of TLS client implementation,\nnotably adjusting minimal and maximal TLS versions and allowed cipher suites,\nenabling TLS client authentication.\n\n```\ntls_client {\n    protocols tls1.2 tls1.3\n    ciphers ...\n    curves X25519\n    root_ca /etc/ssl/cert.pem\n\n    cert /etc/ssl/private/maddy-client.pem\n    key /etc/ssl/private/maddy-client.pem\n}\n```\n\n---\n\n###  protocols _min-version_ _max-version_ | _version_\nDefault: `tls1.0 tls1.3`\n\nMinimum/maximum accepted TLS version. If only one value is specified, it will\nbe the only one usable version.\n\nValid values are: `tls1.0`, `tls1.1`, `tls1.2`, `tls1.3`\n\n---\n\n### ciphers _ciphers..._\nDefault: Go version-defined set of 'secure ciphers', ordered by hardware\nperformance\n\nList of supported cipher suites, in preference order. Not used with TLS 1.3.\n\nSee TLS server configuration for list of supported values.\n\n---\n\n### curves _curves..._\nDefault: defined by Go version\n\nThe elliptic curves that will be used in an ECDHE handshake, in preference\norder.\n\nValid values: `p256`, `p384`, `p521`, `X25519`.\n\n---\n\n### root_ca _paths..._\nDefault: system CA pool\n\nList of files with PEM-encoded CA certificates to use when verifying\nserver certificates.\n\n---\n\n###  cert _cert-path_ <br> key _key-path_\nDefault: not specified\n\nPresent the specified certificate when server requests a client certificate.\nFiles should use PEM format. Both directives should be specified.\n"
  },
  {
    "path": "docs/seclevels.md",
    "content": "# Outbound delivery security\n\nmaddy implements a number of schemes and protocols for discovery and\nenforcement of security features supported by the recipient MTA.\n\n## Introduction to the problems of secure SMTP\n\nOutbound delivery security involves two independent problems:\n\n- MX record authentication\n- TLS enforcement\n\n### MX record authentication\n\nWhen MTA wants to deliver a message to a mailbox at remote domain, it needs to\ndiscover the server to use for it. It is done through the lookup of DNS MX\nrecords for the recipient.\n\nProblem arises from the fact that DNS does not have any cryptographic\nprotection and so any malicious actor can technically modify the response to\ncontain any server. And MTA would use that server!\n\nThere are two protocols that solve this problem: MTA-STS and DNSSEC.\nFormer requires the MTA to verify used records against a list of rules published\nvia HTTPS. Later cryptographically signs the records themselves.\n\n### TLS enforcement\n\nBy default, server-server SMTP is unencrypted. If remote server supports TLS,\nit is advertised via the ESMTP extension named STARTTLS, but malicious actor\ncontrolling communication channel can hide the support for STARTTLS and sender\nMTA will have to use plaintext. There needs to be a out-of-band authenticated\nchannel to indicate TLS support (and to require its use).\n\nMTA-STS and DANE solve this problem. In the first case, if policy is in\n\"enforce\" mode then MTA is required to use TLS when delivering messages to a\nremote server. DANE does pretty much the same thing, but using DNSSEC-signed\nTLSA records.\n\n## maddy policy details\n\nmaddy defines two values indicating how \"secure\" delivery of message will be:\n\n- MX security level\n- TLS security level\n\nThese values correspond to the problems described above. On delivery, the\nestablished connection to the remote server is \"ranked\" using these values and\nthen they are compared against a number of policies (including local\nconfiguration). If the effective value is lower than the required one, the\nconnection is closed and next candidate server is used. If all connections fail\nthis way - the delivery is failed (or deferred if there was a temporary error\nwhen checking policies).\n\nBelow is the table summarizing the security level values defined in maddy and\nprotection they offer.\n\n| MX/TLS level  | None | Encrypted | Authenticated        |\n| ------------- | ---- | --------- | -------------------- |\n|     None      |  -   |    P      |      P               |\n|    MTA-STS    |  -   |    P      |      PA (see note 1) |\n|    DNSSEC     |  -   |    P      |      PA              |\n\nLegend: P - protects against passive attacks; A - protects against active\nattacks\n\n- MX level: None. MX candidate was returned as a result of DNS lookup for the\n  recipient domain, no additional checks done.\n- MX level: MTA-STS. Used MX matches the MTA-STS policy published by the\n  recipient domain (even one in testing mode).\n- MX level: DNSSEC. MX record is signed.\n\n- TLS level: None. Plaintext connection was established, TLS is not available\n  or failed.\n- TLS level: Encrypted. TLS connection was established, the server certificate\n  failed X.509 and DANE verification.\n- TLS level: Authenticated. TLS connection was established, the server\n  certificate passes X.509 **or** DANE verification.\n\n**Note 1:** Persistent attacker able to control network connection can\ninterfere with policy refresh, downgrading protection to be secure only against\npassive attacks.\n\n## maddy security policies\n\nSee [Remote MX delivery](/reference/targets/remote/) for description of configuration options available for each policy mechanism\nsupported by maddy.\n\n[RFC 8461 Section 10.2]: https://www.rfc-editor.org/rfc/rfc8461.html#section-10.2 (SMTP MTA Strict Transport Security - 10.2. Preventing Policy Discovery)\n"
  },
  {
    "path": "docs/third-party/dovecot.md",
    "content": "# Dovecot\n\nBuiltin maddy IMAP server may not match your requirements in terms of\nperformance, reliability or anything. For this reason it is possible to\nintegrate it with any external IMAP server that implements necessary\nprotocols. Here is how to do it for Dovecot.\n\n1. Get rid of `imap` endpoint and existing `local_authdb` and `local_mailboxes`\n   blocks.\n\n2. Setup Dovecot to provide LMTP endpoint\n\nHere is an example configuration snippet:\n```\n# /etc/dovecot/dovecot.conf\nprotocols = imap lmtp\n\n# /etc/dovecot/conf.d/10-master.conf\nservice lmtp {\n unix_listener lmtp-maddy {\n   mode = 0600\n   user = maddy\n  }\n}\n```\n\nAdd `local_mailboxes` block to maddy config using `target.lmtp` module:\n```\ntarget.lmtp local_mailboxes {\n    targets unix:///var/run/dovecot/lmtp-maddy\n}\n```\n\n### Authentication\n\nIn addition to MTA service, maddy also provides Submission service, but it\nneeds authentication provider data to work correctly, maddy can use Dovecot\nSASL authentication protocol for it.\n\nYou need the following in Dovecot's `10-master.conf`:\n```\nservice auth {\n  unix_listener auth-maddy-client {\n    mode = 0660\n    user = maddy\n  }\n}\n```\n\nThen just configure `dovecot_sasl` module for `submission`:\n```\nsubmission ... {\n    auth dovecot_sasl unix:///var/run/dovecot/auth-maddy-client\n    ... other configuration ...\n}\n```\n\n## Other IMAP servers\n\nIntegration with other IMAP servers might be more problematic because there is\nno standard protocol for authentication delegation. You might need to configure\nthe IMAP server to implement MSA functionality by forwarding messages to maddy\nfor outbound delivery. This might require more configuration changes on maddy\nside since by default it will not allow relay on port 25 even for localhost\naddresses. The easiest way is to create another SMTP endpoint on some port\n(probably Submission port):\n```\nsmtp tcp://127.0.0.1:587 {\n    deliver_to &remote_queue\n}\n```\nAnd configure IMAP server's Submission service to forward outbound messages\nthere.\n\nDepending on how Submission service is implemented you may also need to route\nmessages for local domains back to it via LMTP:\n```\nsmtp tcp://127.0.0.1:587 {\n    destination postmaster $(local_domains) {\n        deliver_to &local_routing\n    }\n    default_destination {\n        deliver_to &remote_queue\n    }\n}\n```\n\n"
  },
  {
    "path": "docs/third-party/mailman3.md",
    "content": "# Mailman 3\n\nSetting up Mailman 3 with maddy involves some additional work as compared to\nother MTAs as there is no Python package in Mailman suite that can generate\naddress lists in format supported by maddy.\n\nWe assume you are already familiar with Mailman configuration guidelines and\nhow stuff works in general/for other MTAs.\n\n## Accepting messages\n\nFirst of all, you need to use NullMTA package for mta.incoming so Mailman will\nnot try to generate any configs. LMTP listener is configured as usual.\n```\n[mta]\nincoming: mailman.mta.null.NullMTA\nlmtp_host: 127.0.0.1\nlmtp_port: 8024\n```\n\nAfter that, you will need to configure maddy to send messages to Mailman.\n\nThe preferable way of doing so is destination_in and table.regexp:\n```\nmsgpipeline local_routing {\n    destination_in regexp \"first-mailinglist(-(bounces\\+.*|confirm\\+.*|join|leave|owner|request|subscribe|unsubscribe))?@lists.example.org\" {\n        deliver_to lmtp tcp://127.0.0.1:8024\n    }\n    destination_in regexp \"second-mailinglist(-(bounces\\+.*|confirm\\+.*|join|leave|owner|request|subscribe|unsubscribe))?@lists.example.org\" {\n        deliver_to lmtp tcp://127.0.0.1:8024\n    }\n\n    ...\n}\n```\n\nA more simple option is also meaningful (provided you have a separate domain\nfor lists):\n```\nmsgpipeline local_routing {\n    destination lists.example.org {\n        deliver_to lmtp tcp://127.0.0.1:8024\n    }\n\n    ...\n}\n```\nBut this variant will lead to inefficient handling of non-existing subaddresses.\nSee [Mailman Core issue 14](https://gitlab.com/mailman/mailman/-/issues/14) for\ndetails. (5 year old issue, sigh...)\n\n## Sending messages\n\nIt is recommended to configure Mailman to send messages using Submission port\nwith authentication and TLS as maddy does not allow relay on port 25 for local\nclients as some MTAs do:\n```\n[mta]\n# ... incoming configuration here ...\noutgoing: mailman.mta.deliver.deliver\nsmtp_host: mx.example.org\nsmtp_port: 465\nsmtp_user: mailman@example.org\nsmtp_pass: something-very-secret\nsmtp_secure_mode: smtps\n```\n\nIf you do not want to use TLS and/or authentication you can create a separate\nendpoint and just point Mailman to it. E.g.\n```\nsmtp tcp://127.0.0.1:2525 {\n    destination postmaster $(local_domains) {\n        deliver_to &local_routing\n    }\n    default_destination {\n        deliver_to &remote_queue\n    }\n}\n```\n\nNote that if you use a separate domain for lists, it need to be included in\nlocal_domains macro in default config. This will ensure maddy signs messages\nusing DKIM for outbound messages. It is also highly recommended to configure\nARC in Mailman 3.\n"
  },
  {
    "path": "docs/third-party/rspamd.md",
    "content": "# rspamd\n\nmaddy has direct support for rspamd HTTP protocol. There is no need to use\nmilter proxy.\n\nIf rspamd is running locally, it is enough to just add `rspamd` check\nwith default configuration into appropriate check block (probably in\nlocal_routing):\n```\ncheck {\n    ...\n    rspamd\n}\n```\n\nYou might want to disable builtin SPF, DKIM and DMARC for performance\nreasons but note that at the moment, maddy will not generate\nAuthentication-Results field with rspamd results.\n\nIf rspamd is not running on a local machine, change api_path to point\nto the \"normal\" worker socket:\n\n```\ncheck {\n    ...\n    rspamd {\n        api_path http://spam-check.example.org:11333\n    }\n}\n```\n\nDefault mapping of rspamd action -> maddy action is as follows:\n\n- \"add header\" => Quarantine\n- \"rewrite subject\" => Quarantine\n- \"soft reject\" => Reject with temporary error\n- \"reject\" => Reject with permanent error\n- \"greylist\" => Ignored"
  },
  {
    "path": "docs/third-party/smtp-servers.md",
    "content": "# External SMTP server\n\nIt is possible to use maddy as an IMAP server only and have it interface with\nexternal SMTP server using standard protocols.\n\nHere is the minimal configuration that creates a local IMAP index, credentials\ndatabase and IMAP endpoint:\n```\n# Credentials DB.\ntable.pass_table local_authdb {\n    table sql_table {\n        driver sqlite3\n        dsn credentials.db\n        table_name passwords\n    }\n}\n\n# IMAP storage/index.\nstorage.imapsql local_mailboxes {\n    driver sqlite3\n    dsn imapsql.db\n}\n\n# IMAP endpoint using these above.\nimap tls://0.0.0.0:993 tcp://0.0.0.0:143 {\n    auth &local_authdb\n    storage &local_mailboxes\n}\n```\n\nTo accept local messages from an external SMTP server\nit is possible to create an LMTP endpoint:\n```\n# LMTP endpoint on Unix socket delivering to IMAP storage\n# in previous config snippet.\nlmtp unix:/run/maddy/lmtp.sock {\n    hostname mx.maddy.test\n\n    deliver_to &local_mailboxes\n}\n```\n\nLook up documentation for your SMTP server on how to make it\nsend messages using LMTP to /run/maddy/lmtp.sock.\n\nTo handle authentication for Submission (client-server SMTP) SMTP server\nneeds to access credentials database used by maddy. maddy implements\nserver side of Dovecot authentication protocol so you can use\nit if SMTP server implements \"Dovecot SASL\" client.\n\nTo create a Dovecot-compatible sasld endpoint, add the following configuration\nblock:\n```\n# Dovecot-compatible sasld endpoint using data from local_authdb.\ndovecot_sasld unix:/run/maddy/auth-client.sock {\n    auth &local_authdb\n}\n```\n"
  },
  {
    "path": "docs/tutorials/alias-to-remote.md",
    "content": "# Forward messages to a remote MX\n\nDefault maddy configuration is done in a way that does not result in any\noutbound messages being sent as a result of port 25 traffic.\n\nIn particular, this means that if you handle messages for example.org but not\nexample.com and have the following in your aliases file (e.g. /etc/maddy/aliases):\n\n```\nfoxcpp@example.org: foxcpp@example.com\n```\n\nYou will get \"User does not exist\" error when attempting to send a message to\nfoxcpp@example.org because foxcpp@example.com does not exist on as a local\nuser.\n\nSome users may want to make it work, but it is important to understand the\nconsequences of such configuration:\n\n- Flooding your server will also flood the remote server.\n- If your spam filtering is not good enough, you will send spam to the remote\n  server.\n\nIn both cases, you might harm the reputation of your server (e.g. get your IP\nlisted in a DNSBL).\n\n**So, this is a bad practice. Do so only if you clearly understand the\nconsequences (including the Bounce handling section below).**\n\nIf you want to do it anyway, here is the part of the configuration that needs\ntweaking:\n\n```\nmsgpipeline local_routing {\n    destination postmaster $(local_domains) {\n        modify {\n            replace_rcpt regexp \"(.+)\\+(.+)@(.+)\" \"$1@$3\"\n            replace_rcpt file /etc/maddy/aliases\n        }\n\n        deliver_to &local_mailboxes\n    }\n\n    default_destination {\n        reject 550 5.1.1 \"User doesn't exist\"\n    }\n}\n```\n\nIn default configuration, `local_routing` block is responsible for handling\nmessages that are received via SMTP or Submission and have the initial\ndestination address at a local domain.\n\nNote the `modify { }` block being nested inside `destination` and then followed\nby unconditional `deliver_to &local_mailboxes`. This means: if address is\non `$(local_domains)`, apply aliases and deliver to mailboxes from\n`&local_mailboxes`.\n\nThe problem here is that recipients are matched before aliases are resolved so\nin the end, maddy attempts to look up foxcpp@example.com locally. The solution\nis to insert another step into the pipeline configuration to rerun matching\n*after* aliases are resolved. This can be done using the 'reroute' directive:\n\n```\nmsgpipeline local_routing {\n    destination postmaster $(local_domains) {\n        modify {\n            replace_rcpt file /etc/maddy/aliases\n\t\t\t...\n        }\n\n\t\treroute {\n\t\t\tdestination postmaster $(local_domains) {\n\t\t\t\tdeliver_to &local_mailboxes\n\t\t\t}\n\t\t\tdefault_destination {\n\t\t\t\tdeliver_to &remote_queue\n\t\t\t}\n\t\t}\n    }\n\n    default_destination {\n        reject 550 5.1.1 \"User doesn't exist\"\n    }\n}\n```\n\n## Bounce handling\n\nOnce the message is delivered to `remote_queue`, it will follow the usual path\nfor outbound delivery, including queuing and multiple attempts. This also\nmeans bounce messages will be generated on failures. When accepting messages\nfrom arbitrary senders via the 25 port, the DSN recipient will be whatever\nsender specifies in the MAIL FROM command. This is prone to [collateral spam]\nwhen an automatically generated bounce message gets sent to a spoofed address.\n\nHowever, the default maddy configuration ensures that in this case, the NDN\nwill be delivered only if the original sender is a local user. Backscatter can\nnot happen if the sender spoofed a local address since such messages will not\nbe accepted in the first place.\n\nYou can also configure maddy to send bounce messages to remote\naddresses, but in this case, you should configure a really strict local policy\nto make sure the sender address is not spoofed. There is no detailed\nexplanation of how to do this since this is a terrible idea in general.\n\n[collateral spam]: https://en.wikipedia.org/wiki/Backscatter_(e-mail)\n\n## Transparent forwarding\n\nAs an alternative to silently dropping messages on remote delivery failures,\nyou might want to use transparent forwarding and reject the message without\naccepting it first (\"connection-stage rejection\").\n\nTo do so, simply do not use the queue, replace\n```\ndeliver_to &remote_queue\n```\nwith\n```\ndeliver_to &outbound_delivery\n```\n(assuming outbound_delivery refers to target.remote block)\n"
  },
  {
    "path": "docs/tutorials/building-from-source.md",
    "content": "# Building from source\n\n## System dependencies\n\nYou need C toolchain, Go toolchain and Make:\n\nOn Debian-based system this should work:\n```\napt-get install golang-1.23 gcc libc6-dev make\n```\n\nAdditionally, if you want manual pages, you should also have scdoc installed.\nFiguring out the appropriate way to get scdoc is left as an exercise for\nreader (for Ubuntu 22.04 LTS it is in repositories).\n\n## Recent Go toolchain\n\nmaddy depends on a rather recent Go toolchain version that may not be\navailable in some distributions (*cough* Debian *cough*).\n\n`go` command in Go 1.21 or newer will automatically download up-to-date\ntoolchain to build maddy. It is necessary to run commands below only\nif you have `go` command version older than 1.21.\n\n```\nwget \"https://go.dev/dl/go1.23.5.linux-amd64.tar.gz\"\ntar xf \"go1.23.5.linux-amd64.tar.gz\"\nexport GOROOT=\"$PWD/go\"\nexport PATH=\"$PWD/go/bin:$PATH\"\n```\n\n## Step-by-step\n\n1. Clone repository\n```\n$ git clone https://github.com/foxcpp/maddy.git\n$ cd maddy\n```\n\n2. Select the appropriate version to build:\n```\n$ git checkout v0.8.0      # a specific release\n$ git checkout master      # next bugfix release\n$ git checkout dev         # next feature release\n```\n\n3. Build & install it\n```\n$ ./build.sh\n$ sudo ./build.sh install\n```\n\n4. Finish setup as described in [Setting up](../setting-up) (starting from System configuration).\n\n\n"
  },
  {
    "path": "docs/tutorials/pam.md",
    "content": "# Using PAM authentication\n\nmaddy supports user authentication using PAM infrastructure via `auth.pam`\nmodule.\n\nIn order to use it, however, either maddy itself should be compiled\nwith libpam support or a helper executable should be built and\ninstalled into an appropriate directory.\n\nIt is recommended to use builtin libpam support if you are using\nPAM as an intermediate for authentication provider not directly\nsupported by maddy.\n\nIf PAM authentication requires privileged access on the host system\n(e.g. pam_unix.so aka /etc/shadow) then it is recommended to use\na privileged helper executable since maddy process itself won't\nhave access to it.\n\n## Built-in PAM support\n\nBinary artifacts provided for releases do not come with\nlibpam support. You should build maddy from source.\n\nSee [here](../building-from-source) for detailed instructions.\n\nYou should have libpam development files installed (`libpam-dev`\npackage on Ubuntu/Debian).\n\nThen add `--tags 'libpam'` to the build command:\n```\n./build.sh --tags 'libpam'\n```\n\nThen you should be able to replace `local_authdb` implementation\nin default configuration with `auth.pam`:\n```\nauth.pam local_authdb {\n    use_helper no\n}\n```\n\n## Helper executable\n\nTL;DR\n```\ngit clone https://github.com/foxcpp/maddy\ncd maddy/cmd/maddy-pam-helper\ngcc pam.c main.c -lpam -o maddy-pam-helper\n```\n\nCopy the resulting executable into /usr/lib/maddy/ and make\nit setuid-root so it can read /etc/shadow (if that's necessary):\n```\nchown root:maddy /usr/lib/maddy/maddy-pam-helper\nchmod u+xs,g+x,o-x /usr/lib/maddy/maddy-pam-helper\n```\n\nThen you should be able to replace `local_authdb` implementation\nin default configuration with `auth.pam`:\n```\nauth.pam local_authdb {\n    use_helper yes\n}\n```\n\n## Account names\n\nSince PAM does not use emails for authentication you should configure\nmaddy to either strip domain part when checking credentials or do not\nuse email when authenticating.\n\nSee [Multiple domains configuration](/multiple-domains) for how to configure\nauthentication.\n\n## PAM service\n\nYou should create a PAM configuration file for maddy to use.\nPlace it into /etc/pam.d/maddy.\nHere is the minimal example using pam_unix (shadow database).\n```\n#%PAM-1.0\nauth\trequired\tpam_unix.so\naccount\trequired\tpam_unix.so\n```\n\nHere is the configuration example you could use on Ubuntu\nto use the authentication config system itself uses:\n```\n#%PAM-1.0\n\n@include common-auth\n@include common-account\n@include common-session\n```\n"
  },
  {
    "path": "docs/tutorials/setting-up.md",
    "content": "# Installation & initial configuration\n\nThis is the practical guide on how to set up a mail server using maddy for\npersonal use. It omits most of the technical details for brevity and just gives\nyou the minimal list of things you need to be aware of and what to do to make\nstuff work.\n\nFor purposes of clarity, these values are used in this tutorial as examples,\nwherever you see them, you need to replace them with your actual values:\n\n- Domain: example.org\n- MX domain (hostname): mx1.example.org\n- IPv4 address: 10.2.3.4\n- IPv6 address: 2001:beef::1\n\n## Getting a server\n\nWhere to get a server to run maddy on is out of the scope of this article. Any\nVPS (virtual private server) will work fine for small configurations. However,\nthere are a few things to keep in mind:\n\n- Make sure your provider does not block SMTP traffic (25 TCP port). Most VPS\n  providers don't do it, but some \"cloud\" providers (such as Google Cloud) do\n  it, so you can't host your mail there.\n\n- It is recommended to run your own DNS resolver with DNSSEC verification\n  enabled.\n\n## Installing maddy\n\nYour options are:\n\n* Pre-built tarball (Linux, amd64)\n\n    Available on [GitHub](https://github.com/foxcpp/maddy/releases) or\n    [maddy.email/builds](https://maddy.email/builds/).\n\n\tThe tarball includes maddy executable you can\n\tcopy into /usr/local/bin as well as systemd unit file you can\n\tuse on systemd-based distributions for automatic startup and service\n\tsupervision. You should also create \"maddy\" user and group.\n\tSee below for more detailed instructions.\n\n* Docker image (Linux, amd64)\n\n    ```\n    docker pull foxcpp/maddy:0.6\n    ```\n\n    See [here](../../docker) for Docker-specific instructions.\n\n* Building from source\n\n    See [here](../building-from-source) for instructions.\n\n* Arch Linux packages\n\n\tFor Arch Linux users, `maddy` and `maddy-git` PKGBUILDs are available\n\tin AUR. Additionally, binary packages are available in 3rd-party\n\trepository at [https://maddy.email/archlinux/](https://maddy.email/archlinux/)\n\n## System configuration (systemd-based distribution)\n\nIf you built maddy from source and used `./build.sh install` then\nsystemd unit files should be already installed. If you used\na pre-built tarball - copy `systemd/*.service` to `/etc/systemd/system`\nmanually.\n\nYou need to reload service manager configuration to make service available:\n\n```\nsystemctl daemon-reload\n```\n\nAdditionally, you should create maddy user and group. Unlike most other\nLinux mail servers, maddy never runs as root.\n\n```\nuseradd -mrU -s /sbin/nologin -d /var/lib/maddy -c \"maddy mail server\" maddy\n```\n\n## Host name + domain\n\nOpen /etc/maddy/maddy.conf with vim^W your favorite editor and change\nthe following lines to match your server name and domain you want to handle\nmail for.\nIf you setup a very small mail server you can use example.org in both fields.\nHowever, to easier a future migration of service, it's recommended to use a\nseparate DNS entry for that purpose. It's usually mx1.example.org, mx2, etc.\nYou can of course use another subdomain, for instance: smtp1.example.org.\nAn email failover server will become possible if you forward mx2.example.org\nto another server (as long as you configure it to handle your domain).\n\n```\n$(hostname) = mx1.example.org\n$(primary_domain) = example.org\n```\n\nIf you want to handle multiple domains, you still need to designate\none as \"primary\". Add all other domains to the `local_domains` line:\n\n```\n$(local_domains) = $(primary_domain) example.com other.example.com\n```\n\nDo not forget to set a suitable rDNS (PTR) record for your server's IP address\nto reduce the chances of outgoing mails getting marked as spam or being\ndownright rejected. Ideally, the PTR record should match whatever you specified\nin `$(hostname)`.\n\n## TLS certificates\n\nOne thing that can't be automatically configured is TLS certs. If you already\nhave them somewhere - use them, open /etc/maddy/maddy.conf and put the right\npaths in. You need to make sure maddy can read them while running as\nunprivileged user (maddy never runs as root, even during start-up), one way to\ndo so is to use ACLs (replace with your actual paths):\n```\n$ sudo setfacl -R -m u:maddy:rX /etc/ssl/mx1.example.org.crt /etc/ssl/mx1.example.org.key\n```\n\nmaddy reloads TLS certificates from disk once in a minute so it will notice\nrenewal. It is possible to force reload via `systemctl reload maddy` (or just\n`killall -USR2 maddy`).\n\n### Let's Encrypt and certbot\n\nIf you use certbot to manage your certificates, you can simply symlink\n/etc/maddy/certs into /etc/letsencrypt/live. maddy will pick the right\ncertificate depending on the domain you specified during installation.\n\nYou still need to make keys readable for maddy, though:\n```\n$ sudo setfacl -R -m u:maddy:rX /etc/letsencrypt/{live,archive}\n```\n\n### ACME.sh\n\nIf you use acme.sh to manage your certificates, you could simply run:\n\n```\nmkdir -p /etc/maddy/certs/mx1.example.org\nacme.sh --force --install-cert -d mx1.example.org \\\n  --key-file       /etc/maddy/certs/mx1.example.org/privkey.pem  \\\n  --fullchain-file /etc/maddy/certs/mx1.example.org/fullchain.pem\n```\n\n## First run\n\n```\nsystemctl start maddy\n```\n\nThe daemon should be running now, except that it is useless because we haven't\nconfigured DNS records.\n\n## DNS records\n\nHow it is configured depends on your DNS provider (or server, if you run your\nown). Here is how your DNS zone should look like:\n```\n; Basic domain->IP records, you probably already have them.\nexample.org.   A     10.2.3.4\nexample.org.   AAAA  2001:beef::1\n\n; It says that \"server mx1.example.org is handling messages for example.org\".\nexample.org.   MX    10 mx1.example.org.\n; Of course, mx1 should have A/AAAA entry as well:\nmx1.example.org.   A     10.2.3.4\nmx1.example.org.   AAAA  2001:beef::1\n\n; Use SPF to say that the servers in \"MX\" above are allowed to send email\n; for this domain, and nobody else.\nexample.org.     TXT   \"v=spf1 mx ~all\"\n; It is recommended to server SPF record for both domain and MX hostname\nmx1.example.org. TXT   \"v=spf1 a ~all\"\n\n; Opt-in into DMARC with permissive policy and request reports about broken\n; messages.\n_dmarc.example.org.   TXT    \"v=DMARC1; p=quarantine; ruf=mailto:postmaster@example.org\"\n\n; Mark domain as MTA-STS compatible (see the next section)\n; and request reports about failures to be sent to postmaster@example.org\n_mta-sts.example.org.   TXT    \"v=STSv1; id=1\"\n_smtp._tls.example.org. TXT    \"v=TLSRPTv1;rua=mailto:postmaster@example.org\"\n```\n\nAnd the last one, DKIM key, is a bit tricky. maddy generated a key for you on\nthe first start-up. You can find it in\n/var/lib/maddy/dkim_keys/example.org_default.dns. You need to put it in a TXT\nrecord for `default._domainkey.example.org.` domain, like that:\n```\ndefault._domainkey.example.org.    TXT   \"v=DKIM1; k=ed25519; p=nAcUUozPlhc4VPhp7hZl+owES7j7OlEv0laaDEDBAqg=\"\n```\n\n## MTA-STS and DANE\n\nBy default SMTP is not protected against active attacks. MTA-STS policy tells\ncompatible senders to always use properly authenticated TLS when talking to\nyour server, offering a simple-to-deploy way to protect your server against\nMitM attacks on port 25.\n\nBasically, you to create a file with following contents and make it available\nat https://mta-sts.example.org/.well-known/mta-sts.txt:\n```\nversion: STSv1\nmode: enforce\nmax_age: 604800\nmx: mx1.example.org\n```\n\n**Note**: mx1.example.org in the file is your MX hostname, In a simple configuration,\nit will be the same as your hostname example.org.\nIn a more complex setups, you would have multiple MX servers - add them all once\nper line, like that:\n\n```\nmx: mx1.example.org\nmx: mx2.example.org\n```\n\nIt is also recommended to set a TLSA (DANE) record.\nUse https://www.huque.com/bin/gen_tlsa to generate one.\nSet port to 25, Transport Protocol to \"tcp\" and Domain Name to **the MX hostname**.\nExample of a valid record:\n```\n_25._tcp.mx1.example.org. TLSA 3 1 1 7f59d873a70e224b184c95a4eb54caa9621e47d48b4a25d312d83d96e3498238\n```\n\n## User accounts and maddy command\n\nA mail server is useless without mailboxes, right? Unlike software like postfix\nand dovecot, maddy uses \"virtual users\" by default, meaning it does not care or\nknow about system users.\n\nIMAP mailboxes (\"accounts\") and authentication credentials are kept separate.\n\nTo register user credentials, use `maddy creds create` command.\nLike that:\n```\n$ maddy creds create postmaster@example.org\n```\n\nNote the username is a e-mail address. This is required as username is used to\nauthorize IMAP and SMTP access (unless you configure custom mappings, not\ndescribed here).\n\nAfter registering the user credentials, you also need to create a local\nstorage account:\n```\n$ maddy imap-acct create postmaster@example.org\n```\n\nNote: to run `maddy` CLI commands, your user should be in the `maddy`\ngroup. Alternatively, just use `sudo -u maddy`.\n\nThat is it. Now you have your first e-mail address. when authenticating using\nyour e-mail client, do not forget the username is \"postmaster@example.org\", not\njust \"postmaster\".\n\nYou may find running `maddy creds --help` and `maddy imap-acct --help`\nuseful to learn about other commands. Note that IMAP accounts and credentials\nare managed separately yet usernames should match by default for things to\nwork.\n"
  },
  {
    "path": "docs/upgrading.md",
    "content": "# Upgrading from older maddy versions\n\nIt is generally possible to just install latest version (e.g. using build.sh\nscript) over the existing installation.\n\nIt is recommended to backup state directory (usually /var/lib/maddy for Linux)\nbefore doing so. The new server version may automatically convert DB files in a\nway that will make them unreadable by older versions.\n\nSpecific instructions for upgrading between versions with incompatible changes\nare documented on this page below.\n\n## Incompatible version migration\n\n## 0.2 -> 0.3\n\n0.3 includes a significant change to the authentication code that makes it\ncompletely independent of IMAP index. This means 0.2 \"unified\" database cannot\nbe used in 0.3 and auto-migration is not possible. Additionally, the way\npasswords are hashed is changed, meaning that after migration passwords will\nneed to be reset.\n\n**Migration utility is SQLite-specific, if you need one that works for\nPostgres - reach out at the IRC channel.**\n\n1. Make sure the server is not running.\n\n```\nsystemctl stop maddy\n```\n\n2. Take a backup of `imapsql.db*` files in state directory (/var/lib/maddy).\n\n```\nmkdir backup\ncp /var/lib/maddy/imapsql.db* backup/\n```\n\n3. Compile migration utility:\n\n```\ngit clone https://github.com/foxcpp/maddy.git\ncd maddy/\ngit checkout v0.3.0\ncd cmd/migrate-db-0.2\ngo build\n```\n\n4. Run compiled binary:\n\n```\n./migrate-db-0.2 /var/lib/maddy/imapsql.db\n```\n\n5. Open maddy.conf and make following changes:\n\nRemove `local_authdb` name from imapsql configuration block:\n```\nimapsql local_mailboxes {\n    driver sqlite3\n    dsn imapsql.db\n}\n```\n\nAdd `local_authdb` configuration block using `pass_table` module:\n\n```\npass_table local_authdb {\n    table sql_table {\n        driver sqlite3\n        dsn credentials.db\n        table_name passwords\n    }\n}\n```\n\n6. Use `maddy creds create ACCOUNT_NAME` to add credentials to `pass_table`\n   store.\n\n7. Start the server back.\n\n```\nsystemctl start maddy\n```\n\n## 0.1 -> 0.2\n\n0.2 requires several changes in configuration file.\n\nChange\n```\nsql local_mailboxes local_authdb {\n```\nto\n```\nimapsql local_mailboxes local_authdb {\n```\n\nReplace\n```\nreplace_rcpt postmaster postmaster@$(primary_domain)\n```\nwith\n```\nreplace_rcpt static {\n    entry postmaster postmaster@$(primary_domain)\n}\n```\nand\n\n```\nreplace_rcpt \"(.+)\\+(.+)@(.+)\" \"$1@$3\"\n```\nwith\n```\nreplace_rcpt regexp \"(.+)\\+(.+)@(.+)\" \"$1@$3\"\n```\n"
  },
  {
    "path": "framework/address/doc.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\n// Package address provides utilities for parsing\n// and validation of RFC 2821 addresses.\npackage address\n"
  },
  {
    "path": "framework/address/norm.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage address\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"unicode/utf8\"\n\n\t\"github.com/foxcpp/maddy/framework/dns\"\n\t\"golang.org/x/net/idna\"\n\t\"golang.org/x/text/secure/precis\"\n\t\"golang.org/x/text/unicode/norm\"\n)\n\n// ForLookup transforms the local-part of the address into a canonical form\n// usable for map lookups or direct comparisons.\n//\n// If Equal(addr1, addr2) == true, then ForLookup(addr1) == ForLookup(addr2).\n//\n// On error, case-folded addr is also returned.\nfunc ForLookup(addr string) (string, error) {\n\tif addr == \"\" { // Null return-path case.\n\t\treturn \"\", nil\n\t}\n\n\tmbox, domain, err := Split(addr)\n\tif err != nil {\n\t\treturn strings.ToLower(addr), err\n\t}\n\n\tif domain != \"\" {\n\t\tdomain, err = dns.ForLookup(domain)\n\t\tif err != nil {\n\t\t\treturn strings.ToLower(addr), err\n\t\t}\n\t}\n\n\tmbox = strings.ToLower(norm.NFC.String(mbox))\n\n\tif domain == \"\" {\n\t\treturn mbox, nil\n\t}\n\n\treturn mbox + \"@\" + domain, nil\n}\n\n// CleanDomain returns the address with the domain part converted into its canonical form.\n//\n// More specifically, converts the domain part of the address to U-labels,\n// normalizes it to NFC and then case-folds it.\n//\n// Original value is also returned on the error.\nfunc CleanDomain(addr string) (string, error) {\n\tif addr == \"\" { // Null return-path\n\t\treturn \"\", nil\n\t}\n\n\tmbox, domain, err := Split(addr)\n\tif err != nil {\n\t\treturn addr, err\n\t}\n\n\tuDomain, err := idna.ToUnicode(domain)\n\tif err != nil {\n\t\treturn addr, err\n\t}\n\tuDomain = strings.ToLower(norm.NFC.String(uDomain))\n\n\tif domain == \"\" {\n\t\treturn mbox, nil\n\t}\n\n\treturn mbox + \"@\" + uDomain, nil\n}\n\n// Equal reports whether addr1 and addr2 are considered to be\n// case-insensitively equivalent.\n//\n// The equivalence is defined to be the conjunction of IDN label equivalence\n// for the domain part and canonical equivalence* of the local-part converted\n// to lower case.\n//\n// * IDN label equivalence is defined by RFC 5890 Section 2.3.2.4.\n// ** Canonical equivalence is defined by UAX #15.\n//\n// Equivalence for malformed addresses is defined using regular byte-string\n// comparison with case-folding applied.\nfunc Equal(addr1, addr2 string) bool {\n\t// Short circuit. If they are bit-equivalent, then they are also canonically\n\t// equivalent.\n\tif addr1 == addr2 {\n\t\treturn true\n\t}\n\n\tuAddr1, _ := ForLookup(addr1)\n\tuAddr2, _ := ForLookup(addr2)\n\treturn uAddr1 == uAddr2\n}\n\nfunc IsASCII(s string) bool {\n\tfor _, ch := range s {\n\t\tif ch > utf8.RuneSelf {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc FQDNDomain(addr string) string {\n\tif strings.HasSuffix(addr, \".\") {\n\t\treturn addr\n\t}\n\treturn addr + \".\"\n}\n\n// PRECISFold applies UsernameCaseMapped to the local part and dns.ForLookup\n// to domain part of the address.\nfunc PRECISFold(addr string) (string, error) {\n\treturn precisEmail(addr, precis.UsernameCaseMapped)\n}\n\n// PRECIS applies UsernameCasePreserved to the local part and dns.ForLookup\n// to domain part of the address.\nfunc PRECIS(addr string) (string, error) {\n\treturn precisEmail(addr, precis.UsernameCasePreserved)\n}\n\nfunc precisEmail(addr string, profile *precis.Profile) (string, error) {\n\tmbox, domain, err := Split(addr)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"address: precis: %w\", err)\n\t}\n\n\t// PRECISFold is not included in the regular address.ForLookup since it reduces\n\t// the range of valid addresses to a subset of actually valid values.\n\t// PRECISFold is a matter of our own local policy, not a general rule for all\n\t// email addresses.\n\n\t// Side note: For used profiles, there is no practical difference between\n\t// CompareKey and String.\n\tmbox, err = profile.CompareKey(mbox)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"address: precis: %w\", err)\n\t}\n\n\tdomain, err = dns.ForLookup(domain)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"address: precis: %w\", err)\n\t}\n\n\treturn mbox + \"@\" + domain, nil\n}\n"
  },
  {
    "path": "framework/address/norm_test.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage address\n\nimport (\n\t\"testing\"\n)\n\nfunc addrFuncTest(t *testing.T, f func(string) (string, error)) func(in, wantOut string, fail bool) {\n\treturn func(in, wantOut string, fail bool) {\n\t\tt.Helper()\n\n\t\tout, err := f(in)\n\t\tif err != nil {\n\t\t\tif !fail {\n\t\t\t\tt.Errorf(\"Expected failure, got none\")\n\t\t\t}\n\t\t}\n\t\tif out != wantOut {\n\t\t\tt.Errorf(\"Wrong result: want '%s', got '%s'\", wantOut, out)\n\t\t}\n\t}\n}\n\nfunc TestForLookup(t *testing.T) {\n\ttest := addrFuncTest(t, ForLookup)\n\ttest(\"test@example.org\", \"test@example.org\", false)\n\ttest(\"E\\u0301@example.org\", \"\\u00E9@example.org\", false)\n\ttest(\"test@EXAMPLE.org\", \"test@example.org\", false)\n\ttest(\"test@xn--e1aybc.example.org\", \"test@тест.example.org\", false)\n\ttest(\"TEST@xn--99999999999.example.org\", \"test@xn--99999999999.example.org\", true)\n\ttest(\"tESt@\", \"test@\", true)\n\ttest(\"postmaster\", \"postmaster\", false)\n}\n\nfunc TestCleanDomain(t *testing.T) {\n\ttest := addrFuncTest(t, CleanDomain)\n\ttest(\"test@example.org\", \"test@example.org\", false)\n\ttest(\"whateveR@example.org\", \"whateveR@example.org\", false)\n\ttest(\"E\\u0301@example.org\", \"E\\u0301@example.org\", false)\n\ttest(\"test@EXAMPLE.org\", \"test@example.org\", false)\n\ttest(\"test@xn--e1aybc.example.org\", \"test@тест.example.org\", false)\n\ttest(\"TEST@xn--99999999999.example.org\", \"TEST@xn--99999999999.example.org\", true)\n\ttest(\"tESt@\", \"tESt@\", true)\n\ttest(\"postmaster\", \"postmaster\", false)\n}\n\nfunc TestEqual(t *testing.T) {\n\ttest := func(in1, in2 string, wantEq bool) {\n\t\teq := Equal(in1, in2)\n\t\tif eq != wantEq {\n\t\t\tt.Errorf(\"Want Equal(%s, %s) == %v, got %v\", in1, in2, wantEq, eq)\n\t\t}\n\t}\n\n\ttest(\"test@example.org\", \"test@example.org\", true)\n\ttest(\"test2@example.org\", \"test@example.org\", false)\n\ttest(\"TEST2@example.org\", \"TesT2@example.org\", true)\n\ttest(\"E\\u0301@example.org\", \"\\u00E9@example.org\", true)\n\ttest(\"test@тест.example.org\", \"test@xn--e1aybc.example.org\", true)\n\ttest(\"test@xn--999999999999999.example.org\", \"test@xn--999999999999999.example.org\", true)\n\ttest(\"test@xn--999999999999.example.org\", \"test@xn--999999999999999.example.org\", false)\n}\n\nfunc TestIsASCII(t *testing.T) {\n\tif !IsASCII(\"hello\") {\n\t\tt.Errorf(\"'hello' is ASCII\")\n\t}\n\tif IsASCII(\"тест\") {\n\t\tt.Errorf(\"'тест' is non-ASCII\")\n\t}\n}\n"
  },
  {
    "path": "framework/address/rfc6531.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage address\n\nimport (\n\t\"errors\"\n\n\t\"golang.org/x/net/idna\"\n\t\"golang.org/x/text/unicode/norm\"\n)\n\nvar ErrUnicodeMailbox = errors.New(\"address: cannot convert the Unicode local-part to the ACE form\")\n\n// ToASCII converts the domain part of the email address to the A-label form and\n// fails with ErrUnicodeMailbox if the local-part contains non-ASCII characters.\nfunc ToASCII(addr string) (string, error) {\n\tmbox, domain, err := Split(addr)\n\tif err != nil {\n\t\treturn addr, err\n\t}\n\n\tfor _, ch := range mbox {\n\t\tif ch > 128 {\n\t\t\treturn addr, ErrUnicodeMailbox\n\t\t}\n\t}\n\n\tif domain == \"\" {\n\t\treturn mbox, nil\n\t}\n\n\taDomain, err := idna.ToASCII(domain)\n\tif err != nil {\n\t\treturn addr, err\n\t}\n\n\treturn mbox + \"@\" + aDomain, nil\n}\n\n// ToUnicode converts the domain part of the email address to the U-label form.\nfunc ToUnicode(addr string) (string, error) {\n\tmbox, domain, err := Split(addr)\n\tif err != nil {\n\t\treturn norm.NFC.String(addr), err\n\t}\n\n\tif domain == \"\" {\n\t\treturn mbox, nil\n\t}\n\n\tuDomain, err := idna.ToUnicode(domain)\n\tif err != nil {\n\t\treturn norm.NFC.String(addr), err\n\t}\n\n\treturn mbox + \"@\" + norm.NFC.String(uDomain), nil\n}\n\n// SelectIDNA is a convenience function for conversion of domains in the email\n// addresses to/from the Punycode form.\n//\n// ulabel=true => ToUnicode is used.\n// ulabel=false => ToASCII is used.\nfunc SelectIDNA(ulabel bool, addr string) (string, error) {\n\tif ulabel {\n\t\treturn ToUnicode(addr)\n\t}\n\treturn ToASCII(addr)\n}\n"
  },
  {
    "path": "framework/address/rfc6531_test.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage address\n\nimport (\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestToASCII(t *testing.T) {\n\ttest := addrFuncTest(t, ToASCII)\n\ttest(\"test@тест.example.org\", \"test@xn--e1aybc.example.org\", false)\n\ttest(\"test@org.\"+strings.Repeat(\"x\", 65535)+\"\\uFF00\", \"test@org.\"+strings.Repeat(\"x\", 65535)+\"\\uFF00\", true)\n\ttest(\"тест@example.org\", \"тест@example.org\", true)\n\ttest(\"postmaster\", \"postmaster\", false)\n\ttest(\"postmaster@\", \"postmaster@\", true)\n}\n\nfunc TestToUnicode(t *testing.T) {\n\ttest := addrFuncTest(t, ToUnicode)\n\ttest(\"test@xn--e1aybc.example.org\", \"test@тест.example.org\", false)\n\ttest(\"test@xn--9999999999999999999a.org\", \"test@xn--9999999999999999999a.org\", true)\n\ttest(\"postmaster\", \"postmaster\", false)\n\ttest(\"postmaster@\", \"postmaster@\", true)\n}\n"
  },
  {
    "path": "framework/address/split.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage address\n\nimport (\n\t\"errors\"\n\t\"strings\"\n)\n\n// Split splits a email address (as defined by RFC 5321 as a forward-path\n// token) into local part (mailbox) and domain.\n//\n// Note that definition of the forward-path token includes the special\n// postmaster address without the domain part. Split will return domain == \"\"\n// in this case.\n//\n// Split does almost no sanity checks on the input and is intentionally naive.\n// If this is a concern, ValidMailbox and ValidDomain should be used on the\n// output.\nfunc Split(addr string) (mailbox, domain string, err error) {\n\tif strings.EqualFold(addr, \"postmaster\") {\n\t\treturn addr, \"\", nil\n\t}\n\n\tindx := strings.LastIndexByte(addr, '@')\n\tif indx == -1 {\n\t\treturn \"\", \"\", errors.New(\"address: missing at-sign\")\n\t}\n\tmailbox = addr[:indx]\n\tdomain = addr[indx+1:]\n\tif mailbox == \"\" {\n\t\treturn \"\", \"\", errors.New(\"address: empty local-part\")\n\t}\n\tif domain == \"\" {\n\t\treturn \"\", \"\", errors.New(\"address: empty domain\")\n\t}\n\treturn\n}\n\n// UnquoteMbox undoes escaping and quoting of the local-part.  That is, for\n// local-part `\"test\\\" @ test\"` it will return `test\" @test`.\nfunc UnquoteMbox(mbox string) (string, error) {\n\tvar (\n\t\tquoted          bool\n\t\tescaped         bool\n\t\tterminatedQuote bool\n\t\tmailboxB        strings.Builder\n\t)\n\tfor _, ch := range mbox {\n\t\tif terminatedQuote {\n\t\t\treturn \"\", errors.New(\"address: closing quote should be right before at-sign\")\n\t\t}\n\n\t\tswitch ch {\n\t\tcase '\"':\n\t\t\tif !escaped {\n\t\t\t\tquoted = !quoted\n\t\t\t\tif !quoted {\n\t\t\t\t\tterminatedQuote = true\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\t\tcase '\\\\':\n\t\t\tif !escaped {\n\t\t\t\tif !quoted {\n\t\t\t\t\treturn \"\", errors.New(\"address: escapes are allowed only in quoted strings\")\n\t\t\t\t}\n\t\t\t\tescaped = true\n\t\t\t\tcontinue\n\t\t\t}\n\t\tcase '@':\n\t\t\tif !quoted {\n\t\t\t\treturn \"\", errors.New(\"address: extra at-sign in non-quoted local-part\")\n\t\t\t}\n\t\t}\n\n\t\tescaped = false\n\n\t\tmailboxB.WriteRune(ch)\n\t}\n\n\tif mailboxB.Len() == 0 {\n\t\treturn \"\", errors.New(\"address: empty local part\")\n\t}\n\n\treturn mailboxB.String(), nil\n}\n\n// \"specials\" from RFC5322 grammar with dot removed (it is defined in grammar separately, for some reason)\nvar mboxSpecial = map[rune]struct{}{\n\t'(': {}, ')': {}, '<': {}, '>': {},\n\t'[': {}, ']': {}, ':': {}, ';': {},\n\t'@': {}, '\\\\': {}, ',': {},\n\t'\"': {}, ' ': {},\n}\n\nfunc QuoteMbox(mbox string) string {\n\tvar mailboxEsc strings.Builder\n\tmailboxEsc.Grow(len(mbox))\n\tquoted := false\n\tfor _, ch := range mbox {\n\t\tif _, ok := mboxSpecial[ch]; ok {\n\t\t\tif ch == '\\\\' || ch == '\"' {\n\t\t\t\tmailboxEsc.WriteRune('\\\\')\n\t\t\t}\n\t\t\tmailboxEsc.WriteRune(ch)\n\t\t\tquoted = true\n\t\t} else {\n\t\t\tmailboxEsc.WriteRune(ch)\n\t\t}\n\t}\n\tif quoted {\n\t\treturn `\"` + mailboxEsc.String() + `\"`\n\t}\n\treturn mbox\n}\n"
  },
  {
    "path": "framework/address/split_test.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage address\n\nimport (\n\t\"testing\"\n)\n\nfunc TestSplit(t *testing.T) {\n\ttest := func(addr, mbox, domain string, fail bool) {\n\t\tt.Helper()\n\n\t\tactualMbox, actualDomain, err := Split(addr)\n\t\tif err != nil && !fail {\n\t\t\tt.Errorf(\"%s: unexpected error: %v\", addr, err)\n\t\t\treturn\n\t\t}\n\t\tif err == nil && fail {\n\t\t\tt.Errorf(\"%s: expected error, got %s, %s\", addr, actualMbox, actualDomain)\n\t\t\treturn\n\t\t}\n\n\t\tif actualMbox != mbox {\n\t\t\tt.Errorf(\"%s: wrong local part, want %s, got %s\", addr, mbox, actualMbox)\n\t\t}\n\t\tif actualDomain != domain {\n\t\t\tt.Errorf(\"%s: wrong domain part, want %s, got %s\", addr, domain, actualDomain)\n\t\t}\n\t}\n\n\ttest(\"simple@example.org\", \"simple\", \"example.org\", false)\n\ttest(\"simple@[1.2.3.4]\", \"simple\", \"[1.2.3.4]\", false)\n\ttest(\"simple@[IPv6:beef::1]\", \"simple\", \"[IPv6:beef::1]\", false)\n\ttest(\"@example.org\", \"\", \"\", true)\n\ttest(\"@\", \"\", \"\", true)\n\ttest(\"no-domain@\", \"\", \"\", true)\n\ttest(\"@no-local-part\", \"\", \"\", true)\n\n\t// Not a valid address, but a special value for SMTP\n\t// should be handled separately where necessary.\n\ttest(\"\", \"\", \"\", true)\n\n\t// A special SMTP value too, but permitted now.\n\ttest(\"postmaster\", \"postmaster\", \"\", false)\n}\n\nfunc TestUnquoteMbox(t *testing.T) {\n\ttest := func(inputMbox, expectedMbox string, fail bool) {\n\t\tt.Helper()\n\n\t\tactualMbox, err := UnquoteMbox(inputMbox)\n\t\tif err != nil && !fail {\n\t\t\tt.Errorf(\"unexpected error: %v\", err)\n\t\t\treturn\n\t\t}\n\t\tif err == nil && fail {\n\t\t\tt.Errorf(\"expected error, got %s\", actualMbox)\n\t\t\treturn\n\t\t}\n\n\t\tif actualMbox != expectedMbox {\n\t\t\tt.Errorf(\"wrong local part, want %s, got %s\", actualMbox, actualMbox)\n\t\t}\n\t}\n\n\ttest(`no\\@no`, \"\", true)\n\ttest(\"no@no\", \"\", true)\n\ttest(`no\\\"no`, \"\", true)\n\ttest(`\"no\\\"no\"`, `no\"no`, false)\n\ttest(`\"no@no\"`, `no@no`, false)\n\ttest(`\"no no\"`, `no no`, false)\n\ttest(`\"no\\\\no\"`, `no\\no`, false)\n\ttest(`\"no\"no`, \"\", true)\n\ttest(`postmaster`, \"postmaster\", false)\n\ttest(`foo`, \"foo\", false)\n}\n\nfunc TestQuoteMbox(t *testing.T) {\n\ttest := func(inputMbox, expectedMbox string) {\n\t\tt.Helper()\n\n\t\tactualMbox := QuoteMbox(inputMbox)\n\t\tif actualMbox != expectedMbox {\n\t\t\tt.Errorf(\"wrong local part, want %s, got %s\", actualMbox, actualMbox)\n\t\t}\n\t}\n\n\ttest(`no\"no`, `\"no\\\"no\"`)\n\ttest(`no@no`, `\"no@no\"`)\n\ttest(`no no`, `\"no no\"`)\n\ttest(`no\\no`, `\"no\\\\no\"`)\n\ttest(\"postmaster\", `postmaster`)\n\ttest(\"foo\", `foo`)\n}\n"
  },
  {
    "path": "framework/address/validation.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage address\n\nimport (\n\t\"strings\"\n\n\t\"golang.org/x/net/idna\"\n)\n\n/*\nRules for validation are subset of rules listed here:\nhttps://emailregex.com/email-validation-summary/\n*/\n\n// Valid checks whether ths string is valid as a email address as defined by\n// RFC 5321.\nfunc Valid(addr string) bool {\n\tif len(addr) > 320 { // RFC 3696 says it's 320, not 255.\n\t\treturn false\n\t}\n\n\tmbox, domain, err := Split(addr)\n\tif err != nil {\n\t\treturn false\n\t}\n\n\t// The only case where this can be true is \"postmaster\".\n\t// So allow it.\n\tif domain == \"\" {\n\t\treturn true\n\t}\n\n\treturn ValidMailboxName(mbox) && ValidDomain(domain)\n}\n\nvar validGraphic = map[rune]bool{\n\t'!': true, '#': true,\n\t'$': true, '%': true,\n\t'&': true, '\\'': true,\n\t'*': true, '+': true,\n\t'-': true, '/': true,\n\t'=': true, '?': true,\n\t'^': true, '_': true,\n\t'`': true, '{': true,\n\t'|': true, '}': true,\n\t'~': true, '.': true,\n}\n\n// ValidMailboxName checks whether the specified string is a valid mailbox-name\n// element of e-mail address (left part of it, before at-sign).\nfunc ValidMailboxName(mbox string) bool {\n\tif strings.HasPrefix(mbox, `\"`) {\n\t\traw, err := UnquoteMbox(mbox)\n\t\tif err != nil {\n\t\t\treturn false\n\t\t}\n\n\t\t// Inside quotes, any ASCII graphic and space is allowed.\n\t\t// Additionally, RFC 6531 extends that to allow any Unicode (UTF-8).\n\t\tfor _, ch := range raw {\n\t\t\tif ch < ' ' || ch == 0x7F /* DEL */ {\n\t\t\t\t// ASCII control characters.\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t\treturn true\n\t}\n\n\t// Without quotes, limited set of ASCII graphics is allowed + ASCII\n\t// alphanumeric characters.\n\t// RFC 6531 extends that to allow any Unicode (UTF-8).\n\tfor _, ch := range mbox {\n\t\tif validGraphic[ch] {\n\t\t\tcontinue\n\t\t}\n\t\tif ch >= '0' && ch <= '9' {\n\t\t\tcontinue\n\t\t}\n\t\tif ch >= 'A' && ch <= 'Z' {\n\t\t\tcontinue\n\t\t}\n\t\tif ch >= 'a' && ch <= 'z' {\n\t\t\tcontinue\n\t\t}\n\t\tif ch > 0x7F { // Unicode\n\t\t\tcontinue\n\t\t}\n\n\t\treturn false\n\t}\n\n\treturn true\n}\n\n// ValidDomain checks whether the specified string is a valid DNS domain.\nfunc ValidDomain(domain string) bool {\n\tif len(domain) > 255 || len(domain) == 0 {\n\t\treturn false\n\t}\n\tif strings.HasPrefix(domain, \".\") {\n\t\treturn false\n\t}\n\tif strings.Contains(domain, \"..\") {\n\t\treturn false\n\t}\n\n\t// Length checks are to be applied to A-labels form.\n\t// maddy uses U-labels representation across the code (for lookups, etc).\n\tdomainASCII, err := idna.ToASCII(domain)\n\tif err != nil {\n\t\treturn false\n\t}\n\tlabels := strings.Split(domainASCII, \".\")\n\tfor _, label := range labels {\n\t\tif len(label) > 64 {\n\t\t\treturn false\n\t\t}\n\t}\n\n\treturn true\n}\n"
  },
  {
    "path": "framework/address/validation_test.go",
    "content": "package address_test\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/foxcpp/maddy/framework/address\"\n)\n\nfunc TestValidMailboxName(t *testing.T) {\n\tif !address.ValidMailboxName(\"caddy.bug\") {\n\t\tt.Error(\"caddy.bug should be valid mailbox name\")\n\t}\n}\n\nfunc TestValidDomain(t *testing.T) {\n\tfor _, c := range []struct {\n\t\tDomain string\n\t\tValid  bool\n\t}{\n\t\t{Domain: \"maddy.email\", Valid: true},\n\t\t{Domain: \"\", Valid: false},\n\t\t{Domain: \"maddy.email.\", Valid: true},\n\t\t{Domain: \"..\", Valid: false},\n\t\t{Domain: strings.Repeat(\"a\", 256), Valid: false},\n\t\t{Domain: \"äõäoaõoäaõaäõaoäaoaäõoaäooaoaoiuaiauäõiuüõaõäiauõaaa.tld\", Valid: true},            // https://github.com/foxcpp/maddy/issues/554\n\t\t{Domain: \"xn--oaoaaaoaoaoaooaoaoiuaiauiuaiauaaa-f1cadccdcmd01eddchqcbe07a.tld\", Valid: true}, // https://github.com/foxcpp/maddy/issues/554\n\t} {\n\t\tif actual := address.ValidDomain(c.Domain); actual != c.Valid {\n\t\t\tt.Errorf(\"expected domain %v to be valid=%v, but got %v\", c.Domain, c.Valid, actual)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "framework/buffer/buffer.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\n// The buffer package provides utilities for temporary storage (buffering)\n// of large blobs.\npackage buffer\n\nimport (\n\t\"io\"\n)\n\n// Buffer interface represents abstract temporary storage for blobs.\n//\n// The Buffer storage is assumed to be immutable. If any modifications\n// are made - new storage location should be used for them.\n// This is important to ensure goroutine-safety.\n//\n// Since Buffer objects require a careful management of lifetimes, here\n// is the convention: Its always creator responsibility to call Remove after\n// Buffer is no longer used. If Buffer object is passed to a function - it is not\n// guaranteed to be valid after this function returns. If function needs to preserve\n// the storage contents, it should \"re-buffer\" it either by reading entire blob\n// and storing it somewhere or applying implementation-specific methods (for example,\n// the FileBuffer storage may be \"re-buffered\" by hard-linking the underlying file).\ntype Buffer interface {\n\t// Open creates new Reader reading from the underlying storage.\n\tOpen() (io.ReadCloser, error)\n\n\t// Len reports the length of the stored blob.\n\t//\n\t// Notably, it indicates the amount of bytes that can be read from the\n\t// newly created Reader without hiting io.EOF.\n\tLen() int\n\n\t// Remove discards buffered body and releases all associated resources.\n\t//\n\t// Multiple Buffer objects may refer to the same underlying storage.\n\t// In this case, care should be taken to ensure that Remove is called\n\t// only once since it will discard the shared storage and invalidate\n\t// all Buffer objects using it.\n\t//\n\t// Readers previously created using Open can still be used, but\n\t// new ones can't be created.\n\tRemove() error\n}\n"
  },
  {
    "path": "framework/buffer/bytesreader.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage buffer\n\nimport (\n\t\"bytes\"\n)\n\n// BytesReader is a wrapper for bytes.Reader that stores the original []byte\n// value and allows to retrieve it.\n//\n// It is meant for passing to libraries that expect a io.Reader\n// but apply certain optimizations when the Reader implements\n// Bytes() interface.\ntype BytesReader struct {\n\t*bytes.Reader\n\tvalue []byte\n}\n\n// Bytes returns the unread portion of underlying slice used to construct\n// BytesReader.\nfunc (br BytesReader) Bytes() []byte {\n\treturn br.value[int(br.Size())-br.Len():]\n}\n\n// Copy returns the BytesReader reading from the same slice as br at the same\n// position.\nfunc (br BytesReader) Copy() BytesReader {\n\treturn NewBytesReader(br.Bytes())\n}\n\n// Close is a dummy method for implementation of io.Closer so BytesReader can\n// be used in MemoryBuffer directly.\nfunc (br BytesReader) Close() error {\n\treturn nil\n}\n\nfunc NewBytesReader(b []byte) BytesReader {\n\t// BytesReader and not *BytesReader because BytesReader already wraps two\n\t// pointers and double indirection would be pointless.\n\treturn BytesReader{\n\t\tReader: bytes.NewReader(b),\n\t\tvalue:  b,\n\t}\n}\n"
  },
  {
    "path": "framework/buffer/file.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage buffer\n\nimport (\n\t\"crypto/rand\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n)\n\n// FileBuffer implements Buffer interface using file system.\ntype FileBuffer struct {\n\tPath string\n\n\t// LenHint is the size of the stored blob. It can\n\t// be set to avoid the need to call os.Stat in the\n\t// Len() method.\n\tLenHint int\n}\n\nfunc (fb FileBuffer) Open() (io.ReadCloser, error) {\n\treturn os.Open(fb.Path)\n}\n\nfunc (fb FileBuffer) Len() int {\n\tif fb.LenHint != 0 {\n\t\treturn fb.LenHint\n\t}\n\n\tinfo, err := os.Stat(fb.Path)\n\tif err != nil {\n\t\t// Any access to the file will probably fail too.  So we can't return a\n\t\t// sensible value.\n\t\treturn 0\n\t}\n\n\treturn int(info.Size())\n}\n\nfunc (fb FileBuffer) Remove() error {\n\treturn os.Remove(fb.Path)\n}\n\n// BufferInFile is a convenience function which creates FileBuffer with underlying\n// file created in the specified directory with the random name.\nfunc BufferInFile(r io.Reader, dir string) (Buffer, error) {\n\t// It is assumed that PRNG is initialized somewhere during program startup.\n\tnameBytes := make([]byte, 32)\n\t_, err := rand.Read(nameBytes)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"buffer: failed to generate randomness for file name: %v\", err)\n\t}\n\tpath := filepath.Join(dir, hex.EncodeToString(nameBytes))\n\tf, err := os.Create(path)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"buffer: failed to create file: %v\", err)\n\t}\n\tif _, err = io.Copy(f, r); err != nil {\n\t\treturn nil, fmt.Errorf(\"buffer: failed to write file: %v\", err)\n\t}\n\tif err := f.Close(); err != nil {\n\t\treturn nil, fmt.Errorf(\"buffer: failed to close file: %v\", err)\n\t}\n\n\treturn FileBuffer{Path: path}, nil\n}\n"
  },
  {
    "path": "framework/buffer/memory.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage buffer\n\nimport (\n\t\"io\"\n)\n\n// MemoryBuffer implements Buffer interface using byte slice.\ntype MemoryBuffer struct {\n\tSlice []byte\n}\n\nfunc (mb MemoryBuffer) Open() (io.ReadCloser, error) {\n\treturn NewBytesReader(mb.Slice), nil\n}\n\nfunc (mb MemoryBuffer) Len() int {\n\treturn len(mb.Slice)\n}\n\nfunc (mb MemoryBuffer) Remove() error {\n\treturn nil\n}\n\n// BufferInMemory is a convenience function which creates MemoryBuffer with\n// contents of the passed io.Reader.\nfunc BufferInMemory(r io.Reader) (Buffer, error) {\n\tblob, err := io.ReadAll(r)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn MemoryBuffer{Slice: blob}, nil\n}\n"
  },
  {
    "path": "framework/cfgparser/env.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage parser\n\nimport (\n\t\"os\"\n\t\"regexp\"\n\t\"strings\"\n)\n\nfunc expandEnvironment(nodes []Node) []Node {\n\t// If nodes is nil - don't replace with empty slice, as nil indicates \"no\n\t// block\".\n\tif nodes == nil {\n\t\treturn nil\n\t}\n\n\treplacer := buildEnvReplacer()\n\tnewNodes := make([]Node, 0, len(nodes))\n\tfor _, node := range nodes {\n\t\tnode.Name = removeUnexpandedEnvvars(replacer.Replace(node.Name))\n\t\tnewArgs := make([]string, 0, len(node.Args))\n\t\tfor _, arg := range node.Args {\n\t\t\tnewArgs = append(newArgs, removeUnexpandedEnvvars(replacer.Replace(arg)))\n\t\t}\n\t\tnode.Args = newArgs\n\t\tnode.Children = expandEnvironment(node.Children)\n\t\tnewNodes = append(newNodes, node)\n\t}\n\treturn newNodes\n}\n\nvar unixEnvvarRe = regexp.MustCompile(`{env:([^\\$]+)}`)\n\nfunc removeUnexpandedEnvvars(s string) string {\n\ts = unixEnvvarRe.ReplaceAllString(s, \"\")\n\treturn s\n}\n\nfunc buildEnvReplacer() *strings.Replacer {\n\tenv := os.Environ()\n\tpairs := make([]string, 0, len(env)*4)\n\tfor _, entry := range env {\n\t\tparts := strings.SplitN(entry, \"=\", 2)\n\t\tkey := parts[0]\n\t\tvalue := parts[1]\n\n\t\tpairs = append(pairs, \"{env:\"+key+\"}\", value)\n\t}\n\treturn strings.NewReplacer(pairs...)\n}\n"
  },
  {
    "path": "framework/cfgparser/imports.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage parser\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n)\n\nfunc (ctx *parseContext) expandImports(node Node, expansionDepth int) (Node, error) {\n\t// Leave nil value as is because it is used as non-existent block indicator\n\t// (vs empty slice - empty block).\n\tif node.Children == nil {\n\t\treturn node, nil\n\t}\n\n\tnewChildrens := make([]Node, 0, len(node.Children))\n\tcontainsImports := false\n\tfor _, child := range node.Children {\n\t\tchild, err := ctx.expandImports(child, expansionDepth+1)\n\t\tif err != nil {\n\t\t\treturn node, err\n\t\t}\n\n\t\tif child.Name == \"import\" {\n\t\t\t// We check it here instead of function start so we can\n\t\t\t// use line information from import directive that is likely\n\t\t\t// caused this error.\n\t\t\tif expansionDepth > 255 {\n\t\t\t\treturn node, NodeErr(child, \"hit import expansion limit\")\n\t\t\t}\n\n\t\t\tcontainsImports = true\n\t\t\tif len(child.Args) != 1 {\n\t\t\t\treturn node, ctx.Err(\"import directive requires exactly 1 argument\")\n\t\t\t}\n\n\t\t\tsubtree, err := ctx.resolveImport(child, child.Args[0], expansionDepth)\n\t\t\tif err != nil {\n\t\t\t\treturn node, err\n\t\t\t}\n\n\t\t\tnewChildrens = append(newChildrens, subtree...)\n\t\t} else {\n\t\t\tnewChildrens = append(newChildrens, child)\n\t\t}\n\t}\n\tnode.Children = newChildrens\n\n\t// We need to do another pass to expand any imports added by snippets we\n\t// just expanded.\n\tif containsImports {\n\t\treturn ctx.expandImports(node, expansionDepth+1)\n\t}\n\n\treturn node, nil\n}\n\nfunc (ctx *parseContext) resolveImport(node Node, name string, expansionDepth int) ([]Node, error) {\n\tif subtree, ok := ctx.snippets[name]; ok {\n\t\treturn subtree, nil\n\t}\n\n\tfile := name\n\tif !filepath.IsAbs(name) {\n\t\tfile = filepath.Join(filepath.Dir(ctx.fileLocation), name)\n\t}\n\tsrc, err := os.Open(file)\n\tif err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\tsrc, err = os.Open(file + \".conf\")\n\t\t\tif err != nil {\n\t\t\t\tif os.IsNotExist(err) {\n\t\t\t\t\treturn nil, NodeErr(node, \"unknown import: %s\", name)\n\t\t\t\t}\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t} else {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tnodes, snips, macros, err := readTree(src, file, expansionDepth+1)\n\tif err != nil {\n\t\treturn nodes, err\n\t}\n\tfor k, v := range snips {\n\t\tctx.snippets[k] = v\n\t}\n\tfor k, v := range macros {\n\t\tctx.macros[k] = v\n\t}\n\n\treturn nodes, nil\n}\n\nfunc (ctx *parseContext) expandMacros(node *Node) error {\n\tif strings.HasPrefix(node.Name, \"$(\") && strings.HasSuffix(node.Name, \")\") {\n\t\treturn ctx.Err(\"can't use macro argument as directive name\")\n\t}\n\n\tnewArgs := make([]string, 0, len(node.Args))\n\tfor _, arg := range node.Args {\n\t\tif !strings.HasPrefix(arg, \"$(\") || !strings.HasSuffix(arg, \")\") {\n\t\t\tif strings.Contains(arg, \"$(\") && strings.Contains(arg, \")\") {\n\t\t\t\tvar err error\n\t\t\t\targ, err = ctx.expandSingleValueMacro(arg)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tnewArgs = append(newArgs, arg)\n\t\t\tcontinue\n\t\t}\n\n\t\tmacroName := arg[2 : len(arg)-1]\n\t\treplacement, ok := ctx.macros[macroName]\n\t\tif !ok {\n\t\t\t// Undefined macros are expanded to zero arguments.\n\t\t\tcontinue\n\t\t}\n\n\t\tnewArgs = append(newArgs, replacement...)\n\t}\n\tnode.Args = newArgs\n\n\tif node.Children != nil {\n\t\tfor i := range node.Children {\n\t\t\tif err := ctx.expandMacros(&node.Children[i]); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\nvar macroRe = regexp.MustCompile(`\\$\\(([^\\$]+)\\)`)\n\nfunc (ctx *parseContext) expandSingleValueMacro(arg string) (string, error) {\n\tmatches := macroRe.FindAllStringSubmatch(arg, -1)\n\tfor _, match := range matches {\n\t\tmacroName := match[1]\n\t\tif len(ctx.macros[macroName]) > 1 {\n\t\t\treturn \"\", ctx.Err(\"can't expand macro with multiple arguments inside a string\")\n\t\t}\n\n\t\tvar value string\n\t\tif ctx.macros[macroName] != nil {\n\t\t\t// Macros have at least one argument.\n\t\t\tvalue = ctx.macros[macroName][0]\n\t\t}\n\n\t\targ = strings.ReplaceAll(arg, \"$(\"+macroName+\")\", value)\n\t}\n\n\treturn arg, nil\n}\n"
  },
  {
    "path": "framework/cfgparser/parse.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\n// Package config provides set of utilities for configuration parsing.\npackage parser\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"strings\"\n\t\"unicode\"\n\n\t\"github.com/foxcpp/maddy/framework/config/lexer\"\n)\n\n// Node struct describes a parsed configurtion block or a simple directive.\n//\n//\tname arg0 arg1 {\n//\t children0\n//\t children1\n//\t}\ntype Node struct {\n\t// Name is the first string at node's line.\n\tName string\n\t// Args are any strings placed after the node name.\n\tArgs []string\n\n\t// Children slice contains all children blocks if node is a block. Can be nil.\n\tChildren []Node\n\n\t// Snippet indicates whether current parsed node is a snippet. Always false\n\t// for all nodes returned from Read because snippets are expanded before it\n\t// returns.\n\tSnippet bool\n\n\t// Macro indicates whether current parsed node is a macro. Always false\n\t// for all nodes returned from Read because macros are expanded before it\n\t// returns.\n\tMacro bool\n\n\t// File is the name of node's source file.\n\tFile string\n\n\t// Line is the line number where the directive is located in the source file. For\n\t// blocks this is the line where \"block header\" (name + args) resides.\n\tLine int\n}\n\ntype parseContext struct {\n\tlexer.Dispenser\n\tnesting  int\n\tsnippets map[string][]Node\n\tmacros   map[string][]string\n\n\tfileLocation string\n}\n\nfunc validateNodeName(s string) error {\n\tif len(s) == 0 {\n\t\treturn errors.New(\"empty directive name\")\n\t}\n\n\tif unicode.IsDigit([]rune(s)[0]) {\n\t\treturn errors.New(\"directive name cannot start with a digit\")\n\t}\n\n\tallowedPunct := map[rune]bool{'.': true, '-': true, '_': true}\n\n\tfor _, ch := range s {\n\t\tif !unicode.IsLetter(ch) &&\n\t\t\t!unicode.IsDigit(ch) &&\n\t\t\t!allowedPunct[ch] {\n\t\t\treturn errors.New(\"character not allowed in directive name: \" + string(ch))\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// readNode reads node starting at current token pointed by the lexer's\n// cursor (it should point to node name).\n//\n// After readNode returns, the lexer's cursor will point to the last token of the parsed\n// Node. This ensures predictable cursor location independently of the EOF state.\n// Thus code reading multiple nodes should call readNode then manually\n// advance lexer cursor (ctx.Next) and either call readNode again or stop\n// because cursor hit EOF.\n//\n// readNode calls readNodes if currently parsed node is a block.\nfunc (ctx *parseContext) readNode() (Node, error) {\n\tnode := Node{}\n\tnode.File = ctx.File()\n\tnode.Line = ctx.Line()\n\n\tif ctx.Val() == \"{\" {\n\t\treturn node, ctx.SyntaxErr(\"block header\")\n\t}\n\n\tnode.Name = ctx.Val()\n\tif ok, name := ctx.isSnippet(node.Name); ok {\n\t\tnode.Name = name\n\t\tnode.Snippet = true\n\t}\n\n\tvar continueOnLF bool\n\tfor {\n\t\tfor ctx.NextArg() || (continueOnLF && ctx.NextLine()) {\n\t\t\tcontinueOnLF = false\n\t\t\t// name arg0 arg1 {\n\t\t\t//              # ^ called when we hit this token\n\t\t\t//   c0\n\t\t\t//   c1\n\t\t\t// }\n\t\t\tif ctx.Val() == \"{\" {\n\t\t\t\tvar err error\n\t\t\t\tnode.Children, err = ctx.readNodes()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn node, err\n\t\t\t\t}\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tnode.Args = append(node.Args, ctx.Val())\n\t\t}\n\n\t\t// Continue reading the same Node if the \\ was used to escape the newline.\n\t\t// E.g.\n\t\t//   name arg0 arg1 \\\n\t\t//\t   arg2 arg3\n\t\tif len(node.Args) != 0 && node.Args[len(node.Args)-1] == `\\` {\n\t\t\tlast := len(node.Args) - 1\n\t\t\tnode.Args[last] = node.Args[last][:len(node.Args[last])-1]\n\t\t\tif len(node.Args[last]) == 0 {\n\t\t\t\tnode.Args = node.Args[:last]\n\t\t\t}\n\t\t\tcontinueOnLF = true\n\t\t\tcontinue\n\t\t}\n\t\tbreak\n\t}\n\n\tmacroName, macroArgs, err := ctx.parseAsMacro(&node)\n\tif err != nil {\n\t\treturn node, err\n\t}\n\tif macroName != \"\" {\n\t\tnode.Name = macroName\n\t\tnode.Args = macroArgs\n\t\tnode.Macro = true\n\t}\n\n\tif !node.Macro && !node.Snippet {\n\t\tif err := validateNodeName(node.Name); err != nil {\n\t\t\treturn node, err\n\t\t}\n\t}\n\n\treturn node, nil\n}\n\nfunc NodeErr(node Node, f string, args ...interface{}) error {\n\tif node.File == \"\" {\n\t\treturn fmt.Errorf(f, args...)\n\t}\n\treturn fmt.Errorf(\"%s:%d: %s\", node.File, node.Line, fmt.Sprintf(f, args...))\n}\n\nfunc (ctx *parseContext) isSnippet(name string) (bool, string) {\n\tif strings.HasPrefix(name, \"(\") && strings.HasSuffix(name, \")\") {\n\t\treturn true, name[1 : len(name)-1]\n\t}\n\treturn false, \"\"\n}\n\nfunc (ctx *parseContext) parseAsMacro(node *Node) (macroName string, args []string, err error) {\n\tif !strings.HasPrefix(node.Name, \"$(\") {\n\t\treturn \"\", nil, nil\n\t}\n\tif !strings.HasSuffix(node.Name, \")\") {\n\t\treturn \"\", nil, ctx.Err(\"macro name must end with )\")\n\t}\n\tmacroName = node.Name[2 : len(node.Name)-1]\n\tif len(node.Args) < 2 {\n\t\treturn macroName, nil, ctx.Err(\"at least 2 arguments are required\")\n\t}\n\tif node.Args[0] != \"=\" {\n\t\treturn macroName, nil, ctx.Err(\"missing = in macro declaration\")\n\t}\n\treturn macroName, node.Args[1:], nil\n}\n\n// readNodes reads nodes from the currently parsed block.\n//\n// The lexer's cursor should point to the opening brace\n// name arg0 arg1 {  #< this one\n//\n//\t  c0\n//\t  c1\n//\t}\n//\n// To stay consistent with readNode after this function returns the lexer's cursor points\n// to the last token of the black (closing brace).\nfunc (ctx *parseContext) readNodes() ([]Node, error) {\n\t// It is not 'var res []Node' because we want empty\n\t// but non-nil Children slice for empty braces.\n\tres := []Node{}\n\n\tif ctx.nesting > 255 {\n\t\treturn res, ctx.Err(\"nesting limit reached\")\n\t}\n\n\tctx.nesting++\n\n\tvar requireNewLine bool\n\t// This loop iterates over logical lines.\n\t// Here are some examples, '#' is placed before token where cursor is when\n\t// another iteration of this loop starts.\n\t//\n\t// #a\n\t// #a b\n\t// #a b {\n\t//   #ac aa\n\t// #}\n\t// #aa bbb bbb \\\n\t//    ccc ccc\n\t// #a b { #ac aa }\n\t//\n\t// As can be seen by the latest example, sometimes such logical line might\n\t// not be terminated by an actual LF character and so this needs to be\n\t// handled carefully.\n\t//\n\t// Note that if the '}' is on the same physical line, it is currently\n\t// included as the part of the logical line, that is:\n\t// #a b { #ac aa }\n\t//        ^------- that's the logical line\n\t// #c d\n\t// ^--- that's the next logical line\n\t// This is handled by the \"edge case\" branch inside the loop.\n\tfor {\n\t\tif requireNewLine {\n\t\t\tif !ctx.NextLine() {\n\t\t\t\t// If we can't advance cursor even without Line constraint -\n\t\t\t\t// that's EOF.\n\t\t\t\tif !ctx.Next() {\n\t\t\t\t\treturn res, nil\n\t\t\t\t}\n\t\t\t\treturn res, ctx.Err(\"newline is required after closing brace\")\n\t\t\t}\n\t\t} else if !ctx.Next() {\n\t\t\tbreak\n\t\t}\n\n\t\t// name arg0 arg1 {\n\t\t//   c0\n\t\t//   c1\n\t\t// }\n\t\t// ^ called when we hit } on separate line,\n\t\t// This means block we hit end of our block.\n\t\tif ctx.Val() == \"}\" {\n\t\t\tctx.nesting--\n\t\t\t// name arg0 arg1 { #<1\n\t\t\t// }   }\n\t\t\t// ^2  ^3\n\t\t\t//\n\t\t\t// After #1 ctx.nesting is incremented by ctx.nesting++ before this loop.\n\t\t\t// Then we advance cursor and hit }, we exit loop, ctx.nesting now becomes 0.\n\t\t\t// But then the parent block reader does the same when it hits #3 -\n\t\t\t// ctx.nesting becomes -1 and it fails.\n\t\t\tif ctx.nesting < 0 {\n\t\t\t\treturn res, ctx.Err(\"unexpected }\")\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\t\tnode, err := ctx.readNode()\n\t\tif err != nil {\n\t\t\treturn res, err\n\t\t}\n\t\trequireNewLine = true\n\n\t\tshouldStop := false\n\n\t\t// name arg0 arg1 {\n\t\t//   c1 c2 }\n\t\t//         ^\n\t\t// Edge case, here we check if the last argument of the last node is a }\n\t\t// If it is - we stop as we hit the end of our block.\n\t\tif len(node.Args) != 0 && node.Args[len(node.Args)-1] == \"}\" {\n\t\t\tctx.nesting--\n\t\t\tif ctx.nesting < 0 {\n\t\t\t\treturn res, ctx.Err(\"unexpected }\")\n\t\t\t}\n\t\t\tnode.Args = node.Args[:len(node.Args)-1]\n\t\t\tshouldStop = true\n\t\t}\n\n\t\tif node.Macro {\n\t\t\tif ctx.nesting != 0 {\n\t\t\t\treturn res, ctx.Err(\"macro declarations are only allowed at top-level\")\n\t\t\t}\n\n\t\t\t// Macro declaration itself can contain macro references.\n\t\t\tif err := ctx.expandMacros(&node); err != nil {\n\t\t\t\treturn res, err\n\t\t\t}\n\n\t\t\t// = sign is removed by parseAsMacro.\n\t\t\t// It also cuts $( and ) from name.\n\t\t\tctx.macros[node.Name] = node.Args\n\t\t\tcontinue\n\t\t}\n\t\tif node.Snippet {\n\t\t\tif ctx.nesting != 0 {\n\t\t\t\treturn res, ctx.Err(\"snippet declarations are only allowed at top-level\")\n\t\t\t}\n\t\t\tif len(node.Args) != 0 {\n\t\t\t\treturn res, ctx.Err(\"snippet declarations can't have arguments\")\n\t\t\t}\n\n\t\t\tctx.snippets[node.Name] = node.Children\n\t\t\tcontinue\n\t\t}\n\n\t\tif err := ctx.expandMacros(&node); err != nil {\n\t\t\treturn res, err\n\t\t}\n\n\t\tres = append(res, node)\n\t\tif shouldStop {\n\t\t\tbreak\n\t\t}\n\t}\n\treturn res, nil\n}\n\nfunc readTree(r io.Reader, location string, expansionDepth int) (nodes []Node, snips map[string][]Node, macros map[string][]string, err error) {\n\tctx := parseContext{\n\t\tDispenser:    lexer.NewDispenser(location, r),\n\t\tsnippets:     make(map[string][]Node),\n\t\tmacros:       map[string][]string{},\n\t\tnesting:      -1,\n\t\tfileLocation: location,\n\t}\n\n\troot := Node{}\n\troot.File = location\n\troot.Line = 1\n\t// Before parsing starts the lexer's cursor points to the non-existent\n\t// token before the first one. From readNodes viewpoint this is opening\n\t// brace so we don't break any requirements here.\n\t//\n\t// For the same reason we use -1 as a starting nesting. So readNodes\n\t// will see this as it is reading block at nesting level 0.\n\troot.Children, err = ctx.readNodes()\n\tif err != nil {\n\t\treturn root.Children, ctx.snippets, ctx.macros, err\n\t}\n\n\t// There is no need to check ctx.nesting < 0 because it is checked by readNodes.\n\tif ctx.nesting > 0 {\n\t\treturn root.Children, ctx.snippets, ctx.macros, ctx.Err(\"unexpected EOF when looking for }\")\n\t}\n\n\troot, err = ctx.expandImports(root, expansionDepth)\n\tif err != nil {\n\t\treturn root.Children, ctx.snippets, ctx.macros, err\n\t}\n\n\treturn root.Children, ctx.snippets, ctx.macros, nil\n}\n\nfunc Read(r io.Reader, location string) (nodes []Node, err error) {\n\tnodes, _, _, err = readTree(r, location, 0)\n\tnodes = expandEnvironment(nodes)\n\treturn\n}\n"
  },
  {
    "path": "framework/cfgparser/parse_test.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage parser\n\nimport (\n\t\"os\"\n\t\"reflect\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nvar cases = []struct {\n\tname string\n\tcfg  string\n\ttree []Node\n\tfail bool\n}{\n\t{\n\t\t\"single directive without args\",\n\t\t`a`,\n\t\t[]Node{\n\t\t\t{\n\t\t\t\tName:     \"a\",\n\t\t\t\tArgs:     []string{},\n\t\t\t\tChildren: nil,\n\t\t\t\tFile:     \"test\",\n\t\t\t\tLine:     1,\n\t\t\t},\n\t\t},\n\t\tfalse,\n\t},\n\t{\n\t\t\"single directive with args\",\n\t\t`a a1 a2`,\n\t\t[]Node{\n\t\t\t{\n\t\t\t\tName:     \"a\",\n\t\t\t\tArgs:     []string{\"a1\", \"a2\"},\n\t\t\t\tChildren: nil,\n\t\t\t\tFile:     \"test\",\n\t\t\t\tLine:     1,\n\t\t\t},\n\t\t},\n\t\tfalse,\n\t},\n\t{\n\t\t\"single directive with empty braces\",\n\t\t`a { }`,\n\t\t[]Node{\n\t\t\t{\n\t\t\t\tName:     \"a\",\n\t\t\t\tArgs:     []string{},\n\t\t\t\tChildren: []Node{},\n\t\t\t\tFile:     \"test\",\n\t\t\t\tLine:     1,\n\t\t\t},\n\t\t},\n\t\tfalse,\n\t},\n\t{\n\t\t\"single directive with arguments and empty braces\",\n\t\t`a a1 a2 { }`,\n\t\t[]Node{\n\t\t\t{\n\t\t\t\tName:     \"a\",\n\t\t\t\tArgs:     []string{\"a1\", \"a2\"},\n\t\t\t\tChildren: []Node{},\n\t\t\t\tFile:     \"test\",\n\t\t\t\tLine:     1,\n\t\t\t},\n\t\t},\n\t\tfalse,\n\t},\n\t{\n\t\t\"single directive with a block\",\n\t\t`a a1 a2 {\n\t\t\ta_child1 c1arg1 c1arg2\n\t\t\ta_child2 c2arg1 c2arg2\n\t\t}`,\n\t\t[]Node{\n\t\t\t{\n\t\t\t\tName: \"a\",\n\t\t\t\tArgs: []string{\"a1\", \"a2\"},\n\t\t\t\tChildren: []Node{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:     \"a_child1\",\n\t\t\t\t\t\tArgs:     []string{\"c1arg1\", \"c1arg2\"},\n\t\t\t\t\t\tChildren: nil,\n\t\t\t\t\t\tFile:     \"test\",\n\t\t\t\t\t\tLine:     2,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tName:     \"a_child2\",\n\t\t\t\t\t\tArgs:     []string{\"c2arg1\", \"c2arg2\"},\n\t\t\t\t\t\tChildren: nil,\n\t\t\t\t\t\tFile:     \"test\",\n\t\t\t\t\t\tLine:     3,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tFile: \"test\",\n\t\t\t\tLine: 1,\n\t\t\t},\n\t\t},\n\t\tfalse,\n\t},\n\t{\n\t\t\"single directive with missing closing brace\",\n\t\t`a {`,\n\t\tnil,\n\t\ttrue,\n\t},\n\t{\n\t\t\"single directive with missing opening brace\",\n\t\t`a }`,\n\t\tnil,\n\t\ttrue,\n\t},\n\t{\n\t\t\"two directives\",\n\t\t`a\n\t\t b`,\n\t\t[]Node{\n\t\t\t{\n\t\t\t\tName:     \"a\",\n\t\t\t\tArgs:     []string{},\n\t\t\t\tChildren: nil,\n\t\t\t\tFile:     \"test\",\n\t\t\t\tLine:     1,\n\t\t\t},\n\t\t\t{\n\t\t\t\tName:     \"b\",\n\t\t\t\tArgs:     []string{},\n\t\t\t\tChildren: nil,\n\t\t\t\tFile:     \"test\",\n\t\t\t\tLine:     2,\n\t\t\t},\n\t\t},\n\t\tfalse,\n\t},\n\t{\n\t\t\"two directives with arguments\",\n\t\t`a a1 a2\n\t\t b b1 b2`,\n\t\t[]Node{\n\t\t\t{\n\t\t\t\tName:     \"a\",\n\t\t\t\tArgs:     []string{\"a1\", \"a2\"},\n\t\t\t\tChildren: nil,\n\t\t\t\tFile:     \"test\",\n\t\t\t\tLine:     1,\n\t\t\t},\n\t\t\t{\n\t\t\t\tName:     \"b\",\n\t\t\t\tArgs:     []string{\"b1\", \"b2\"},\n\t\t\t\tChildren: nil,\n\t\t\t\tFile:     \"test\",\n\t\t\t\tLine:     2,\n\t\t\t},\n\t\t},\n\t\tfalse,\n\t},\n\t{\n\t\t\"backslash on the end of line\",\n\t\t`a a1 a2 \\\n\t\t   a3 a4`,\n\t\t[]Node{\n\t\t\t{\n\t\t\t\tName:     \"a\",\n\t\t\t\tArgs:     []string{\"a1\", \"a2\", \"a3\", \"a4\"},\n\t\t\t\tChildren: nil,\n\t\t\t\tFile:     \"test\",\n\t\t\t\tLine:     1,\n\t\t\t},\n\t\t},\n\t\tfalse,\n\t},\n\t{\n\t\t\"directive with missing closing brace on different line\",\n\t\t`a a1 a2 {\n\t\t\ta_child1 c1arg1 c1arg2\n\t\t`,\n\t\tnil,\n\t\ttrue,\n\t},\n\t{\n\t\t\"single directive with closing brace on children's line\",\n\t\t`a a1 a2 {\n\t\t\ta_child1 c1arg1 c1arg2\n\t\t\ta_child2 c2arg1 c2arg2 }\n\t\t b`,\n\t\t[]Node{\n\t\t\t{\n\t\t\t\tName: \"a\",\n\t\t\t\tArgs: []string{\"a1\", \"a2\"},\n\t\t\t\tChildren: []Node{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:     \"a_child1\",\n\t\t\t\t\t\tArgs:     []string{\"c1arg1\", \"c1arg2\"},\n\t\t\t\t\t\tChildren: nil,\n\t\t\t\t\t\tFile:     \"test\",\n\t\t\t\t\t\tLine:     2,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tName:     \"a_child2\",\n\t\t\t\t\t\tArgs:     []string{\"c2arg1\", \"c2arg2\"},\n\t\t\t\t\t\tChildren: nil,\n\t\t\t\t\t\tFile:     \"test\",\n\t\t\t\t\t\tLine:     3,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tFile: \"test\",\n\t\t\t\tLine: 1,\n\t\t\t},\n\t\t\t{\n\t\t\t\tName:     \"b\",\n\t\t\t\tArgs:     []string{},\n\t\t\t\tChildren: nil,\n\t\t\t\tFile:     \"test\",\n\t\t\t\tLine:     4,\n\t\t\t},\n\t\t},\n\t\tfalse,\n\t},\n\t{\n\t\t\"single directive with childrens on the same line\",\n\t\t`a a1 a2 { a_child1 c1arg1 c1arg2 }`,\n\t\t[]Node{\n\t\t\t{\n\t\t\t\tName: \"a\",\n\t\t\t\tArgs: []string{\"a1\", \"a2\"},\n\t\t\t\tChildren: []Node{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:     \"a_child1\",\n\t\t\t\t\t\tArgs:     []string{\"c1arg1\", \"c1arg2\"},\n\t\t\t\t\t\tChildren: nil,\n\t\t\t\t\t\tFile:     \"test\",\n\t\t\t\t\t\tLine:     1,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tFile: \"test\",\n\t\t\t\tLine: 1,\n\t\t\t},\n\t\t},\n\t\tfalse,\n\t},\n\t{\n\t\t\"invalid directive name\",\n\t\t`a-a4@%8 whatever`,\n\t\tnil,\n\t\ttrue,\n\t},\n\t{\n\t\t\"directive name starts with a digit\",\n\t\t`1w whatever`,\n\t\tnil,\n\t\ttrue,\n\t},\n\t{\n\t\t\"missing block header\",\n\t\t`{ a_child1 c1arg1 c1arg2 }`,\n\t\tnil,\n\t\ttrue,\n\t},\n\t{\n\t\t\"extra closing brace\",\n\t\t`a {\n\t\t\tchild1\n\t\t} }\n\t\t`,\n\t\tnil,\n\t\ttrue,\n\t},\n\t{\n\t\t\"extra opening brace\",\n\t\t`a { {\n\t\t}`,\n\t\tnil,\n\t\ttrue,\n\t},\n\t{\n\t\t\"closing brace in next block header\",\n\t\t`a {\n\t\t} b b1`,\n\t\tnil,\n\t\ttrue,\n\t},\n\t{\n\t\t\"environment variable expansion\",\n\t\t`a {env:TESTING_VARIABLE}`,\n\t\t[]Node{\n\t\t\t{\n\t\t\t\tName:     \"a\",\n\t\t\t\tArgs:     []string{\"ABCDEF\"},\n\t\t\t\tChildren: nil,\n\t\t\t\tFile:     \"test\",\n\t\t\t\tLine:     1,\n\t\t\t},\n\t\t},\n\t\tfalse,\n\t},\n\t{\n\t\t\"missing environment variable expansion (unix-like syntax)\",\n\t\t`a {env:TESTING_VARIABLE3}`,\n\t\t[]Node{\n\t\t\t{\n\t\t\t\tName:     \"a\",\n\t\t\t\tArgs:     []string{\"\"},\n\t\t\t\tChildren: nil,\n\t\t\t\tFile:     \"test\",\n\t\t\t\tLine:     1,\n\t\t\t},\n\t\t},\n\t\tfalse,\n\t},\n\t{\n\t\t\"incomplete environment variable syntax\",\n\t\t`a {env:TESTING_VARIABLE`,\n\t\t[]Node{\n\t\t\t{\n\t\t\t\tName:     \"a\",\n\t\t\t\tArgs:     []string{\"{env:TESTING_VARIABLE\"},\n\t\t\t\tChildren: nil,\n\t\t\t\tFile:     \"test\",\n\t\t\t\tLine:     1,\n\t\t\t},\n\t\t},\n\t\tfalse,\n\t},\n\t{\n\t\t\"snippet expansion\",\n\t\t`(foo) { a }\n\t\t import foo`,\n\t\t[]Node{\n\t\t\t{\n\t\t\t\tName:     \"a\",\n\t\t\t\tArgs:     []string{},\n\t\t\t\tChildren: nil,\n\t\t\t\tFile:     \"test\",\n\t\t\t\tLine:     1,\n\t\t\t},\n\t\t},\n\t\tfalse,\n\t},\n\t{\n\t\t\"snippet expansion inside a block\",\n\t\t`(foo) { a }\n        foo {\n            boo\n            import foo\n        }`,\n\t\t[]Node{\n\t\t\t{\n\t\t\t\tName: \"foo\",\n\t\t\t\tArgs: []string{},\n\t\t\t\tChildren: []Node{\n\t\t\t\t\t{\n\t\t\t\t\t\tName: \"boo\",\n\t\t\t\t\t\tArgs: []string{},\n\t\t\t\t\t\tFile: \"test\",\n\t\t\t\t\t\tLine: 3,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tName: \"a\",\n\t\t\t\t\t\tArgs: []string{},\n\t\t\t\t\t\tFile: \"test\",\n\t\t\t\t\t\tLine: 1,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tFile: \"test\",\n\t\t\t\tLine: 2,\n\t\t\t},\n\t\t},\n\t\tfalse,\n\t},\n\t{\n\t\t\"missing snippet\",\n\t\t`import foo`,\n\t\tnil,\n\t\ttrue,\n\t},\n\t{\n\t\t\"unlimited recursive snippet expansion\",\n\t\t`(foo) { import foo }\n\t\t import foo`,\n\t\tnil,\n\t\ttrue,\n\t},\n\t{\n\t\t\"snippet declaration with args\",\n\t\t`(foo) a b c { }`,\n\t\tnil,\n\t\ttrue,\n\t},\n\t{\n\t\t\"snippet declaration inside block\",\n\t\t`abc {\n\t\t\t(foo) { }\n\t\t}`,\n\t\tnil,\n\t\ttrue,\n\t},\n\t{\n\t\t\"block nesting limit\",\n\t\t`a ` + strings.Repeat(\"a { \", 1000) + strings.Repeat(\" }\", 1000),\n\t\tnil,\n\t\ttrue,\n\t},\n\t{\n\t\t\"macro expansion, single argument\",\n\t\t`$(foo) = bar\n\t\tdir $(foo)`,\n\t\t[]Node{\n\t\t\t{\n\t\t\t\tName:     \"dir\",\n\t\t\t\tArgs:     []string{\"bar\"},\n\t\t\t\tChildren: nil,\n\t\t\t\tFile:     \"test\",\n\t\t\t\tLine:     2,\n\t\t\t},\n\t\t},\n\t\tfalse,\n\t},\n\t{\n\t\t\"macro expansion, inside argument\",\n\t\t`$(foo) = bar\n\t\tdir aaa/$(foo)/bbb`,\n\t\t[]Node{\n\t\t\t{\n\t\t\t\tName:     \"dir\",\n\t\t\t\tArgs:     []string{\"aaa/bar/bbb\"},\n\t\t\t\tChildren: nil,\n\t\t\t\tFile:     \"test\",\n\t\t\t\tLine:     2,\n\t\t\t},\n\t\t},\n\t\tfalse,\n\t},\n\t{\n\t\t\"macro expansion, inside argument, multi-value\",\n\t\t`$(foo) = bar baz\n\t\tdir aaa/$(foo)/bbb`,\n\t\tnil,\n\t\ttrue,\n\t},\n\t{\n\t\t\"macro expansion, multiple arguments\",\n\t\t`$(foo) = bar baz\n\t\tdir $(foo)`,\n\t\t[]Node{\n\t\t\t{\n\t\t\t\tName:     \"dir\",\n\t\t\t\tArgs:     []string{\"bar\", \"baz\"},\n\t\t\t\tChildren: nil,\n\t\t\t\tFile:     \"test\",\n\t\t\t\tLine:     2,\n\t\t\t},\n\t\t},\n\t\tfalse,\n\t},\n\t{\n\t\t\"macro expansion, undefined\",\n\t\t`dir $(foo)`,\n\t\t[]Node{\n\t\t\t{\n\t\t\t\tName:     \"dir\",\n\t\t\t\tArgs:     []string{},\n\t\t\t\tChildren: nil,\n\t\t\t\tFile:     \"test\",\n\t\t\t\tLine:     1,\n\t\t\t},\n\t\t},\n\t\tfalse,\n\t},\n\t{\n\t\t\"macro expansion, empty\",\n\t\t`$(foo) =`,\n\t\tnil,\n\t\ttrue,\n\t},\n\t{\n\t\t\"macro expansion, name replacement\",\n\t\t`$(foo) = a b\n\t\t\t$(foo) 1`,\n\t\tnil,\n\t\ttrue,\n\t},\n\t{\n\t\t\"macro expansion, missing =\",\n\t\t`$(foo) a b\n\t\t\t$(foo) 1`,\n\t\tnil,\n\t\ttrue,\n\t},\n\t{\n\t\t\"macro expansion, not on top level\",\n\t\t`a {\n\t\t\t\t$(foo) = a b\n\t\t\t}\n\t\t\t$(foo) 1`,\n\t\tnil,\n\t\ttrue,\n\t},\n\t{\n\t\t\"macro expansion, nested\",\n\t\t`$(foo) = a\n\t\t\t$(bar) = $(foo) b\n\t\t\tdir $(bar)`,\n\t\t[]Node{\n\t\t\t{\n\t\t\t\tName:     \"dir\",\n\t\t\t\tArgs:     []string{\"a\", \"b\"},\n\t\t\t\tChildren: nil,\n\t\t\t\tFile:     \"test\",\n\t\t\t\tLine:     3,\n\t\t\t},\n\t\t},\n\t\tfalse,\n\t},\n\t{\n\t\t\"macro expansion, used inside snippet\",\n\t\t`$(foo) = a\n\t\t\t(bar) {\n\t\t\t\tdir $(foo)\n\t\t\t}\n\t\t\timport bar`,\n\t\t[]Node{\n\t\t\t{\n\t\t\t\tName:     \"dir\",\n\t\t\t\tArgs:     []string{\"a\"},\n\t\t\t\tChildren: nil,\n\t\t\t\tFile:     \"test\",\n\t\t\t\tLine:     3,\n\t\t\t},\n\t\t},\n\t\tfalse,\n\t},\n\t{\n\t\t\"macro expansion, used inside snippet, defined after\",\n\t\t`\n\t\t\t(bar) {\n\t\t\t\tdir $(foo)\n\t\t\t}\n\t\t\t$(foo) = a\n\t\t\timport bar`,\n\t\t[]Node{\n\t\t\t{\n\t\t\t\tName:     \"dir\",\n\t\t\t\tArgs:     []string{},\n\t\t\t\tChildren: nil,\n\t\t\t\tFile:     \"test\",\n\t\t\t\tLine:     3,\n\t\t\t},\n\t\t},\n\t\tfalse,\n\t},\n}\n\nfunc printTree(t *testing.T, root Node, indent int) {\n\tt.Log(strings.Repeat(\" \", indent)+root.Name, root.Args)\n\tfor _, child := range root.Children {\n\t\tt.Log(child, indent+1)\n\t}\n}\n\nfunc TestRead(t *testing.T) {\n\trequire.NoError(t, os.Setenv(\"TESTING_VARIABLE\", \"ABCDEF\"))\n\trequire.NoError(t, os.Setenv(\"TESTING_VARIABLE2\", \"ABC2 DEF2\"))\n\n\tfor _, case_ := range cases {\n\t\tt.Run(case_.name, func(t *testing.T) {\n\t\t\ttree, err := Read(strings.NewReader(case_.cfg), \"test\")\n\t\t\tif !case_.fail && err != nil {\n\t\t\t\tt.Error(\"unexpected failure:\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif case_.fail {\n\t\t\t\tif err == nil {\n\t\t\t\t\tt.Log(\"expected failure but Read succeeded\")\n\t\t\t\t\tt.Log(\"got tree:\")\n\t\t\t\t\tt.Logf(\"%+v\", tree)\n\t\t\t\t\tfor _, node := range tree {\n\t\t\t\t\t\tprintTree(t, node, 0)\n\t\t\t\t\t}\n\t\t\t\t\tt.Fail()\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif !reflect.DeepEqual(case_.tree, tree) {\n\t\t\t\tt.Log(\"parse result mismatch\")\n\t\t\t\tt.Log(\"expected:\")\n\t\t\t\tt.Logf(\"%+#v\", case_.tree)\n\t\t\t\tfor _, node := range case_.tree {\n\t\t\t\t\tprintTree(t, node, 0)\n\t\t\t\t}\n\t\t\t\tt.Log(\"actual:\")\n\t\t\t\tt.Logf(\"%+#v\", tree)\n\t\t\t\tfor _, node := range tree {\n\t\t\t\t\tprintTree(t, node, 0)\n\t\t\t\t}\n\t\t\t\tt.Fail()\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "framework/config/config.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage config\n\nimport (\n\t\"fmt\"\n\n\tparser \"github.com/foxcpp/maddy/framework/cfgparser\"\n)\n\ntype (\n\tNode = parser.Node\n)\n\nfunc NodeErr(node Node, f string, args ...interface{}) error {\n\tif node.File == \"\" {\n\t\treturn fmt.Errorf(f, args...)\n\t}\n\treturn fmt.Errorf(\"%s:%d: %s\", node.File, node.Line, fmt.Sprintf(f, args...))\n}\n"
  },
  {
    "path": "framework/config/directories.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage config\n\nvar (\n\t// StateDirectory contains the path to the directory that\n\t// should be used to store any data that should be\n\t// preserved between sessions.\n\t//\n\t// Value of this variable must not change after initialization\n\t// in cmd/maddy/main.go.\n\tStateDirectory string\n\n\t// RuntimeDirectory contains the path to the directory that\n\t// should be used to store any temporary data.\n\t//\n\t// It should be preferred over os.TempDir, which is\n\t// global and world-readable on most systems, while\n\t// RuntimeDirectory can be dedicated for maddy.\n\t//\n\t// Value of this variable must not change after initialization\n\t// in cmd/maddy/main.go.\n\tRuntimeDirectory string\n\n\t// LibexecDirectory contains the path to the directory\n\t// where helper binaries should be searched.\n\t//\n\t// Value of this variable must not change after initialization\n\t// in cmd/maddy/main.go.\n\tLibexecDirectory string\n)\n"
  },
  {
    "path": "framework/config/endpoint.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage config\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\t\"net/url\"\n\t\"path/filepath\"\n\t\"strings\"\n)\n\n// Endpoint represents a site address. It contains the original input value,\n// and the component parts of an address. The component parts may be updated to\n// the correct values as setup proceeds, but the original value should never be\n// changed.\ntype Endpoint struct {\n\tOriginal, Scheme, Host, Port, Path string\n}\n\n// String returns a human-friendly print of the address.\nfunc (e Endpoint) String() string {\n\tif e.Original != \"\" {\n\t\treturn e.Original\n\t}\n\n\tif e.Scheme == \"unix\" {\n\t\treturn \"unix://\" + e.Path\n\t}\n\n\tif e.Host == \"\" && e.Port == \"\" {\n\t\treturn \"\"\n\t}\n\ts := e.Scheme\n\tif s != \"\" {\n\t\ts += \"://\"\n\t}\n\n\thost := e.Host\n\tif strings.Contains(host, \":\") {\n\t\thost = \"[\" + host + \"]\"\n\t}\n\ts += host\n\n\tif e.Port != \"\" {\n\t\ts += \":\" + e.Port\n\t}\n\tif e.Path != \"\" {\n\t\ts += e.Path\n\t}\n\treturn s\n}\n\nfunc (e Endpoint) Network() string {\n\tif e.Scheme == \"unix\" {\n\t\treturn \"unix\"\n\t}\n\treturn \"tcp\"\n}\n\nfunc (e Endpoint) Address() string {\n\tif e.Scheme == \"unix\" {\n\t\treturn e.Path\n\t}\n\treturn net.JoinHostPort(e.Host, e.Port)\n}\n\nfunc (e Endpoint) IsTLS() bool {\n\treturn e.Scheme == \"tls\"\n}\n\n// ParseEndpoint parses an endpoint string into a structured format with separate\n// scheme, host, port, and path portions, as well as the original input string.\nfunc ParseEndpoint(str string) (Endpoint, error) {\n\tinput := str\n\n\tu, err := url.Parse(str)\n\tif err != nil {\n\t\treturn Endpoint{}, err\n\t}\n\n\tswitch u.Scheme {\n\tcase \"tcp\", \"tls\":\n\t\t// ALL GREEN\n\n\t\t// scheme:OPAQUE URL syntax\n\t\tif u.Host == \"\" && u.Opaque != \"\" {\n\t\t\tu.Host = u.Opaque\n\t\t}\n\tcase \"unix\":\n\t\t// scheme:OPAQUE URL syntax\n\t\tif u.Path == \"\" && u.Opaque != \"\" {\n\t\t\tu.Path = u.Opaque\n\t\t}\n\n\t\tvar actualPath string\n\t\tif u.Host != \"\" {\n\t\t\tactualPath += u.Host\n\t\t}\n\t\tif u.Path != \"\" {\n\t\t\tactualPath += u.Path\n\t\t}\n\n\t\tif !filepath.IsAbs(actualPath) {\n\t\t\tactualPath = filepath.Join(RuntimeDirectory, actualPath)\n\t\t}\n\n\t\treturn Endpoint{Original: input, Scheme: u.Scheme, Path: actualPath}, err\n\tdefault:\n\t\treturn Endpoint{}, fmt.Errorf(\"unsupported scheme: %s (%+v)\", input, u)\n\t}\n\n\t// separate host and port\n\thost, port, err := net.SplitHostPort(u.Host)\n\tif err != nil {\n\t\thost, port, err = net.SplitHostPort(u.Host + \":\")\n\t\tif err != nil {\n\t\t\thost = u.Host\n\t\t}\n\t}\n\tif port == \"\" {\n\t\treturn Endpoint{}, fmt.Errorf(\"port is required\")\n\t}\n\n\treturn Endpoint{Original: input, Scheme: u.Scheme, Host: host, Port: port, Path: u.Path}, err\n}\n"
  },
  {
    "path": "framework/config/endpoint_test.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage config\n\nimport (\n\t\"reflect\"\n\t\"testing\"\n)\n\nfunc TestStandardizeAddress(t *testing.T) {\n\tfor _, expected := range []Endpoint{\n\t\t{Original: \"tcp://0.0.0.0:10025\", Scheme: \"tcp\", Host: \"0.0.0.0\", Port: \"10025\"},\n\t\t{Original: \"tcp://[::]:10025\", Scheme: \"tcp\", Host: \"::\", Port: \"10025\"},\n\t\t{Original: \"tcp:127.0.0.1:10025\", Scheme: \"tcp\", Host: \"127.0.0.1\", Port: \"10025\"},\n\t\t{Original: \"unix://path\", Scheme: \"unix\", Host: \"\", Path: \"path\", Port: \"\"},\n\t\t{Original: \"unix:path\", Scheme: \"unix\", Host: \"\", Path: \"path\", Port: \"\"},\n\t\t{Original: \"unix:/path\", Scheme: \"unix\", Host: \"\", Path: \"/path\", Port: \"\"},\n\t\t{Original: \"unix:///path\", Scheme: \"unix\", Host: \"\", Path: \"/path\", Port: \"\"},\n\t\t{Original: \"unix://also/path\", Scheme: \"unix\", Host: \"\", Path: \"also/path\", Port: \"\"},\n\t\t{Original: \"unix:///also/path\", Scheme: \"unix\", Host: \"\", Path: \"/also/path\", Port: \"\"},\n\t\t{Original: \"tls://0.0.0.0:10025\", Scheme: \"tls\", Host: \"0.0.0.0\", Port: \"10025\"},\n\t\t{Original: \"tls:0.0.0.0:10025\", Scheme: \"tls\", Host: \"0.0.0.0\", Port: \"10025\"},\n\t} {\n\t\tactual, err := ParseEndpoint(expected.Original)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"Unexpected failure for %s: %v\", expected.Original, err)\n\t\t\treturn\n\t\t}\n\n\t\tif !reflect.DeepEqual(expected, actual) {\n\t\t\tt.Errorf(\"Didn't parse URL %q correctly\\ngot %#v\\nwant %#v\", expected.Original, actual, expected)\n\t\t\tcontinue\n\t\t}\n\n\t\tif actual.String() != expected.Original {\n\t\t\tt.Errorf(\"actual.String() = %s, want %s\", actual.String(), expected.Original)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "framework/config/lexer/LICENSE.APACHE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"{}\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright {yyyy} {name of copyright owner}\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "framework/config/lexer/README.md",
    "content": "caddyfile lexer copied from [caddy](https://github.com/caddyserver/caddy) project.\n\nTaken from the following commit:\n```\ncommit ed4c2775e46b924d4851e04cc281633b1b2c15af\nAuthor: Alexander Danilov <SmilingNavern@users.noreply.github.com>\nDate:   Wed Aug 21 20:13:34 2019 +0300\n\n    main: log caddy version on start (#2717)\n\n```\n\nLicense of the original code is included in LICENSE.APACHE file in this\ndirectory.\n\nNo signficant changes was made to the code (e.g. it is safe to update it from\ncaddy repo).\n\nThe code is copied because caddy brings quite a lot of dependencies we don't\nuse and this slows down many tools.\n"
  },
  {
    "path": "framework/config/lexer/dispenser.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\n// Copyright 2015 Light Code Labs, LLC\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage lexer\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"strings\"\n)\n\n// Dispenser is a type that dispenses tokens, similarly to a lexer,\n// except that it can do so with some notion of structure and has\n// some really convenient methods.\ntype Dispenser struct {\n\tfilename string\n\ttokens   []Token\n\tcursor   int\n\tnesting  int\n}\n\n// NewDispenser returns a Dispenser, ready to use for parsing the given input.\nfunc NewDispenser(filename string, input io.Reader) Dispenser {\n\ttokens, _ := allTokens(input) // ignoring error because nothing to do with it\n\treturn Dispenser{\n\t\tfilename: filename,\n\t\ttokens:   tokens,\n\t\tcursor:   -1,\n\t}\n}\n\n// NewDispenserTokens returns a Dispenser filled with the given tokens.\nfunc NewDispenserTokens(filename string, tokens []Token) Dispenser {\n\treturn Dispenser{\n\t\tfilename: filename,\n\t\ttokens:   tokens,\n\t\tcursor:   -1,\n\t}\n}\n\n// Next loads the next token. Returns true if a token\n// was loaded; false otherwise. If false, all tokens\n// have been consumed.\nfunc (d *Dispenser) Next() bool {\n\tif d.cursor < len(d.tokens)-1 {\n\t\td.cursor++\n\t\treturn true\n\t}\n\treturn false\n}\n\n// NextArg loads the next token if it is on the same\n// line. Returns true if a token was loaded; false\n// otherwise. If false, all tokens on the line have\n// been consumed. It handles imported tokens correctly.\nfunc (d *Dispenser) NextArg() bool {\n\tif d.cursor < 0 {\n\t\td.cursor++\n\t\treturn true\n\t}\n\tif d.cursor >= len(d.tokens) {\n\t\treturn false\n\t}\n\tif d.cursor < len(d.tokens)-1 &&\n\t\td.tokens[d.cursor].File == d.tokens[d.cursor+1].File &&\n\t\td.tokens[d.cursor].Line+d.numLineBreaks(d.cursor) == d.tokens[d.cursor+1].Line {\n\t\td.cursor++\n\t\treturn true\n\t}\n\treturn false\n}\n\n// NextLine loads the next token only if it is not on the same\n// line as the current token, and returns true if a token was\n// loaded; false otherwise. If false, there is not another token\n// or it is on the same line. It handles imported tokens correctly.\nfunc (d *Dispenser) NextLine() bool {\n\tif d.cursor < 0 {\n\t\td.cursor++\n\t\treturn true\n\t}\n\tif d.cursor >= len(d.tokens) {\n\t\treturn false\n\t}\n\tif d.cursor < len(d.tokens)-1 &&\n\t\t(d.tokens[d.cursor].File != d.tokens[d.cursor+1].File ||\n\t\t\td.tokens[d.cursor].Line+d.numLineBreaks(d.cursor) < d.tokens[d.cursor+1].Line) {\n\t\td.cursor++\n\t\treturn true\n\t}\n\treturn false\n}\n\n// NextBlock can be used as the condition of a for loop\n// to load the next token as long as it opens a block or\n// is already in a block. It returns true if a token was\n// loaded, or false when the block's closing curly brace\n// was loaded and thus the block ended. Nested blocks are\n// not supported.\nfunc (d *Dispenser) NextBlock() bool {\n\tif d.nesting > 0 {\n\t\td.Next()\n\t\tif d.Val() == \"}\" {\n\t\t\td.nesting--\n\t\t\treturn false\n\t\t}\n\t\treturn true\n\t}\n\tif !d.NextArg() { // block must open on same line\n\t\treturn false\n\t}\n\tif d.Val() != \"{\" {\n\t\td.cursor-- // roll back if not opening brace\n\t\treturn false\n\t}\n\td.Next()\n\tif d.Val() == \"}\" {\n\t\t// Open and then closed right away\n\t\treturn false\n\t}\n\td.nesting++\n\treturn true\n}\n\n// Val gets the text of the current token. If there is no token\n// loaded, it returns empty string.\nfunc (d *Dispenser) Val() string {\n\tif d.cursor < 0 || d.cursor >= len(d.tokens) {\n\t\treturn \"\"\n\t}\n\treturn d.tokens[d.cursor].Text\n}\n\n// Line gets the line number of the current token. If there is no token\n// loaded, it returns 0.\nfunc (d *Dispenser) Line() int {\n\tif d.cursor < 0 || d.cursor >= len(d.tokens) {\n\t\treturn 0\n\t}\n\treturn d.tokens[d.cursor].Line\n}\n\n// File gets the filename of the current token. If there is no token loaded,\n// it returns the filename originally given when parsing started.\nfunc (d *Dispenser) File() string {\n\tif d.cursor < 0 || d.cursor >= len(d.tokens) {\n\t\treturn d.filename\n\t}\n\tif tokenFilename := d.tokens[d.cursor].File; tokenFilename != \"\" {\n\t\treturn tokenFilename\n\t}\n\treturn d.filename\n}\n\n// Args is a convenience function that loads the next arguments\n// (tokens on the same line) into an arbitrary number of strings\n// pointed to in targets. If there are fewer tokens available\n// than string pointers, the remaining strings will not be changed\n// and false will be returned. If there were enough tokens available\n// to fill the arguments, then true will be returned.\nfunc (d *Dispenser) Args(targets ...*string) bool {\n\tenough := true\n\tfor i := 0; i < len(targets); i++ {\n\t\tif !d.NextArg() {\n\t\t\tenough = false\n\t\t\tbreak\n\t\t}\n\t\t*targets[i] = d.Val()\n\t}\n\treturn enough\n}\n\n// RemainingArgs loads any more arguments (tokens on the same line)\n// into a slice and returns them. Open curly brace tokens also indicate\n// the end of arguments, and the curly brace is not included in\n// the return value nor is it loaded.\nfunc (d *Dispenser) RemainingArgs() []string {\n\tvar args []string\n\n\tfor d.NextArg() {\n\t\tif d.Val() == \"{\" {\n\t\t\td.cursor--\n\t\t\tbreak\n\t\t}\n\t\targs = append(args, d.Val())\n\t}\n\n\treturn args\n}\n\n// ArgErr returns an argument error, meaning that another\n// argument was expected but not found. In other words,\n// a line break or open curly brace was encountered instead of\n// an argument.\nfunc (d *Dispenser) ArgErr() error {\n\tif d.Val() == \"{\" {\n\t\treturn d.Err(\"Unexpected token '{', expecting argument\")\n\t}\n\treturn d.Errf(\"Wrong argument count or unexpected line ending after '%s'\", d.Val())\n}\n\n// SyntaxErr creates a generic syntax error which explains what was\n// found and what was expected.\nfunc (d *Dispenser) SyntaxErr(expected string) error {\n\tmsg := fmt.Sprintf(\"%s:%d - Syntax error: Unexpected token '%s', expecting '%s'\", d.File(), d.Line(), d.Val(), expected)\n\treturn errors.New(msg)\n}\n\n// EOFErr returns an error indicating that the dispenser reached\n// the end of the input when searching for the next token.\nfunc (d *Dispenser) EOFErr() error {\n\treturn d.Errf(\"Unexpected EOF\")\n}\n\n// Err generates a custom parse-time error with a message of msg.\nfunc (d *Dispenser) Err(msg string) error {\n\tmsg = fmt.Sprintf(\"%s:%d - Error during parsing: %s\", d.File(), d.Line(), msg)\n\treturn errors.New(msg)\n}\n\n// Errf is like Err, but for formatted error messages\nfunc (d *Dispenser) Errf(format string, args ...interface{}) error {\n\treturn d.Err(fmt.Sprintf(format, args...))\n}\n\n// numLineBreaks counts how many line breaks are in the token\n// value given by the token index tknIdx. It returns 0 if the\n// token does not exist or there are no line breaks.\nfunc (d *Dispenser) numLineBreaks(tknIdx int) int {\n\tif tknIdx < 0 || tknIdx >= len(d.tokens) {\n\t\treturn 0\n\t}\n\treturn strings.Count(d.tokens[tknIdx].Text, \"\\n\")\n}\n"
  },
  {
    "path": "framework/config/lexer/dispenser_test.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\n// Copyright 2015 Light Code Labs, LLC\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage lexer\n\nimport (\n\t\"reflect\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestDispenser_Val_Next(t *testing.T) {\n\tinput := `host:port\n\t\t\t  dir1 arg1\n\t\t\t  dir2 arg2 arg3\n\t\t\t  dir3`\n\td := NewDispenser(\"Testfile\", strings.NewReader(input))\n\n\tif val := d.Val(); val != \"\" {\n\t\tt.Fatalf(\"Val(): Should return empty string when no token loaded; got '%s'\", val)\n\t}\n\n\tassertNext := func(shouldLoad bool, expectedCursor int, expectedVal string) {\n\t\tif loaded := d.Next(); loaded != shouldLoad {\n\t\t\tt.Errorf(\"Next(): Expected %v but got %v instead (val '%s')\", shouldLoad, loaded, d.Val())\n\t\t}\n\t\tif d.cursor != expectedCursor {\n\t\t\tt.Errorf(\"Expected cursor to be %d, but was %d\", expectedCursor, d.cursor)\n\t\t}\n\t\tif d.nesting != 0 {\n\t\t\tt.Errorf(\"Nesting should be 0, was %d instead\", d.nesting)\n\t\t}\n\t\tif val := d.Val(); val != expectedVal {\n\t\t\tt.Errorf(\"Val(): Expected '%s' but got '%s'\", expectedVal, val)\n\t\t}\n\t}\n\n\tassertNext(true, 0, \"host:port\")\n\tassertNext(true, 1, \"dir1\")\n\tassertNext(true, 2, \"arg1\")\n\tassertNext(true, 3, \"dir2\")\n\tassertNext(true, 4, \"arg2\")\n\tassertNext(true, 5, \"arg3\")\n\tassertNext(true, 6, \"dir3\")\n\t// Note: This next test simply asserts existing behavior.\n\t// If desired, we may wish to empty the token value after\n\t// reading past the EOF. Open an issue if you want this change.\n\tassertNext(false, 6, \"dir3\")\n}\n\nfunc TestDispenser_NextArg(t *testing.T) {\n\tinput := `dir1 arg1\n\t\t\t  dir2 arg2 arg3\n\t\t\t  dir3`\n\td := NewDispenser(\"Testfile\", strings.NewReader(input))\n\n\tassertNext := func(shouldLoad bool, expectedVal string, expectedCursor int) {\n\t\tif d.Next() != shouldLoad {\n\t\t\tt.Errorf(\"Next(): Should load token but got false instead (val: '%s')\", d.Val())\n\t\t}\n\t\tif d.cursor != expectedCursor {\n\t\t\tt.Errorf(\"Next(): Expected cursor to be at %d, but it was %d\", expectedCursor, d.cursor)\n\t\t}\n\t\tif val := d.Val(); val != expectedVal {\n\t\t\tt.Errorf(\"Val(): Expected '%s' but got '%s'\", expectedVal, val)\n\t\t}\n\t}\n\n\tassertNextArg := func(expectedVal string, loadAnother bool, expectedCursor int) {\n\t\tif !d.NextArg() {\n\t\t\tt.Error(\"NextArg(): Should load next argument but got false instead\")\n\t\t}\n\t\tif d.cursor != expectedCursor {\n\t\t\tt.Errorf(\"NextArg(): Expected cursor to be at %d, but it was %d\", expectedCursor, d.cursor)\n\t\t}\n\t\tif val := d.Val(); val != expectedVal {\n\t\t\tt.Errorf(\"Val(): Expected '%s' but got '%s'\", expectedVal, val)\n\t\t}\n\t\tif !loadAnother {\n\t\t\tif d.NextArg() {\n\t\t\t\tt.Fatalf(\"NextArg(): Should NOT load another argument, but got true instead (val: '%s')\", d.Val())\n\t\t\t}\n\t\t\tif d.cursor != expectedCursor {\n\t\t\t\tt.Errorf(\"NextArg(): Expected cursor to remain at %d, but it was %d\", expectedCursor, d.cursor)\n\t\t\t}\n\t\t}\n\t}\n\n\tassertNext(true, \"dir1\", 0)\n\tassertNextArg(\"arg1\", false, 1)\n\tassertNext(true, \"dir2\", 2)\n\tassertNextArg(\"arg2\", true, 3)\n\tassertNextArg(\"arg3\", false, 4)\n\tassertNext(true, \"dir3\", 5)\n\tassertNext(false, \"dir3\", 5)\n}\n\nfunc TestDispenser_NextLine(t *testing.T) {\n\tinput := `host:port\n\t\t\t  dir1 arg1\n\t\t\t  dir2 arg2 arg3`\n\td := NewDispenser(\"Testfile\", strings.NewReader(input))\n\n\tassertNextLine := func(shouldLoad bool, expectedVal string, expectedCursor int) {\n\t\tif d.NextLine() != shouldLoad {\n\t\t\tt.Errorf(\"NextLine(): Should load token but got false instead (val: '%s')\", d.Val())\n\t\t}\n\t\tif d.cursor != expectedCursor {\n\t\t\tt.Errorf(\"NextLine(): Expected cursor to be %d, instead was %d\", expectedCursor, d.cursor)\n\t\t}\n\t\tif val := d.Val(); val != expectedVal {\n\t\t\tt.Errorf(\"Val(): Expected '%s' but got '%s'\", expectedVal, val)\n\t\t}\n\t}\n\n\tassertNextLine(true, \"host:port\", 0)\n\tassertNextLine(true, \"dir1\", 1)\n\tassertNextLine(false, \"dir1\", 1)\n\td.Next() // arg1\n\tassertNextLine(true, \"dir2\", 3)\n\tassertNextLine(false, \"dir2\", 3)\n\td.Next() // arg2\n\tassertNextLine(false, \"arg2\", 4)\n\td.Next() // arg3\n\tassertNextLine(false, \"arg3\", 5)\n}\n\nfunc TestDispenser_NextBlock(t *testing.T) {\n\tinput := `foobar1 {\n\t\t\t  \tsub1 arg1\n\t\t\t  \tsub2\n\t\t\t  }\n\t\t\t  foobar2 {\n\t\t\t  }`\n\td := NewDispenser(\"Testfile\", strings.NewReader(input))\n\n\tassertNextBlock := func(shouldLoad bool, expectedCursor, expectedNesting int) {\n\t\tif loaded := d.NextBlock(); loaded != shouldLoad {\n\t\t\tt.Errorf(\"NextBlock(): Should return %v but got %v\", shouldLoad, loaded)\n\t\t}\n\t\tif d.cursor != expectedCursor {\n\t\t\tt.Errorf(\"NextBlock(): Expected cursor to be %d, was %d\", expectedCursor, d.cursor)\n\t\t}\n\t\tif d.nesting != expectedNesting {\n\t\t\tt.Errorf(\"NextBlock(): Nesting should be %d, not %d\", expectedNesting, d.nesting)\n\t\t}\n\t}\n\n\tassertNextBlock(false, -1, 0)\n\td.Next() // foobar1\n\tassertNextBlock(true, 2, 1)\n\tassertNextBlock(true, 3, 1)\n\tassertNextBlock(true, 4, 1)\n\tassertNextBlock(false, 5, 0)\n\td.Next()                     // foobar2\n\tassertNextBlock(false, 8, 0) // empty block is as if it didn't exist\n}\n\nfunc TestDispenser_Args(t *testing.T) {\n\tvar s1, s2, s3 string\n\tinput := `dir1 arg1 arg2 arg3\n\t\t\t  dir2 arg4 arg5\n\t\t\t  dir3 arg6 arg7\n\t\t\t  dir4`\n\td := NewDispenser(\"Testfile\", strings.NewReader(input))\n\n\td.Next() // dir1\n\n\t// As many strings as arguments\n\tif all := d.Args(&s1, &s2, &s3); !all {\n\t\tt.Error(\"Args(): Expected true, got false\")\n\t}\n\tif s1 != \"arg1\" {\n\t\tt.Errorf(\"Args(): Expected s1 to be 'arg1', got '%s'\", s1)\n\t}\n\tif s2 != \"arg2\" {\n\t\tt.Errorf(\"Args(): Expected s2 to be 'arg2', got '%s'\", s2)\n\t}\n\tif s3 != \"arg3\" {\n\t\tt.Errorf(\"Args(): Expected s3 to be 'arg3', got '%s'\", s3)\n\t}\n\n\td.Next() // dir2\n\n\t// More strings than arguments\n\tif all := d.Args(&s1, &s2, &s3); all {\n\t\tt.Error(\"Args(): Expected false, got true\")\n\t}\n\tif s1 != \"arg4\" {\n\t\tt.Errorf(\"Args(): Expected s1 to be 'arg4', got '%s'\", s1)\n\t}\n\tif s2 != \"arg5\" {\n\t\tt.Errorf(\"Args(): Expected s2 to be 'arg5', got '%s'\", s2)\n\t}\n\tif s3 != \"arg3\" {\n\t\tt.Errorf(\"Args(): Expected s3 to be unchanged ('arg3'), instead got '%s'\", s3)\n\t}\n\n\t// (quick cursor check just for kicks and giggles)\n\tif d.cursor != 6 {\n\t\tt.Errorf(\"Cursor should be 6, but is %d\", d.cursor)\n\t}\n\n\td.Next() // dir3\n\n\t// More arguments than strings\n\tif all := d.Args(&s1); !all {\n\t\tt.Error(\"Args(): Expected true, got false\")\n\t}\n\tif s1 != \"arg6\" {\n\t\tt.Errorf(\"Args(): Expected s1 to be 'arg6', got '%s'\", s1)\n\t}\n\n\td.Next() // dir4\n\n\t// No arguments or strings\n\tif all := d.Args(); !all {\n\t\tt.Error(\"Args(): Expected true, got false\")\n\t}\n\n\t// No arguments but at least one string\n\tif all := d.Args(&s1); all {\n\t\tt.Error(\"Args(): Expected false, got true\")\n\t}\n}\n\nfunc TestDispenser_RemainingArgs(t *testing.T) {\n\tinput := `dir1 arg1 arg2 arg3\n\t\t\t  dir2 arg4 arg5\n\t\t\t  dir3 arg6 { arg7\n\t\t\t  dir4`\n\td := NewDispenser(\"Testfile\", strings.NewReader(input))\n\n\td.Next() // dir1\n\n\targs := d.RemainingArgs()\n\tif expected := []string{\"arg1\", \"arg2\", \"arg3\"}; !reflect.DeepEqual(args, expected) {\n\t\tt.Errorf(\"RemainingArgs(): Expected %v, got %v\", expected, args)\n\t}\n\n\td.Next() // dir2\n\n\targs = d.RemainingArgs()\n\tif expected := []string{\"arg4\", \"arg5\"}; !reflect.DeepEqual(args, expected) {\n\t\tt.Errorf(\"RemainingArgs(): Expected %v, got %v\", expected, args)\n\t}\n\n\td.Next() // dir3\n\n\targs = d.RemainingArgs()\n\tif expected := []string{\"arg6\"}; !reflect.DeepEqual(args, expected) {\n\t\tt.Errorf(\"RemainingArgs(): Expected %v, got %v\", expected, args)\n\t}\n\n\td.Next() // {\n\td.Next() // arg7\n\td.Next() // dir4\n\n\targs = d.RemainingArgs()\n\tif len(args) != 0 {\n\t\tt.Errorf(\"RemainingArgs(): Expected %v, got %v\", []string{}, args)\n\t}\n}\n\nfunc TestDispenser_ArgErr_Err(t *testing.T) {\n\tinput := `dir1 {\n\t\t\t  }\n\t\t\t  dir2 arg1 arg2`\n\td := NewDispenser(\"Testfile\", strings.NewReader(input))\n\n\td.cursor = 1 // {\n\n\tif err := d.ArgErr(); err == nil || !strings.Contains(err.Error(), \"{\") {\n\t\tt.Errorf(\"ArgErr(): Expected an error message with { in it, but got '%v'\", err)\n\t}\n\n\td.cursor = 5 // arg2\n\n\tif err := d.ArgErr(); err == nil || !strings.Contains(err.Error(), \"arg2\") {\n\t\tt.Errorf(\"ArgErr(): Expected an error message with 'arg2' in it; got '%v'\", err)\n\t}\n\n\terr := d.Err(\"foobar\")\n\tif err == nil {\n\t\tt.Fatalf(\"Err(): Expected an error, got nil\")\n\t}\n\n\tif !strings.Contains(err.Error(), \"Testfile:3\") {\n\t\tt.Errorf(\"Expected error message with filename:line in it; got '%v'\", err)\n\t}\n\n\tif !strings.Contains(err.Error(), \"foobar\") {\n\t\tt.Errorf(\"Expected error message with custom message in it ('foobar'); got '%v'\", err)\n\t}\n}\n"
  },
  {
    "path": "framework/config/lexer/lexer.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\n// Copyright 2015 Light Code Labs, LLC\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage lexer\n\nimport (\n\t\"bufio\"\n\t\"io\"\n\t\"unicode\"\n)\n\ntype (\n\t// lexer is a utility which can get values, token by\n\t// token, from a Reader. A token is a word, and tokens\n\t// are separated by whitespace. A word can be enclosed\n\t// in quotes if it contains whitespace.\n\tlexer struct {\n\t\treader *bufio.Reader\n\t\ttoken  Token\n\t\tline   int\n\n\t\tlastErr error\n\t}\n\n\t// Token represents a single parsable unit.\n\tToken struct {\n\t\tFile string\n\t\tLine int\n\t\tText string\n\t}\n)\n\n// load prepares the lexer to scan an input for tokens.\n// It discards any leading byte order mark.\nfunc (l *lexer) load(input io.Reader) error {\n\tl.reader = bufio.NewReader(input)\n\tl.line = 1\n\n\t// discard byte order mark, if present\n\tfirstCh, _, err := l.reader.ReadRune()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif firstCh != 0xFEFF {\n\t\terr := l.reader.UnreadRune()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (l *lexer) err() error {\n\treturn l.lastErr\n}\n\n// next loads the next token into the lexer.\n//\n// A token is delimited by whitespace, unless the token starts with a quotes\n// character (\") in which case the token goes until the closing quotes (the\n// enclosing quotes are not included). Inside quoted strings, quotes may be\n// escaped with a preceding \\ character. No other chars may be escaped. Curly\n// braces ('{', '}') are emitted as a separate tokens.\n//\n// The rest of the line is skipped if a \"#\" character is read in.\n//\n// Returns true if a token was loaded; false otherwise. If read from\n// underlying Reader fails, next() returns false and err() will return the\n// error occurred.\nfunc (l *lexer) next() bool {\n\tvar val []rune\n\tvar comment, quoted, escaped bool\n\n\tmakeToken := func() bool {\n\t\tl.token.Text = string(val)\n\t\tl.lastErr = nil\n\t\treturn true\n\t}\n\n\tfor {\n\t\tch, _, err := l.reader.ReadRune()\n\t\tif err != nil {\n\t\t\tif len(val) > 0 {\n\t\t\t\treturn makeToken()\n\t\t\t}\n\t\t\tif err == io.EOF {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tl.lastErr = err\n\t\t\treturn false\n\t\t}\n\n\t\tif quoted {\n\t\t\tif !escaped {\n\t\t\t\tif ch == '\\\\' {\n\t\t\t\t\tescaped = true\n\t\t\t\t\tcontinue\n\t\t\t\t} else if ch == '\"' {\n\t\t\t\t\treturn makeToken()\n\t\t\t\t}\n\t\t\t}\n\t\t\tif ch == '\\n' {\n\t\t\t\tl.line++\n\t\t\t}\n\t\t\tif escaped {\n\t\t\t\t// only escape quotes\n\t\t\t\tif ch != '\"' {\n\t\t\t\t\tval = append(val, '\\\\')\n\t\t\t\t}\n\t\t\t}\n\t\t\tval = append(val, ch)\n\t\t\tescaped = false\n\t\t\tcontinue\n\t\t}\n\n\t\tif unicode.IsSpace(ch) {\n\t\t\tif ch == '\\r' {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif ch == '\\n' {\n\t\t\t\tl.line++\n\t\t\t\tcomment = false\n\t\t\t}\n\t\t\tif len(val) > 0 {\n\t\t\t\treturn makeToken()\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tif ch == '#' {\n\t\t\tcomment = true\n\t\t}\n\n\t\tif comment {\n\t\t\tcontinue\n\t\t}\n\n\t\tif len(val) == 0 {\n\t\t\tl.token = Token{Line: l.line}\n\t\t\tif ch == '\"' {\n\t\t\t\tquoted = true\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\tval = append(val, ch)\n\t}\n}\n"
  },
  {
    "path": "framework/config/lexer/lexer_test.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\n// Copyright 2015 Light Code Labs, LLC\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage lexer\n\nimport (\n\t\"log\"\n\t\"strings\"\n\t\"testing\"\n)\n\ntype lexerTestCase struct {\n\tinput    string\n\texpected []Token\n}\n\nfunc TestLexer(t *testing.T) {\n\ttestCases := []lexerTestCase{\n\t\t{\n\t\t\tinput: `host:123`,\n\t\t\texpected: []Token{\n\t\t\t\t{Line: 1, Text: \"host:123\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tinput: `host:123\n\n\t\t\t\t\tdirective`,\n\t\t\texpected: []Token{\n\t\t\t\t{Line: 1, Text: \"host:123\"},\n\t\t\t\t{Line: 3, Text: \"directive\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tinput: `host:123 {\n\t\t\t\t\t\tdirective\n\t\t\t\t\t}`,\n\t\t\texpected: []Token{\n\t\t\t\t{Line: 1, Text: \"host:123\"},\n\t\t\t\t{Line: 1, Text: \"{\"},\n\t\t\t\t{Line: 2, Text: \"directive\"},\n\t\t\t\t{Line: 3, Text: \"}\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tinput: `host:123 { directive }`,\n\t\t\texpected: []Token{\n\t\t\t\t{Line: 1, Text: \"host:123\"},\n\t\t\t\t{Line: 1, Text: \"{\"},\n\t\t\t\t{Line: 1, Text: \"directive\"},\n\t\t\t\t{Line: 1, Text: \"}\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tinput: `host:123 {\n\t\t\t\t\t\t#comment\n\t\t\t\t\t\tdirective\n\t\t\t\t\t\t# comment\n\t\t\t\t\t\tfoobar # another comment\n\t\t\t\t\t}`,\n\t\t\texpected: []Token{\n\t\t\t\t{Line: 1, Text: \"host:123\"},\n\t\t\t\t{Line: 1, Text: \"{\"},\n\t\t\t\t{Line: 3, Text: \"directive\"},\n\t\t\t\t{Line: 5, Text: \"foobar\"},\n\t\t\t\t{Line: 6, Text: \"}\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tinput: `a \"quoted value\" b\n\t\t\t\t\tfoobar`,\n\t\t\texpected: []Token{\n\t\t\t\t{Line: 1, Text: \"a\"},\n\t\t\t\t{Line: 1, Text: \"quoted value\"},\n\t\t\t\t{Line: 1, Text: \"b\"},\n\t\t\t\t{Line: 2, Text: \"foobar\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tinput: `A \"quoted \\\"value\\\" inside\" B`,\n\t\t\texpected: []Token{\n\t\t\t\t{Line: 1, Text: \"A\"},\n\t\t\t\t{Line: 1, Text: `quoted \"value\" inside`},\n\t\t\t\t{Line: 1, Text: \"B\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tinput: `\"don't\\escape\"`,\n\t\t\texpected: []Token{\n\t\t\t\t{Line: 1, Text: `don't\\escape`},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tinput: `\"don't\\\\escape\"`,\n\t\t\texpected: []Token{\n\t\t\t\t{Line: 1, Text: `don't\\\\escape`},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tinput: `A \"quoted value with line\n\t\t\t\t\tbreak inside\" {\n\t\t\t\t\t\tfoobar\n\t\t\t\t\t}`,\n\t\t\texpected: []Token{\n\t\t\t\t{Line: 1, Text: \"A\"},\n\t\t\t\t{Line: 1, Text: \"quoted value with line\\n\\t\\t\\t\\t\\tbreak inside\"},\n\t\t\t\t{Line: 2, Text: \"{\"},\n\t\t\t\t{Line: 3, Text: \"foobar\"},\n\t\t\t\t{Line: 4, Text: \"}\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tinput: `\"C:\\php\\php-cgi.exe\"`,\n\t\t\texpected: []Token{\n\t\t\t\t{Line: 1, Text: `C:\\php\\php-cgi.exe`},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tinput: `empty \"\" string`,\n\t\t\texpected: []Token{\n\t\t\t\t{Line: 1, Text: `empty`},\n\t\t\t\t{Line: 1, Text: ``},\n\t\t\t\t{Line: 1, Text: `string`},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tinput: \"skip those\\r\\nCR characters\",\n\t\t\texpected: []Token{\n\t\t\t\t{Line: 1, Text: \"skip\"},\n\t\t\t\t{Line: 1, Text: \"those\"},\n\t\t\t\t{Line: 2, Text: \"CR\"},\n\t\t\t\t{Line: 2, Text: \"characters\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tinput: \"\\xEF\\xBB\\xBF:8080\", // test with leading byte order mark\n\t\t\texpected: []Token{\n\t\t\t\t{Line: 1, Text: \":8080\"},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor i, testCase := range testCases {\n\t\tactual := tokenize(testCase.input)\n\t\tlexerCompare(t, i, testCase.expected, actual)\n\t}\n}\n\nfunc tokenize(input string) (tokens []Token) {\n\tl := lexer{}\n\tif err := l.load(strings.NewReader(input)); err != nil {\n\t\tlog.Printf(\"[ERROR] load failed: %v\", err)\n\t}\n\tfor l.next() {\n\t\ttokens = append(tokens, l.token)\n\t}\n\treturn\n}\n\nfunc lexerCompare(t *testing.T, n int, expected, actual []Token) {\n\tif len(expected) != len(actual) {\n\t\tt.Errorf(\"Test case %d: expected %d token(s) but got %d\", n, len(expected), len(actual))\n\t}\n\n\tfor i := 0; i < len(actual) && i < len(expected); i++ {\n\t\tif actual[i].Line != expected[i].Line {\n\t\t\tt.Errorf(\"Test case %d token %d ('%s'): expected line %d but was line %d\",\n\t\t\t\tn, i, expected[i].Text, expected[i].Line, actual[i].Line)\n\t\t\tbreak\n\t\t}\n\t\tif actual[i].Text != expected[i].Text {\n\t\t\tt.Errorf(\"Test case %d token %d: expected text '%s' but was '%s'\",\n\t\t\t\tn, i, expected[i].Text, actual[i].Text)\n\t\t\tbreak\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "framework/config/lexer/parse.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage lexer\n\nimport (\n\t\"io\"\n)\n\n// allTokens lexes the entire input, but does not parse it.\n// It returns all the tokens from the input, unstructured\n// and in order.\nfunc allTokens(input io.Reader) ([]Token, error) {\n\tl := new(lexer)\n\terr := l.load(input)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar tokens []Token\n\tfor l.next() {\n\t\ttokens = append(tokens, l.token)\n\t}\n\tif err := l.err(); err != nil {\n\t\treturn nil, err\n\t}\n\treturn tokens, nil\n}\n"
  },
  {
    "path": "framework/config/map.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage config\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"reflect\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\t\"unicode\"\n)\n\ntype matcher struct {\n\tname          string\n\trequired      bool\n\tinheritGlobal bool\n\tdefaultVal    func() (interface{}, error)\n\tmapper        func(*Map, Node) (interface{}, error)\n\tstore         *reflect.Value\n\n\tcustomCallback func(*Map, Node) error\n}\n\nfunc (m *matcher) assign(val interface{}) {\n\tvalRefl := reflect.ValueOf(val)\n\t// Convert untyped nil into typed nil. Otherwise it will panic.\n\tif !valRefl.IsValid() {\n\t\tvalRefl = reflect.Zero(m.store.Type())\n\t}\n\n\tm.store.Set(valRefl)\n}\n\n// Map structure implements reflection-based conversion between configuration\n// directives and Go variables.\ntype Map struct {\n\tallowUnknown bool\n\n\t// All values saved by Map during processing.\n\tValues map[string]interface{}\n\n\tentries map[string]matcher\n\n\t// Values used by Process as default values if inheritGlobal is true.\n\tGlobals map[string]interface{}\n\t// Config block used by Process.\n\tBlock Node\n}\n\nfunc NewMap(globals map[string]interface{}, block Node) *Map {\n\treturn &Map{Globals: globals, Block: block}\n}\n\n// AllowUnknown makes config.Map skip unknown configuration directives instead\n// of failing.\nfunc (m *Map) AllowUnknown() {\n\tm.allowUnknown = true\n}\n\n// EnumList maps a configuration directive to a []string variable.\n//\n// Directive must be in form 'name string1 string2' where each string should be from *allowed*\n// slice. At least one argument should be present.\n//\n// See Map.Custom for description of inheritGlobal and required.\nfunc (m *Map) EnumList(name string, inheritGlobal, required bool, allowed, defaultVal []string, store *[]string) {\n\tm.Custom(name, inheritGlobal, required, func() (interface{}, error) {\n\t\treturn defaultVal, nil\n\t}, func(_ *Map, node Node) (interface{}, error) {\n\t\tif len(node.Children) != 0 {\n\t\t\treturn nil, NodeErr(node, \"can't declare a block here\")\n\t\t}\n\t\tif len(node.Args) == 0 {\n\t\t\treturn nil, NodeErr(node, \"expected at least one argument\")\n\t\t}\n\n\t\tfor _, arg := range node.Args {\n\t\t\tisAllowed := false\n\t\t\tfor _, str := range allowed {\n\t\t\t\tif str == arg {\n\t\t\t\t\tisAllowed = true\n\t\t\t\t}\n\t\t\t}\n\t\t\tif !isAllowed {\n\t\t\t\treturn nil, NodeErr(node, \"invalid argument, valid values are: %v\", allowed)\n\t\t\t}\n\t\t}\n\n\t\treturn node.Args, nil\n\t}, store)\n}\n\n// Enum maps a configuration directive to a string variable.\n//\n// Directive must be in form 'name string' where string should be from *allowed*\n// slice. That string argument will be stored in store variable.\n//\n// See Map.Custom for description of inheritGlobal and required.\nfunc (m *Map) Enum(name string, inheritGlobal, required bool, allowed []string, defaultVal string, store *string) {\n\tm.Custom(name, inheritGlobal, required, func() (interface{}, error) {\n\t\treturn defaultVal, nil\n\t}, func(_ *Map, node Node) (interface{}, error) {\n\t\tif len(node.Children) != 0 {\n\t\t\treturn nil, NodeErr(node, \"can't declare a block here\")\n\t\t}\n\t\tif len(node.Args) != 1 {\n\t\t\treturn nil, NodeErr(node, \"expected exactly one argument\")\n\t\t}\n\n\t\tfor _, str := range allowed {\n\t\t\tif str == node.Args[0] {\n\t\t\t\treturn node.Args[0], nil\n\t\t\t}\n\t\t}\n\n\t\treturn nil, NodeErr(node, \"invalid argument, valid values are: %v\", allowed)\n\t}, store)\n}\n\n// EnumMapped is similar to Map.Enum but maps a stirng to a custom type.\nfunc EnumMapped[V any](m *Map, name string, inheritGlobal, required bool, mapped map[string]V, defaultVal V, store *V) {\n\tm.Custom(name, inheritGlobal, required, func() (interface{}, error) {\n\t\treturn defaultVal, nil\n\t}, func(_ *Map, node Node) (interface{}, error) {\n\t\tif len(node.Children) != 0 {\n\t\t\treturn nil, NodeErr(node, \"can't declare a block here\")\n\t\t}\n\t\tif len(node.Args) != 1 {\n\t\t\treturn nil, NodeErr(node, \"expected exactly one argument\")\n\t\t}\n\n\t\tval, ok := mapped[node.Args[0]]\n\t\tif !ok {\n\t\t\tvalidValues := make([]string, 0, len(mapped))\n\t\t\tfor k := range mapped {\n\t\t\t\tvalidValues = append(validValues, k)\n\t\t\t}\n\t\t\treturn nil, NodeErr(node, \"invalid argument, valid values are: %v\", validValues)\n\t\t}\n\n\t\treturn val, nil\n\t}, store)\n}\n\n// EnumListMapped is similar to Map.EnumList but maps a stirng to a custom type.\nfunc EnumListMapped[V any](m *Map, name string, inheritGlobal, required bool, mapped map[string]V, defaultVal []V, store *[]V) {\n\tm.Custom(name, inheritGlobal, required, func() (interface{}, error) {\n\t\treturn defaultVal, nil\n\t}, func(_ *Map, node Node) (interface{}, error) {\n\t\tif len(node.Children) != 0 {\n\t\t\treturn nil, NodeErr(node, \"can't declare a block here\")\n\t\t}\n\t\tif len(node.Args) == 0 {\n\t\t\treturn nil, NodeErr(node, \"expected at least one argument\")\n\t\t}\n\n\t\tvalues := make([]V, 0, len(node.Args))\n\t\tfor _, arg := range node.Args {\n\t\t\tval, ok := mapped[arg]\n\t\t\tif !ok {\n\t\t\t\tvalidValues := make([]string, 0, len(mapped))\n\t\t\t\tfor k := range mapped {\n\t\t\t\t\tvalidValues = append(validValues, k)\n\t\t\t\t}\n\t\t\t\treturn nil, NodeErr(node, \"invalid argument, valid values are: %v\", validValues)\n\t\t\t}\n\t\t\tvalues = append(values, val)\n\t\t}\n\t\treturn values, nil\n\t}, store)\n}\n\n// Duration maps configuration directive to a time.Duration variable.\n//\n// Directive must be in form 'name duration' where duration is any string accepted by\n// time.ParseDuration. As an additional requirement, result of time.ParseDuration must not\n// be negative.\n//\n// Note that for convenience, if directive does have multiple arguments, they will be joined\n// without separators. E.g. 'name 1h 2m' will become 'name 1h2m' and so '1h2m' will be passed\n// to time.ParseDuration.\n//\n// See Map.Custom for description of arguments.\nfunc (m *Map) Duration(name string, inheritGlobal, required bool, defaultVal time.Duration, store *time.Duration) {\n\tm.Custom(name, inheritGlobal, required, func() (interface{}, error) {\n\t\treturn defaultVal, nil\n\t}, func(_ *Map, node Node) (interface{}, error) {\n\t\tif len(node.Children) != 0 {\n\t\t\treturn nil, NodeErr(node, \"can't declare block here\")\n\t\t}\n\t\tif len(node.Args) == 0 {\n\t\t\treturn nil, NodeErr(node, \"at least one argument is required\")\n\t\t}\n\n\t\tdurationStr := strings.Join(node.Args, \"\")\n\t\tdur, err := time.ParseDuration(durationStr)\n\t\tif err != nil {\n\t\t\treturn nil, NodeErr(node, \"%v\", err)\n\t\t}\n\n\t\tif dur < 0 {\n\t\t\treturn nil, NodeErr(node, \"duration must not be negative\")\n\t\t}\n\n\t\treturn dur, nil\n\t}, store)\n}\n\nfunc ParseDataSize(s string) (int, error) {\n\tif len(s) == 0 {\n\t\treturn 0, errors.New(\"missing a number\")\n\t}\n\n\t// ' ' terminates the number+suffix pair.\n\ts = s + \" \"\n\n\tvar total int\n\tcurrentDigit := \"\"\n\tsuffix := \"\"\n\tfor _, ch := range s {\n\t\tif unicode.IsDigit(ch) {\n\t\t\tif suffix != \"\" {\n\t\t\t\treturn 0, errors.New(\"unexpected digit after a suffix\")\n\t\t\t}\n\t\t\tcurrentDigit += string(ch)\n\t\t\tcontinue\n\t\t}\n\t\tif ch != ' ' {\n\t\t\tsuffix += string(ch)\n\t\t\tcontinue\n\t\t}\n\n\t\tnum, err := strconv.Atoi(currentDigit)\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\n\t\tif num < 0 {\n\t\t\treturn 0, errors.New(\"value must not be negative\")\n\t\t}\n\n\t\tswitch suffix {\n\t\tcase \"G\":\n\t\t\ttotal += num * 1024 * 1024 * 1024\n\t\tcase \"M\":\n\t\t\ttotal += num * 1024 * 1024\n\t\tcase \"K\":\n\t\t\ttotal += num * 1024\n\t\tcase \"B\", \"b\":\n\t\t\ttotal += num\n\t\tdefault:\n\t\t\tif num != 0 {\n\t\t\t\treturn 0, errors.New(\"unknown unit suffix: \" + suffix)\n\t\t\t}\n\t\t}\n\n\t\tsuffix = \"\"\n\t\tcurrentDigit = \"\"\n\t}\n\n\treturn total, nil\n}\n\n// DataSize maps configuration directive to a int variable, representing data size.\n//\n// Syntax requires unit suffix to be added to the end of string to specify\n// data unit and allows multiple arguments (they will be added together).\n//\n// See Map.Custom for description of arguments.\nfunc (m *Map) DataSize(name string, inheritGlobal, required bool, defaultVal int64, store *int64) {\n\tm.Custom(name, inheritGlobal, required, func() (interface{}, error) {\n\t\treturn defaultVal, nil\n\t}, func(_ *Map, node Node) (interface{}, error) {\n\t\tif len(node.Children) != 0 {\n\t\t\treturn nil, NodeErr(node, \"can't declare block here\")\n\t\t}\n\t\tif len(node.Args) == 0 {\n\t\t\treturn nil, NodeErr(node, \"at least one argument is required\")\n\t\t}\n\n\t\tdurationStr := strings.Join(node.Args, \" \")\n\t\tdur, err := ParseDataSize(durationStr)\n\t\tif err != nil {\n\t\t\treturn nil, NodeErr(node, \"%v\", err)\n\t\t}\n\n\t\treturn int64(dur), nil\n\t}, store)\n}\n\nfunc ParseBool(s string) (bool, error) {\n\tswitch strings.ToLower(s) {\n\tcase \"1\", \"true\", \"on\", \"yes\":\n\t\treturn true, nil\n\tcase \"0\", \"false\", \"off\", \"no\":\n\t\treturn false, nil\n\t}\n\treturn false, fmt.Errorf(\"bool argument should be 'yes' or 'no'\")\n}\n\n// Bool maps presence of some configuration directive to a boolean variable.\n// Additionally, 'name yes' and 'name no' are mapped to true and false\n// correspondingly.\n//\n// I.e. if directive 'io_debug' exists in processed configuration block or in\n// the global configuration (if inheritGlobal is true) then Process will store\n// true in target variable.\nfunc (m *Map) Bool(name string, inheritGlobal, defaultVal bool, store *bool) {\n\tm.Custom(name, inheritGlobal, false, func() (interface{}, error) {\n\t\treturn defaultVal, nil\n\t}, func(_ *Map, node Node) (interface{}, error) {\n\t\tif len(node.Children) != 0 {\n\t\t\treturn nil, NodeErr(node, \"can't declare block here\")\n\t\t}\n\n\t\tif len(node.Args) == 0 {\n\t\t\treturn true, nil\n\t\t}\n\t\tif len(node.Args) != 1 {\n\t\t\treturn nil, NodeErr(node, \"expected exactly 1 argument\")\n\t\t}\n\n\t\tb, err := ParseBool(node.Args[0])\n\t\tif err != nil {\n\t\t\treturn nil, NodeErr(node, \"bool argument should be 'yes' or 'no'\")\n\t\t}\n\t\treturn b, nil\n\t}, store)\n}\n\n// StringList maps configuration directive with the specified name to variable\n// referenced by 'store' pointer.\n//\n// Configuration directive must be in form 'name arbitrary_string arbitrary_string ...'\n// Where at least one argument must be present.\n//\n// See Custom function for details about inheritGlobal, required and\n// defaultVal.\nfunc (m *Map) StringList(name string, inheritGlobal, required bool, defaultVal []string, store *[]string) {\n\tm.Custom(name, inheritGlobal, required, func() (interface{}, error) {\n\t\treturn defaultVal, nil\n\t}, func(_ *Map, node Node) (interface{}, error) {\n\t\tif len(node.Args) == 0 {\n\t\t\treturn nil, NodeErr(node, \"expected at least one argument\")\n\t\t}\n\t\tif len(node.Children) != 0 {\n\t\t\treturn nil, NodeErr(node, \"can't declare block here\")\n\t\t}\n\n\t\treturn node.Args, nil\n\t}, store)\n}\n\n// String maps configuration directive with the specified name to variable\n// referenced by 'store' pointer.\n//\n// Configuration directive must be in form 'name arbitrary_string'.\n//\n// See Custom function for details about inheritGlobal, required and\n// defaultVal.\nfunc (m *Map) String(name string, inheritGlobal, required bool, defaultVal string, store *string) {\n\tm.Custom(name, inheritGlobal, required, func() (interface{}, error) {\n\t\treturn defaultVal, nil\n\t}, func(_ *Map, node Node) (interface{}, error) {\n\t\tif len(node.Args) != 1 {\n\t\t\treturn nil, NodeErr(node, \"expected 1 argument\")\n\t\t}\n\t\tif len(node.Children) != 0 {\n\t\t\treturn nil, NodeErr(node, \"can't declare block here\")\n\t\t}\n\n\t\treturn node.Args[0], nil\n\t}, store)\n}\n\n// Int maps configuration directive with the specified name to variable\n// referenced by 'store' pointer.\n//\n// Configuration directive must be in form 'name 123'.\n//\n// See Custom function for details about inheritGlobal, required and\n// defaultVal.\nfunc (m *Map) Int(name string, inheritGlobal, required bool, defaultVal int, store *int) {\n\tm.Custom(name, inheritGlobal, required, func() (interface{}, error) {\n\t\treturn defaultVal, nil\n\t}, func(_ *Map, node Node) (interface{}, error) {\n\t\tif len(node.Args) != 1 {\n\t\t\treturn nil, NodeErr(node, \"expected 1 argument\")\n\t\t}\n\t\tif len(node.Children) != 0 {\n\t\t\treturn nil, NodeErr(node, \"can't declare block here\")\n\t\t}\n\n\t\ti, err := strconv.Atoi(node.Args[0])\n\t\tif err != nil {\n\t\t\treturn nil, NodeErr(node, \"invalid integer: %s\", node.Args[0])\n\t\t}\n\t\treturn i, nil\n\t}, store)\n}\n\n// UInt maps configuration directive with the specified name to variable\n// referenced by 'store' pointer.\n//\n// Configuration directive must be in form 'name 123'.\n//\n// See Custom function for details about inheritGlobal, required and\n// defaultVal.\nfunc (m *Map) UInt(name string, inheritGlobal, required bool, defaultVal uint, store *uint) {\n\tm.Custom(name, inheritGlobal, required, func() (interface{}, error) {\n\t\treturn defaultVal, nil\n\t}, func(_ *Map, node Node) (interface{}, error) {\n\t\tif len(node.Args) != 1 {\n\t\t\treturn nil, NodeErr(node, \"expected 1 argument\")\n\t\t}\n\t\tif len(node.Children) != 0 {\n\t\t\treturn nil, NodeErr(node, \"can't declare block here\")\n\t\t}\n\n\t\ti, err := strconv.ParseUint(node.Args[0], 10, 32)\n\t\tif err != nil {\n\t\t\treturn nil, NodeErr(node, \"invalid integer: %s\", node.Args[0])\n\t\t}\n\t\treturn uint(i), nil\n\t}, store)\n}\n\n// Int32 maps configuration directive with the specified name to variable\n// referenced by 'store' pointer.\n//\n// Configuration directive must be in form 'name 123'.\n//\n// See Custom function for details about inheritGlobal, required and\n// defaultVal.\nfunc (m *Map) Int32(name string, inheritGlobal, required bool, defaultVal int32, store *int32) {\n\tm.Custom(name, inheritGlobal, required, func() (interface{}, error) {\n\t\treturn defaultVal, nil\n\t}, func(_ *Map, node Node) (interface{}, error) {\n\t\tif len(node.Args) != 1 {\n\t\t\treturn nil, NodeErr(node, \"expected 1 argument\")\n\t\t}\n\t\tif len(node.Children) != 0 {\n\t\t\treturn nil, NodeErr(node, \"can't declare block here\")\n\t\t}\n\n\t\ti, err := strconv.ParseInt(node.Args[0], 10, 32)\n\t\tif err != nil {\n\t\t\treturn nil, NodeErr(node, \"invalid integer: %s\", node.Args[0])\n\t\t}\n\t\treturn int32(i), nil\n\t}, store)\n}\n\n// UInt32 maps configuration directive with the specified name to variable\n// referenced by 'store' pointer.\n//\n// Configuration directive must be in form 'name 123'.\n//\n// See Custom function for details about inheritGlobal, required and\n// defaultVal.\nfunc (m *Map) UInt32(name string, inheritGlobal, required bool, defaultVal uint32, store *uint32) {\n\tm.Custom(name, inheritGlobal, required, func() (interface{}, error) {\n\t\treturn defaultVal, nil\n\t}, func(_ *Map, node Node) (interface{}, error) {\n\t\tif len(node.Args) != 1 {\n\t\t\treturn nil, NodeErr(node, \"expected 1 argument\")\n\t\t}\n\t\tif len(node.Children) != 0 {\n\t\t\treturn nil, NodeErr(node, \"can't declare block here\")\n\t\t}\n\n\t\ti, err := strconv.ParseUint(node.Args[0], 10, 32)\n\t\tif err != nil {\n\t\t\treturn nil, NodeErr(node, \"invalid integer: %s\", node.Args[0])\n\t\t}\n\t\treturn uint32(i), nil\n\t}, store)\n}\n\n// Int64 maps configuration directive with the specified name to variable\n// referenced by 'store' pointer.\n//\n// Configuration directive must be in form 'name 123'.\n//\n// See Custom function for details about inheritGlobal, required and\n// defaultVal.\nfunc (m *Map) Int64(name string, inheritGlobal, required bool, defaultVal int64, store *int64) {\n\tm.Custom(name, inheritGlobal, required, func() (interface{}, error) {\n\t\treturn defaultVal, nil\n\t}, func(_ *Map, node Node) (interface{}, error) {\n\t\tif len(node.Args) != 1 {\n\t\t\treturn nil, NodeErr(node, \"expected 1 argument\")\n\t\t}\n\t\tif len(node.Children) != 0 {\n\t\t\treturn nil, NodeErr(node, \"can't declare block here\")\n\t\t}\n\n\t\ti, err := strconv.ParseInt(node.Args[0], 10, 64)\n\t\tif err != nil {\n\t\t\treturn nil, NodeErr(node, \"invalid integer: %s\", node.Args[0])\n\t\t}\n\t\treturn i, nil\n\t}, store)\n}\n\n// UInt64 maps configuration directive with the specified name to variable\n// referenced by 'store' pointer.\n//\n// Configuration directive must be in form 'name 123'.\n//\n// See Custom function for details about inheritGlobal, required and\n// defaultVal.\nfunc (m *Map) UInt64(name string, inheritGlobal, required bool, defaultVal uint64, store *uint64) {\n\tm.Custom(name, inheritGlobal, required, func() (interface{}, error) {\n\t\treturn defaultVal, nil\n\t}, func(_ *Map, node Node) (interface{}, error) {\n\t\tif len(node.Args) != 1 {\n\t\t\treturn nil, NodeErr(node, \"expected 1 argument\")\n\t\t}\n\t\tif len(node.Children) != 0 {\n\t\t\treturn nil, NodeErr(node, \"can't declare block here\")\n\t\t}\n\n\t\ti, err := strconv.ParseUint(node.Args[0], 10, 64)\n\t\tif err != nil {\n\t\t\treturn nil, NodeErr(node, \"invalid integer: %s\", node.Args[0])\n\t\t}\n\t\treturn i, nil\n\t}, store)\n}\n\n// Float maps configuration directive with the specified name to variable\n// referenced by 'store' pointer.\n//\n// Configuration directive must be in form 'name 123.55'.\n//\n// See Custom function for details about inheritGlobal, required and\n// defaultVal.\nfunc (m *Map) Float(name string, inheritGlobal, required bool, defaultVal float64, store *float64) {\n\tm.Custom(name, inheritGlobal, required, func() (interface{}, error) {\n\t\treturn defaultVal, nil\n\t}, func(_ *Map, node Node) (interface{}, error) {\n\t\tif len(node.Args) != 1 {\n\t\t\treturn nil, NodeErr(node, \"expected 1 argument\")\n\t\t}\n\n\t\tf, err := strconv.ParseFloat(node.Args[0], 64)\n\t\tif err != nil {\n\t\t\treturn nil, NodeErr(node, \"invalid float: %s\", node.Args[0])\n\t\t}\n\t\treturn f, nil\n\t}, store)\n}\n\n// Custom maps configuration directive with the specified name to variable\n// referenced by 'store' pointer.\n//\n// If inheritGlobal is true - Map will try to use a value from globalCfg if\n// none is set in a processed configuration block.\n//\n// If required is true - Map will fail if no value is set in the configuration,\n// both global (if inheritGlobal is true) and in the processed block.\n//\n// defaultVal is a factory function that should return the default value for\n// the variable. It will be used if no value is set in the config. It can be\n// nil if required is true.\n// Note that if inheritGlobal is true, defaultVal of the global directive\n// will be used instead.\n//\n// mapper is a function that should convert configuration directive arguments\n// into variable value.  Both functions may fail with errors, configuration\n// processing will stop immediately then.\n// Note: mapper function should not modify passed values.\n//\n// store is where the value returned by mapper should be stored. Can be nil\n// (value will be saved only in Map.Values).\nfunc (m *Map) Custom(name string, inheritGlobal, required bool, defaultVal func() (interface{}, error), mapper func(*Map, Node) (interface{}, error), store interface{}) {\n\tif m.entries == nil {\n\t\tm.entries = make(map[string]matcher)\n\t}\n\tif _, ok := m.entries[name]; ok {\n\t\tpanic(\"Map.Custom: duplicate matcher\")\n\t}\n\n\tvar target *reflect.Value\n\tptr := reflect.ValueOf(store)\n\tif ptr.IsValid() && !ptr.IsNil() {\n\t\tval := ptr.Elem()\n\t\tif !val.CanSet() {\n\t\t\tpanic(\"Map.Custom: store argument must be settable (a pointer)\")\n\t\t}\n\t\ttarget = &val\n\t}\n\n\tm.entries[name] = matcher{\n\t\tname:          name,\n\t\tinheritGlobal: inheritGlobal,\n\t\trequired:      required,\n\t\tdefaultVal:    defaultVal,\n\t\tmapper:        mapper,\n\t\tstore:         target,\n\t}\n}\n\n// Callback creates mapping that will call mapper() function for each\n// directive with the specified name. No further processing is done.\n//\n// Directives with the specified name will not be returned by Process if\n// AllowUnknown is used.\n//\n// It is intended to permit multiple independent values of directive with\n// implementation-defined handling.\nfunc (m *Map) Callback(name string, mapper func(*Map, Node) error) {\n\tif m.entries == nil {\n\t\tm.entries = make(map[string]matcher)\n\t}\n\tif _, ok := m.entries[name]; ok {\n\t\tpanic(\"Map.Custom: duplicate matcher\")\n\t}\n\n\tm.entries[name] = matcher{\n\t\tname:           name,\n\t\tcustomCallback: mapper,\n\t}\n}\n\n// Process maps variables from global configuration and block passed in NewMap.\n//\n// If Map instance was not created using NewMap - Process panics.\nfunc (m *Map) Process() (unknown []Node, err error) {\n\treturn m.ProcessWith(m.Globals, m.Block)\n}\n\n// Process maps variables from global configuration and block passed in arguments.\nfunc (m *Map) ProcessWith(globalCfg map[string]interface{}, block Node) (unknown []Node, err error) {\n\tunknown = make([]Node, 0, len(block.Children))\n\tmatched := make(map[string]bool)\n\tm.Values = make(map[string]interface{})\n\n\tfor _, subnode := range block.Children {\n\t\tmatcher, ok := m.entries[subnode.Name]\n\t\tif !ok {\n\t\t\tif !m.allowUnknown {\n\t\t\t\treturn nil, NodeErr(subnode, \"unexpected directive: %s\", subnode.Name)\n\t\t\t}\n\t\t\tunknown = append(unknown, subnode)\n\t\t\tcontinue\n\t\t}\n\n\t\tif matcher.customCallback != nil {\n\t\t\tif err := matcher.customCallback(m, subnode); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tmatched[subnode.Name] = true\n\t\t\tcontinue\n\t\t}\n\n\t\tif matched[subnode.Name] {\n\t\t\treturn nil, NodeErr(subnode, \"duplicate directive: %s\", subnode.Name)\n\t\t}\n\t\tmatched[subnode.Name] = true\n\n\t\tval, err := matcher.mapper(m, subnode)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tm.Values[matcher.name] = val\n\t\tif matcher.store != nil {\n\t\t\tmatcher.assign(val)\n\t\t}\n\t}\n\n\tfor _, matcher := range m.entries {\n\t\tif matched[matcher.name] {\n\t\t\tcontinue\n\t\t}\n\t\tif matcher.mapper == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tvar val interface{}\n\t\tglobalVal, ok := globalCfg[matcher.name]\n\t\tif matcher.inheritGlobal && ok {\n\t\t\tval = globalVal\n\t\t} else if !matcher.required {\n\t\t\tif matcher.defaultVal == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tval, err = matcher.defaultVal()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t} else {\n\t\t\treturn nil, NodeErr(block, \"missing required directive: %s\", matcher.name)\n\t\t}\n\n\t\t// If we put zero values into map then code that checks globalCfg\n\t\t// above will inherit them for required fields instead of failing.\n\t\t//\n\t\t// This is important for fields that are required to be specified\n\t\t// either globally or on per-block basis (e.g. tls, hostname).\n\t\t// For these directives, global Map does have required = false\n\t\t// so global values are default which is usually zero value.\n\t\t//\n\t\t// This is a temporary solutions, of course, in the long-term\n\t\t// the way global values and \"inheritance\" is handled should be\n\t\t// revised.\n\t\tstore := false\n\t\tvalT := reflect.TypeOf(val)\n\t\tif valT != nil {\n\t\t\tzero := reflect.Zero(valT)\n\t\t\tstore = !reflect.DeepEqual(val, zero.Interface())\n\t\t}\n\n\t\tif store {\n\t\t\tm.Values[matcher.name] = val\n\t\t}\n\t\tif matcher.store != nil {\n\t\t\tmatcher.assign(val)\n\t\t}\n\t}\n\n\treturn unknown, nil\n}\n"
  },
  {
    "path": "framework/config/map_test.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage config\n\nimport (\n\t\"testing\"\n)\n\nfunc TestMapProcess(t *testing.T) {\n\tcfg := Node{\n\t\tChildren: []Node{\n\t\t\t{\n\t\t\t\tName: \"foo\",\n\t\t\t\tArgs: []string{\"bar\"},\n\t\t\t},\n\t\t},\n\t}\n\n\tm := NewMap(nil, cfg)\n\n\tfoo := \"\"\n\tm.Custom(\"foo\", false, true, nil, func(_ *Map, n Node) (interface{}, error) {\n\t\treturn n.Args[0], nil\n\t}, &foo)\n\n\t_, err := m.Process()\n\tif err != nil {\n\t\tt.Fatalf(\"Unexpected failure: %v\", err)\n\t}\n\n\tif foo != \"bar\" {\n\t\tt.Errorf(\"Incorrect value stored in variable, want 'bar', got '%s'\", foo)\n\t}\n}\n\nfunc TestMapProcess_MissingRequired(t *testing.T) {\n\tcfg := Node{\n\t\tChildren: []Node{},\n\t}\n\n\tm := NewMap(nil, cfg)\n\n\tfoo := \"\"\n\tm.Custom(\"foo\", false, true, nil, func(_ *Map, n Node) (interface{}, error) {\n\t\treturn n.Args[0], nil\n\t}, &foo)\n\n\t_, err := m.Process()\n\tif err == nil {\n\t\tt.Errorf(\"Expected failure\")\n\t}\n}\n\nfunc TestMapProcess_InheritGlobal(t *testing.T) {\n\tcfg := Node{\n\t\tChildren: []Node{},\n\t}\n\n\tm := NewMap(map[string]interface{}{\"foo\": \"bar\"}, cfg)\n\n\tfoo := \"\"\n\tm.Custom(\"foo\", true, true, nil, func(_ *Map, n Node) (interface{}, error) {\n\t\treturn n.Args[0], nil\n\t}, &foo)\n\n\t_, err := m.Process()\n\tif err != nil {\n\t\tt.Fatalf(\"Unexpected failure: %v\", err)\n\t}\n\n\tif foo != \"bar\" {\n\t\tt.Errorf(\"Incorrect value stored in variable, want 'bar', got '%s'\", foo)\n\t}\n}\n\nfunc TestMapProcess_InheritGlobal_MissingRequired(t *testing.T) {\n\tcfg := Node{\n\t\tChildren: []Node{},\n\t}\n\n\tm := NewMap(map[string]interface{}{}, cfg)\n\n\tfoo := \"\"\n\tm.Custom(\"foo\", false, true, nil, func(_ *Map, n Node) (interface{}, error) {\n\t\treturn n.Args[0], nil\n\t}, &foo)\n\n\t_, err := m.Process()\n\tif err == nil {\n\t\tt.Errorf(\"Expected failure\")\n\t}\n}\n\nfunc TestMapProcess_InheritGlobal_Override(t *testing.T) {\n\tcfg := Node{\n\t\tChildren: []Node{\n\t\t\t{\n\t\t\t\tName: \"foo\",\n\t\t\t\tArgs: []string{\"bar\"},\n\t\t\t},\n\t\t},\n\t}\n\n\tm := NewMap(map[string]interface{}{}, cfg)\n\n\tfoo := \"\"\n\tm.Custom(\"foo\", false, true, nil, func(_ *Map, n Node) (interface{}, error) {\n\t\treturn n.Args[0], nil\n\t}, &foo)\n\n\t_, err := m.Process()\n\tif err != nil {\n\t\tt.Fatalf(\"Unexpected failure: %v\", err)\n\t}\n\n\tif foo != \"bar\" {\n\t\tt.Errorf(\"Incorrect value stored in variable, want 'bar', got '%s'\", foo)\n\t}\n}\n\nfunc TestMapProcess_DefaultValue(t *testing.T) {\n\tcfg := Node{\n\t\tChildren: []Node{},\n\t}\n\n\tm := NewMap(nil, cfg)\n\n\tfoo := \"\"\n\tm.Custom(\"foo\", false, false, func() (interface{}, error) {\n\t\treturn \"bar\", nil\n\t}, func(_ *Map, n Node) (interface{}, error) {\n\t\treturn n.Args[0], nil\n\t}, &foo)\n\n\t_, err := m.Process()\n\tif err != nil {\n\t\tt.Fatalf(\"Unexpected failure: %v\", err)\n\t}\n\n\tif foo != \"bar\" {\n\t\tt.Errorf(\"Incorrect value stored in variable, want 'bar', got '%s'\", foo)\n\t}\n}\n\nfunc TestMapProcess_InheritGlobal_DefaultValue(t *testing.T) {\n\tcfg := Node{\n\t\tChildren: []Node{},\n\t}\n\n\tm := NewMap(map[string]interface{}{\"foo\": \"baz\"}, cfg)\n\n\tfoo := \"\"\n\tm.Custom(\"foo\", true, false, func() (interface{}, error) {\n\t\treturn \"bar\", nil\n\t}, func(_ *Map, n Node) (interface{}, error) {\n\t\treturn n.Args[0], nil\n\t}, &foo)\n\n\t_, err := m.Process()\n\tif err != nil {\n\t\tt.Fatalf(\"Unexpected failure: %v\", err)\n\t}\n\n\tif foo != \"baz\" {\n\t\tt.Errorf(\"Incorrect value stored in variable, want 'baz', got '%s'\", foo)\n\t}\n\n\tt.Run(\"no global\", func(t *testing.T) {\n\t\t_, err := m.ProcessWith(map[string]interface{}{}, cfg)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"Unexpected failure: %v\", err)\n\t\t}\n\n\t\tif foo != \"bar\" {\n\t\t\tt.Errorf(\"Incorrect value stored in variable, want 'bar', got '%s'\", foo)\n\t\t}\n\t})\n}\n\nfunc TestMapProcess_Duplicate(t *testing.T) {\n\tcfg := Node{\n\t\tChildren: []Node{\n\t\t\t{\n\t\t\t\tName: \"foo\",\n\t\t\t\tArgs: []string{\"bar\"},\n\t\t\t},\n\t\t\t{\n\t\t\t\tName: \"foo\",\n\t\t\t\tArgs: []string{\"bar\"},\n\t\t\t},\n\t\t},\n\t}\n\n\tm := NewMap(nil, cfg)\n\n\tfoo := \"\"\n\tm.Custom(\"foo\", false, true, nil, func(_ *Map, n Node) (interface{}, error) {\n\t\treturn n.Args[0], nil\n\t}, &foo)\n\n\t_, err := m.Process()\n\tif err == nil {\n\t\tt.Errorf(\"Expected failure\")\n\t}\n}\n\nfunc TestMapProcess_Unexpected(t *testing.T) {\n\tcfg := Node{\n\t\tChildren: []Node{\n\t\t\t{\n\t\t\t\tName: \"foo\",\n\t\t\t\tArgs: []string{\"baz\"},\n\t\t\t},\n\t\t\t{\n\t\t\t\tName: \"bar\",\n\t\t\t\tArgs: []string{\"baz\"},\n\t\t\t},\n\t\t},\n\t}\n\n\tm := NewMap(nil, cfg)\n\n\tfoo := \"\"\n\tm.Custom(\"bar\", false, true, nil, func(_ *Map, n Node) (interface{}, error) {\n\t\treturn n.Args[0], nil\n\t}, &foo)\n\n\t_, err := m.Process()\n\tif err == nil {\n\t\tt.Errorf(\"Expected failure\")\n\t}\n\n\tm.AllowUnknown()\n\n\tunknown, err := m.Process()\n\tif err != nil {\n\t\tt.Errorf(\"Unexpected failure: %v\", err)\n\t}\n\n\tif len(unknown) != 1 {\n\t\tt.Fatalf(\"Wrong amount of unknown nodes: %v\", len(unknown))\n\t}\n\n\tif unknown[0].Name != \"foo\" {\n\t\tt.Fatalf(\"Wrong node in unknown: %v\", unknown[0].Name)\n\t}\n}\n\nfunc TestMapInt(t *testing.T) {\n\tcfg := Node{\n\t\tChildren: []Node{\n\t\t\t{\n\t\t\t\tName: \"foo\",\n\t\t\t\tArgs: []string{\"1\"},\n\t\t\t},\n\t\t},\n\t}\n\n\tm := NewMap(nil, cfg)\n\n\tfoo := 0\n\tm.Int(\"foo\", false, true, 0, &foo)\n\n\t_, err := m.Process()\n\tif err != nil {\n\t\tt.Fatalf(\"Unexpected failure: %v\", err)\n\t}\n\n\tif foo != 1 {\n\t\tt.Errorf(\"Incorrect value stored in variable, want 1, got %d\", foo)\n\t}\n}\n\nfunc TestMapInt_Invalid(t *testing.T) {\n\tcfg := Node{\n\t\tChildren: []Node{\n\t\t\t{\n\t\t\t\tName: \"foo\",\n\t\t\t\tArgs: []string{\"AAAA\"},\n\t\t\t},\n\t\t},\n\t}\n\n\tm := NewMap(nil, cfg)\n\n\tfoo := 0\n\tm.Int(\"foo\", false, true, 0, &foo)\n\n\t_, err := m.Process()\n\tif err == nil {\n\t\tt.Errorf(\"Expected failure\")\n\t}\n}\n\nfunc TestMapFloat(t *testing.T) {\n\tcfg := Node{\n\t\tChildren: []Node{\n\t\t\t{\n\t\t\t\tName: \"foo\",\n\t\t\t\tArgs: []string{\"1\"},\n\t\t\t},\n\t\t},\n\t}\n\n\tm := NewMap(nil, cfg)\n\n\tfoo := 0.0\n\tm.Float(\"foo\", false, true, 0, &foo)\n\n\t_, err := m.Process()\n\tif err != nil {\n\t\tt.Fatalf(\"Unexpected failure: %v\", err)\n\t}\n\n\tif foo != 1.0 {\n\t\tt.Errorf(\"Incorrect value stored in variable, want 1, got %v\", foo)\n\t}\n}\n\nfunc TestMapFloat_Invalid(t *testing.T) {\n\tcfg := Node{\n\t\tChildren: []Node{\n\t\t\t{\n\t\t\t\tName: \"foo\",\n\t\t\t\tArgs: []string{\"AAAA\"},\n\t\t\t},\n\t\t},\n\t}\n\n\tm := NewMap(nil, cfg)\n\n\tfoo := 0.0\n\tm.Float(\"foo\", false, true, 0, &foo)\n\n\t_, err := m.Process()\n\tif err == nil {\n\t\tt.Errorf(\"Expected failure\")\n\t}\n}\n\nfunc TestMapBool(t *testing.T) {\n\tcfg := Node{\n\t\tChildren: []Node{\n\t\t\t{\n\t\t\t\tName: \"foo\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tName: \"bar\",\n\t\t\t\tArgs: []string{\"yes\"},\n\t\t\t},\n\t\t\t{\n\t\t\t\tName: \"baz\",\n\t\t\t\tArgs: []string{\"no\"},\n\t\t\t},\n\t\t},\n\t}\n\n\tm := NewMap(nil, cfg)\n\n\tfoo, bar, baz, boo := false, false, false, false\n\tm.Bool(\"foo\", false, false, &foo)\n\tm.Bool(\"bar\", false, false, &bar)\n\tm.Bool(\"baz\", false, false, &baz)\n\tm.Bool(\"boo\", false, false, &boo)\n\n\t_, err := m.Process()\n\tif err != nil {\n\t\tt.Fatalf(\"Unexpected failure: %v\", err)\n\t}\n\n\tif !foo {\n\t\tt.Errorf(\"Incorrect value stored in variable foo, want true, got false\")\n\t}\n\tif !bar {\n\t\tt.Errorf(\"Incorrect value stored in variable bar, want true, got false\")\n\t}\n\tif baz {\n\t\tt.Errorf(\"Incorrect value stored in variable baz, want false, got true\")\n\t}\n\tif boo {\n\t\tt.Errorf(\"Incorrect value stored in variable boo, want false, got true\")\n\t}\n}\n\nfunc TestParseDataSize(t *testing.T) {\n\tcheck := func(s string, ok bool, expected int) {\n\t\tval, err := ParseDataSize(s)\n\t\tif err != nil && ok {\n\t\t\tt.Errorf(\"unexpected parseDataSize('%s') fail: %v\", s, err)\n\t\t\treturn\n\t\t}\n\t\tif err == nil && !ok {\n\t\t\tt.Errorf(\"unexpected parseDataSize('%s') success, got %d\", s, val)\n\t\t\treturn\n\t\t}\n\t\tif val != expected {\n\t\t\tt.Errorf(\"parseDataSize('%s') != %d\", s, expected)\n\t\t\treturn\n\t\t}\n\t}\n\n\tcheck(\"1M\", true, 1024*1024)\n\tcheck(\"1K\", true, 1024)\n\tcheck(\"1b\", true, 1)\n\tcheck(\"1M 5b\", true, 1024*1024+5)\n\tcheck(\"1M 5K 5b\", true, 1024*1024+5*1024+5)\n\tcheck(\"0\", true, 0)\n\tcheck(\"1\", false, 0)\n\tcheck(\"1d\", false, 0)\n\tcheck(\"d\", false, 0)\n\tcheck(\"unrelated\", false, 0)\n\tcheck(\"1M5b\", false, 0)\n\tcheck(\"\", false, 0)\n\tcheck(\"-5M\", false, 0)\n}\n\nfunc TestMap_Callback(t *testing.T) {\n\tcalled := map[string]int{}\n\n\tcfg := Node{\n\t\tChildren: []Node{\n\t\t\t{\n\t\t\t\tName: \"test2\",\n\t\t\t\tArgs: []string{\"a\"},\n\t\t\t},\n\t\t\t{\n\t\t\t\tName: \"test3\",\n\t\t\t\tArgs: []string{\"b\"},\n\t\t\t},\n\t\t\t{\n\t\t\t\tName: \"test3\",\n\t\t\t\tArgs: []string{\"b\"},\n\t\t\t},\n\t\t\t{\n\t\t\t\tName: \"unrelated\",\n\t\t\t\tArgs: []string{\"b\"},\n\t\t\t},\n\t\t},\n\t}\n\tm := NewMap(nil, cfg)\n\tm.Callback(\"test1\", func(*Map, Node) error {\n\t\tcalled[\"test1\"]++\n\t\treturn nil\n\t})\n\tm.Callback(\"test2\", func(_ *Map, n Node) error {\n\t\tcalled[\"test2\"]++\n\t\tif n.Args[0] != \"a\" {\n\t\t\tt.Fatal(\"Wrong n.Args[0] for test2:\", n.Args[0])\n\t\t}\n\t\treturn nil\n\t})\n\tm.Callback(\"test3\", func(_ *Map, n Node) error {\n\t\tcalled[\"test3\"]++\n\t\tif n.Args[0] != \"b\" {\n\t\t\tt.Fatal(\"Wrong n.Args[0] for test2:\", n.Args[0])\n\t\t}\n\t\treturn nil\n\t})\n\tm.AllowUnknown()\n\tothers, err := m.Process()\n\tif err != nil {\n\t\tt.Fatal(\"Unexpected error:\", err)\n\t}\n\tif called[\"test1\"] != 0 {\n\t\tt.Error(\"test1 CB was called when it should not\")\n\t}\n\tif called[\"test2\"] != 1 {\n\t\tt.Error(\"test2 CB was not called when it should\")\n\t}\n\tif called[\"test3\"] != 2 {\n\t\tt.Error(\"test3 CB was not called when it should\")\n\t}\n\tif len(others) != 1 {\n\t\tt.Error(\"Wrong amount of unmatched directives\")\n\t}\n\tif others[0].Name != \"unrelated\" {\n\t\tt.Error(\"Wrong directive returned in unmatched slice:\", others[0].Name)\n\t}\n}\n"
  },
  {
    "path": "framework/config/module/check_action.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage modconfig\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/exterrors\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n)\n\n// FailAction specifies actions that messages pipeline should take based on the\n// result of the check.\n//\n// Its check module responsibility to apply FailAction on the CheckResult it\n// returns. It is intended to be used as follows:\n//\n// Add the configuration directive to allow user to specify the action:\n//\n//\tcfg.Custom(\"SOME_action\", false, false,\n//\t\tfunc() (interface{}, error) {\n//\t\t\treturn modconfig.FailAction{Quarantine: true}, nil\n//\t\t}, modconfig.FailActionDirective, &yourModule.SOMEAction)\n//\n// return in func literal is the default value, you might want to adjust it.\n//\n// Call yourModule.SOMEAction.Apply on CheckResult containing only the\n// Reason field:\n//\n//\tfunc (yourModule YourModule) CheckConnection() module.CheckResult {\n//\t    return yourModule.SOMEAction.Apply(module.CheckResult{\n//\t        Reason: ...,\n//\t    })\n//\t}\ntype FailAction struct {\n\tQuarantine bool\n\tReject     bool\n\n\tReasonOverride *exterrors.SMTPError\n}\n\nfunc FailActionDirective(_ *config.Map, node config.Node) (interface{}, error) {\n\tif len(node.Children) != 0 {\n\t\treturn nil, config.NodeErr(node, \"can't declare block here\")\n\t}\n\n\tval, err := ParseActionDirective(node.Args)\n\tif err != nil {\n\t\treturn nil, config.NodeErr(node, \"%v\", err)\n\t}\n\treturn val, nil\n}\n\nfunc ParseActionDirective(args []string) (FailAction, error) {\n\tif len(args) == 0 {\n\t\treturn FailAction{}, errors.New(\"expected at least 1 argument\")\n\t}\n\n\tres := FailAction{}\n\n\tswitch args[0] {\n\tcase \"reject\", \"quarantine\":\n\t\tif len(args) > 1 {\n\t\t\tvar err error\n\t\t\tres.ReasonOverride, err = ParseRejectDirective(args[1:])\n\t\t\tif err != nil {\n\t\t\t\treturn FailAction{}, err\n\t\t\t}\n\t\t}\n\tcase \"ignore\":\n\tdefault:\n\t\treturn FailAction{}, errors.New(\"invalid action\")\n\t}\n\n\tres.Reject = args[0] == \"reject\"\n\tres.Quarantine = args[0] == \"quarantine\"\n\treturn res, nil\n}\n\n// Apply merges the result of check execution with action configuration specified\n// in the check configuration.\nfunc (cfa FailAction) Apply(originalRes module.CheckResult) module.CheckResult {\n\tif originalRes.Reason == nil {\n\t\treturn originalRes\n\t}\n\n\tif cfa.ReasonOverride != nil {\n\t\t// Wrap instead of replace to preserve other fields.\n\t\toriginalRes.Reason = &exterrors.SMTPError{\n\t\t\tCode:         cfa.ReasonOverride.Code,\n\t\t\tEnhancedCode: cfa.ReasonOverride.EnhancedCode,\n\t\t\tMessage:      cfa.ReasonOverride.Message,\n\t\t\tErr:          originalRes.Reason,\n\t\t}\n\t}\n\n\toriginalRes.Quarantine = cfa.Quarantine || originalRes.Quarantine\n\toriginalRes.Reject = cfa.Reject || originalRes.Reject\n\treturn originalRes\n}\n\nfunc ParseRejectDirective(args []string) (*exterrors.SMTPError, error) {\n\tcode := 554\n\tenchCode := exterrors.EnhancedCode{0, 7, 0}\n\tmsg := \"Message rejected due to a local policy\"\n\tvar err error\n\tswitch len(args) {\n\tcase 3:\n\t\tmsg = args[2]\n\t\tif msg == \"\" {\n\t\t\treturn nil, fmt.Errorf(\"message can't be empty\")\n\t\t}\n\t\tfallthrough\n\tcase 2:\n\t\tenchCode, err = parseEnhancedCode(args[1])\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif enchCode[0] != 4 && enchCode[0] != 5 {\n\t\t\treturn nil, fmt.Errorf(\"enhanced code should use either 4 or 5 as a first number\")\n\t\t}\n\t\tfallthrough\n\tcase 1:\n\t\tcode, err = strconv.Atoi(args[0])\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"invalid error code integer: %v\", err)\n\t\t}\n\t\tif (code/100) != 4 && (code/100) != 5 {\n\t\t\treturn nil, fmt.Errorf(\"error code should start with either 4 or 5\")\n\t\t}\n\t\t// If enchanced code is not set - set first digit based on provided \"basic\" code.\n\t\tif enchCode[0] == 0 {\n\t\t\tenchCode[0] = code / 100\n\t\t}\n\tcase 0:\n\t\t// If no codes provided at all - use 5.7.0 and 554.\n\t\tenchCode[0] = 5\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"invalid count of arguments\")\n\t}\n\treturn &exterrors.SMTPError{\n\t\tCode:         code,\n\t\tEnhancedCode: enchCode,\n\t\tMessage:      msg,\n\t\tReason:       \"reject directive used\",\n\t}, nil\n}\n\nfunc parseEnhancedCode(s string) (exterrors.EnhancedCode, error) {\n\tparts := strings.Split(s, \".\")\n\tif len(parts) != 3 {\n\t\treturn exterrors.EnhancedCode{}, fmt.Errorf(\"wrong amount of enhanced code parts\")\n\t}\n\n\tcode := exterrors.EnhancedCode{}\n\tfor i, part := range parts {\n\t\tnum, err := strconv.Atoi(part)\n\t\tif err != nil {\n\t\t\treturn code, err\n\t\t}\n\t\tcode[i] = num\n\t}\n\treturn code, nil\n}\n"
  },
  {
    "path": "framework/config/module/interfaces.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage modconfig\n\nimport (\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n)\n\nfunc MessageCheck(globals map[string]interface{}, args []string, block config.Node) (module.Check, error) {\n\tvar check module.Check\n\tif err := ModuleFromNode(\"check\", args, block, globals, &check); err != nil {\n\t\treturn nil, err\n\t}\n\treturn check, nil\n}\n\n// DeliveryDirective is a callback for use in config.Map.Custom.\n//\n// It does all work necessary to create a module instance from the config\n// directive with the following structure:\n//\n//\tdirective_name mod_name [inst_name] [{\n//\t  inline_mod_config\n//\t}]\n//\n// Note that if used configuration structure lacks directive_name before mod_name - this function\n// should not be used (call DeliveryTarget directly).\nfunc DeliveryDirective(m *config.Map, node config.Node) (interface{}, error) {\n\treturn DeliveryTarget(m.Globals, node.Args, node)\n}\n\nfunc DeliveryTarget(globals map[string]interface{}, args []string, block config.Node) (module.DeliveryTarget, error) {\n\tvar target module.DeliveryTarget\n\tif err := ModuleFromNode(\"target\", args, block, globals, &target); err != nil {\n\t\treturn nil, err\n\t}\n\treturn target, nil\n}\n\nfunc MsgModifier(globals map[string]interface{}, args []string, block config.Node) (module.Modifier, error) {\n\tvar check module.Modifier\n\tif err := ModuleFromNode(\"modify\", args, block, globals, &check); err != nil {\n\t\treturn nil, err\n\t}\n\treturn check, nil\n}\n\nfunc IMAPFilter(globals map[string]interface{}, args []string, block config.Node) (module.IMAPFilter, error) {\n\tvar filter module.IMAPFilter\n\tif err := ModuleFromNode(\"imap.filter\", args, block, globals, &filter); err != nil {\n\t\treturn nil, err\n\t}\n\treturn filter, nil\n}\n\nfunc StorageDirective(m *config.Map, node config.Node) (interface{}, error) {\n\tvar backend module.Storage\n\tif err := ModuleFromNode(\"storage\", node.Args, node, m.Globals, &backend); err != nil {\n\t\treturn nil, err\n\t}\n\treturn backend, nil\n}\n\n// Table is a convenience wrapper for TableDirective.\n//\n//\tcfg.Bool(...)\n//\tmodconfig.Table(cfg, \"auth_map\", false, false, nil, &mod.authMap)\n//\tcfg.Process()\nfunc Table(cfg *config.Map, name string, inheritGlobal, required bool, defaultVal module.Table, store *module.Table) {\n\tcfg.Custom(name, inheritGlobal, required, func() (interface{}, error) {\n\t\treturn defaultVal, nil\n\t}, TableDirective, store)\n}\n\nfunc TableDirective(m *config.Map, node config.Node) (interface{}, error) {\n\tvar tbl module.Table\n\tif err := ModuleFromNode(\"table\", node.Args, node, m.Globals, &tbl); err != nil {\n\t\treturn nil, err\n\t}\n\treturn tbl, nil\n}\n"
  },
  {
    "path": "framework/config/module/modconfig.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\n// Package modconfig provides matchers for config.Map that query\n// modules registry and parse inline module definitions.\n//\n// They should be used instead of manual querying when there is need to\n// reference a module instance in the configuration.\n//\n// See ModuleFromNode documentation for explanation of what is 'args'\n// for some functions (DeliveryTarget).\npackage modconfig\n\nimport (\n\t\"fmt\"\n\t\"reflect\"\n\t\"strings\"\n\n\tparser \"github.com/foxcpp/maddy/framework/cfgparser\"\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/log\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n)\n\n// createInlineModule is a helper function for config matchers that can create inline modules.\nfunc createInlineModule(c *container.C, preferredNamespace, modName string) (module.Module, error) {\n\tvar newMod modules.FuncNewModule\n\toriginalModName := modName\n\n\t// First try to extend the name with preferred namespace unless the name\n\t// already contains it.\n\tif !strings.Contains(modName, \".\") && preferredNamespace != \"\" {\n\t\tmodName = preferredNamespace + \".\" + modName\n\t\tnewMod = modules.Get(modName)\n\t}\n\n\t// Then try global namespace for compatibility and complex modules.\n\tif newMod == nil {\n\t\tnewMod = modules.Get(originalModName)\n\t}\n\n\t// Bail if both failed.\n\tif newMod == nil {\n\t\treturn nil, fmt.Errorf(\"unknown module: %s (namespace: %s)\", originalModName, preferredNamespace)\n\t}\n\n\treturn newMod(c, modName, \"\")\n}\n\n// configureInlineModule constructs \"faked\" config tree and passes it to module\n// Init function to make it look like it is defined at top-level.\n//\n// args must contain at least one argument, otherwise configureInlineModule panics.\nfunc configureInlineModule(modObj module.Module, args []string, globals map[string]interface{}, block config.Node) error {\n\terr := modObj.Configure(args, config.NewMap(globals, block))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif li, ok := modObj.(container.LifetimeModule); ok {\n\t\tcontainer.Global.Lifetime.Add(li)\n\t}\n\n\treturn nil\n}\n\n// ModuleFromNode does all work to create or get existing module object with a certain type.\n// It is not used by top-level module definitions, only for references from other\n// modules configuration blocks.\n//\n// inlineCfg should contain configuration directives for inline declarations.\n// args should contain values that are used to create module.\n// It should be either module name + instance name or just module name. Further extensions\n// may add other string arguments (currently, they can be accessed by module instances\n// as inlineArgs argument to constructor).\n//\n// It checks using reflection whether it is possible to store a module object into modObj\n// pointer (e.g. it implements all necessary interfaces) and stores it if everything is fine.\n// If module object doesn't implement necessary module interfaces - error is returned.\n// If modObj is not a pointer, ModuleFromNode panics.\n//\n// preferredNamespace is used as an implicit prefix for module name lookups.\n// Module with name preferredNamespace + \".\" + args[0] will be preferred over just args[0].\n// It can be omitted.\nfunc ModuleFromNode(preferredNamespace string, args []string, inlineCfg config.Node, globals map[string]interface{}, moduleIface interface{}) error {\n\tif len(args) == 0 {\n\t\treturn parser.NodeErr(inlineCfg, \"at least one argument is required\")\n\t}\n\n\treferenceExisting := strings.HasPrefix(args[0], \"&\")\n\n\tvar modObj module.Module\n\tvar err error\n\tif referenceExisting {\n\t\tif len(args) != 1 || inlineCfg.Children != nil {\n\t\t\treturn parser.NodeErr(inlineCfg, \"exactly one argument is required to use existing config block\")\n\t\t}\n\t\tmodObj, err = container.Global.Modules.Get(args[0][1:])\n\t\tlog.Debugf(\"%s:%d: reference %s\", inlineCfg.File, inlineCfg.Line, args[0])\n\t} else {\n\t\tlog.Debugf(\"%s:%d: new module %s %v\", inlineCfg.File, inlineCfg.Line, args[0], args[1:])\n\t\tmodObj, err = createInlineModule(container.Global, preferredNamespace, args[0])\n\t}\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// NOTE: This will panic if moduleIface is not a pointer.\n\tmodIfaceType := reflect.TypeOf(moduleIface).Elem()\n\tmodObjType := reflect.TypeOf(modObj)\n\n\tif modIfaceType.Kind() == reflect.Interface {\n\t\t// Case for assignment to module interface type.\n\t\tif !modObjType.Implements(modIfaceType) && !modObjType.AssignableTo(modIfaceType) {\n\t\t\treturn parser.NodeErr(inlineCfg, \"module %s (%s) doesn't implement %v interface\", modObj.Name(), modObj.InstanceName(), modIfaceType)\n\t\t}\n\t} else if !modObjType.AssignableTo(modIfaceType) {\n\t\t// Case for assignment to concrete module type. Used in \"module groups\".\n\t\treturn parser.NodeErr(inlineCfg, \"module %s (%s) is not %v\", modObj.Name(), modObj.InstanceName(), modIfaceType)\n\t}\n\n\treflect.ValueOf(moduleIface).Elem().Set(reflect.ValueOf(modObj))\n\n\tif !referenceExisting {\n\t\tif err := configureInlineModule(modObj, args[1:], globals, inlineCfg); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// GroupFromNode provides a special kind of ModuleFromNode syntax that allows\n// to omit the module name when defining inine configuration.  If it is not\n// present, name in defaultModule is used.\nfunc GroupFromNode(defaultModule string, args []string, inlineCfg config.Node, globals map[string]interface{}, moduleIface interface{}) error {\n\tif len(args) == 0 {\n\t\targs = append(args, defaultModule)\n\t}\n\treturn ModuleFromNode(\"\", args, inlineCfg, globals, moduleIface)\n}\n"
  },
  {
    "path": "framework/config/tls/client.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage tls\n\nimport (\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/log\"\n)\n\nfunc TLSClientBlock(_ *config.Map, node config.Node) (interface{}, error) {\n\tcfg := tls.Config{}\n\n\tchildM := config.NewMap(nil, node)\n\tvar (\n\t\ttlsVersions       [2]uint16\n\t\trootCAPaths       []string\n\t\tcertPath, keyPath string\n\t)\n\n\tchildM.StringList(\"root_ca\", false, false, nil, &rootCAPaths)\n\tchildM.String(\"cert\", false, false, \"\", &certPath)\n\tchildM.String(\"key\", false, false, \"\", &keyPath)\n\tchildM.Custom(\"protocols\", false, false, func() (interface{}, error) {\n\t\treturn [2]uint16{0, 0}, nil\n\t}, TLSVersionsDirective, &tlsVersions)\n\tchildM.Custom(\"ciphers\", false, false, func() (interface{}, error) {\n\t\treturn nil, nil\n\t}, TLSCiphersDirective, &cfg.CipherSuites)\n\tchildM.Custom(\"curves\", false, false, func() (interface{}, error) {\n\t\treturn nil, nil\n\t}, TLSCurvesDirective, &cfg.CurvePreferences)\n\n\tif _, err := childM.Process(); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif len(rootCAPaths) != 0 {\n\t\tpool := x509.NewCertPool()\n\t\tfor _, path := range rootCAPaths {\n\t\t\tblob, err := os.ReadFile(path)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tif !pool.AppendCertsFromPEM(blob) {\n\t\t\t\treturn nil, fmt.Errorf(\"no certificates was loaded from %s\", path)\n\t\t\t}\n\t\t}\n\t\tcfg.RootCAs = pool\n\t}\n\n\tif certPath != \"\" && keyPath != \"\" {\n\t\tkeypair, err := tls.LoadX509KeyPair(certPath, keyPath)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tlog.Debugf(\"using client keypair %s/%s\", certPath, keyPath)\n\t\tcfg.GetClientCertificate = func(*tls.CertificateRequestInfo) (*tls.Certificate, error) {\n\t\t\treturn &keypair, nil\n\t\t}\n\t}\n\n\tcfg.MinVersion = tlsVersions[0]\n\tcfg.MaxVersion = tlsVersions[1]\n\tlog.Debugf(\"tls: min version: %x, max version: %x\", tlsVersions[0], tlsVersions[1])\n\n\treturn &cfg, nil\n}\n"
  },
  {
    "path": "framework/config/tls/general.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage tls\n\nimport (\n\t\"crypto/tls\"\n\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/log\"\n)\n\nvar strVersionsMap = map[string]uint16{\n\t\"tls1.0\": tls.VersionTLS10,\n\t\"tls1.1\": tls.VersionTLS11,\n\t\"tls1.2\": tls.VersionTLS12,\n\t\"tls1.3\": tls.VersionTLS13,\n\t\"\":       0, // use crypto/tls defaults if value is not specified\n}\n\nvar strCiphersMap = map[string]uint16{\n\t// TLS 1.0 - 1.2 cipher suites.\n\t\"RSA-WITH-RC4128-SHA\":                tls.TLS_RSA_WITH_RC4_128_SHA,\n\t\"RSA-WITH-3DES-EDE-CBC-SHA\":          tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,\n\t\"RSA-WITH-AES128-CBC-SHA\":            tls.TLS_RSA_WITH_AES_128_CBC_SHA,\n\t\"RSA-WITH-AES256-CBC-SHA\":            tls.TLS_RSA_WITH_AES_256_CBC_SHA,\n\t\"RSA-WITH-AES128-CBC-SHA256\":         tls.TLS_RSA_WITH_AES_128_CBC_SHA256,\n\t\"RSA-WITH-AES128-GCM-SHA256\":         tls.TLS_RSA_WITH_AES_128_GCM_SHA256,\n\t\"RSA-WITH-AES256-GCM-SHA384\":         tls.TLS_RSA_WITH_AES_256_GCM_SHA384,\n\t\"ECDHE-ECDSA-WITH-RC4128-SHA\":        tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA,\n\t\"ECDHE-ECDSA-WITH-AES128-CBC-SHA\":    tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,\n\t\"ECDHE-ECDSA-WITH-AES256-CBC-SHA\":    tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,\n\t\"ECDHE-RSA-WITH-RC4128-SHA\":          tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA,\n\t\"ECDHE-RSA-WITH-3DES-EDE-CBC-SHA\":    tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,\n\t\"ECDHE-RSA-WITH-AES128-CBC-SHA\":      tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,\n\t\"ECDHE-RSA-WITH-AES256-CBC-SHA\":      tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,\n\t\"ECDHE-ECDSA-WITH-AES128-CBC-SHA256\": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256,\n\t\"ECDHE-RSA-WITH-AES128-CBC-SHA256\":   tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,\n\t\"ECDHE-RSA-WITH-AES128-GCM-SHA256\":   tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,\n\t\"ECDHE-ECDSA-WITH-AES128-GCM-SHA256\": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,\n\t\"ECDHE-RSA-WITH-AES256-GCM-SHA384\":   tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,\n\t\"ECDHE-ECDSA-WITH-AES256-GCM-SHA384\": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,\n\t\"ECDHE-RSA-WITH-CHACHA20-POLY1305\":   tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,\n\t\"ECDHE-ECDSA-WITH-CHACHA20-POLY1305\": tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,\n}\n\nvar strCurvesMap = map[string]tls.CurveID{\n\t\"p256\":   tls.CurveP256,\n\t\"p384\":   tls.CurveP384,\n\t\"p521\":   tls.CurveP521,\n\t\"X25519\": tls.X25519,\n}\n\n// TLSversionsDirective parses directive with arguments that specify\n// minimum and maximum supported TLS versions.\n//\n// It returns [2]uint16 value for use in corresponding fields from tls.Config.\nfunc TLSVersionsDirective(_ *config.Map, node config.Node) (interface{}, error) {\n\tswitch len(node.Args) {\n\tcase 1:\n\t\tvalue, ok := strVersionsMap[node.Args[0]]\n\t\tif !ok {\n\t\t\treturn nil, config.NodeErr(node, \"invalid TLS version value: %s\", node.Args[0])\n\t\t}\n\t\treturn [2]uint16{value, value}, nil\n\tcase 2:\n\t\tminValue, ok := strVersionsMap[node.Args[0]]\n\t\tif !ok {\n\t\t\treturn nil, config.NodeErr(node, \"invalid TLS version value: %s\", node.Args[0])\n\t\t}\n\t\tmaxValue, ok := strVersionsMap[node.Args[1]]\n\t\tif !ok {\n\t\t\treturn nil, config.NodeErr(node, \"invalid TLS version value: %s\", node.Args[1])\n\t\t}\n\t\treturn [2]uint16{minValue, maxValue}, nil\n\tdefault:\n\t\treturn nil, config.NodeErr(node, \"expected 1 or 2 arguments\")\n\t}\n}\n\n// TLSCiphersDirective parses directive with arguments that specify\n// list of ciphers to offer to clients (or to use for outgoing connections).\n//\n// It returns list of []uint16 with corresponding cipher IDs.\nfunc TLSCiphersDirective(_ *config.Map, node config.Node) (interface{}, error) {\n\tif len(node.Args) == 0 {\n\t\treturn nil, config.NodeErr(node, \"expected at least 1 argument, got 0\")\n\t}\n\n\tres := make([]uint16, 0, len(node.Args))\n\tfor _, arg := range node.Args {\n\t\tcipherId, ok := strCiphersMap[arg]\n\t\tif !ok {\n\t\t\treturn nil, config.NodeErr(node, \"unknown cipher: %s\", arg)\n\t\t}\n\t\tres = append(res, cipherId)\n\t}\n\tlog.Debugln(\"tls: using non-default cipherset:\", node.Args)\n\treturn res, nil\n}\n\n// TLSCurvesDirective parses directive with arguments that specify\n// elliptic curves to use during TLS key exchange.\n//\n// It returns []tls.CurveID.\nfunc TLSCurvesDirective(_ *config.Map, node config.Node) (interface{}, error) {\n\tif len(node.Args) == 0 {\n\t\treturn nil, config.NodeErr(node, \"expected at least 1 argument, got 0\")\n\t}\n\n\tres := make([]tls.CurveID, 0, len(node.Args))\n\tfor _, arg := range node.Args {\n\t\tcurveId, ok := strCurvesMap[arg]\n\t\tif !ok {\n\t\t\treturn nil, config.NodeErr(node, \"unknown curve: %s\", arg)\n\t\t}\n\t\tres = append(res, curveId)\n\t}\n\tlog.Debugln(\"tls: using non-default curve preferences:\", node.Args)\n\treturn res, nil\n}\n"
  },
  {
    "path": "framework/config/tls/server.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage tls\n\nimport (\n\t\"crypto/tls\"\n\n\t\"github.com/foxcpp/maddy/framework/config\"\n\tmodconfig \"github.com/foxcpp/maddy/framework/config/module\"\n\t\"github.com/foxcpp/maddy/framework/log\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n)\n\ntype TLSConfig struct {\n\tloader  module.TLSLoader\n\tbaseCfg *tls.Config\n}\n\nfunc (cfg *TLSConfig) Get() (*tls.Config, error) {\n\tif cfg.loader == nil {\n\t\treturn nil, nil\n\t}\n\ttlsCfg := cfg.baseCfg.Clone()\n\n\terr := cfg.loader.ConfigureTLS(tlsCfg)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn tlsCfg, nil\n}\n\n// TLSDirective reads the TLS configuration and adds the reload handler to\n// reread certificates on SIGUSR2.\n//\n// The returned value is *tls.Config with GetConfigForClient set.\n// If the 'tls off' is used, returned value is nil.\nfunc TLSDirective(m *config.Map, node config.Node) (interface{}, error) {\n\tcfg, err := readTLSBlock(m.Globals, node)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif cfg == nil {\n\t\treturn nil, nil\n\t}\n\n\treturn &tls.Config{\n\t\tGetConfigForClient: func(hello *tls.ClientHelloInfo) (*tls.Config, error) {\n\t\t\treturn cfg.Get()\n\t\t},\n\t}, nil\n}\n\nfunc readTLSBlock(globals map[string]interface{}, blockNode config.Node) (*TLSConfig, error) {\n\tbaseCfg := tls.Config{\n\t\t// Workaround for issue https://github.com/foxcpp/maddy/issues/730\n\t\tSessionTicketsDisabled: true,\n\t}\n\n\tvar loader module.TLSLoader\n\tif len(blockNode.Args) > 0 {\n\t\tif blockNode.Args[0] == \"off\" {\n\t\t\treturn nil, nil\n\t\t}\n\n\t\terr := modconfig.ModuleFromNode(\"tls.loader\", blockNode.Args, config.Node{}, globals, &loader)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tchildM := config.NewMap(globals, blockNode)\n\tvar tlsVersions [2]uint16\n\n\tchildM.Custom(\"loader\", false, false, func() (interface{}, error) {\n\t\treturn loader, nil\n\t}, func(_ *config.Map, node config.Node) (interface{}, error) {\n\t\tvar l module.TLSLoader\n\t\terr := modconfig.ModuleFromNode(\"tls.loader\", node.Args, node, globals, &l)\n\t\treturn l, err\n\t}, &loader)\n\n\tchildM.Custom(\"protocols\", false, false, func() (interface{}, error) {\n\t\treturn [2]uint16{tls.VersionTLS10, 0}, nil\n\t}, TLSVersionsDirective, &tlsVersions)\n\n\tchildM.Custom(\"ciphers\", false, false, func() (interface{}, error) {\n\t\treturn nil, nil\n\t}, TLSCiphersDirective, &baseCfg.CipherSuites)\n\n\tchildM.Custom(\"curves\", false, false, func() (interface{}, error) {\n\t\treturn nil, nil\n\t}, TLSCurvesDirective, &baseCfg.CurvePreferences)\n\n\tif _, err := childM.Process(); err != nil {\n\t\treturn nil, err\n\t}\n\n\tbaseCfg.MinVersion = tlsVersions[0]\n\tbaseCfg.MaxVersion = tlsVersions[1]\n\tlog.Debugf(\"tls: min version: %x, max version: %x\", tlsVersions[0], tlsVersions[1])\n\n\treturn &TLSConfig{\n\t\tloader:  loader,\n\t\tbaseCfg: &baseCfg,\n\t}, nil\n}\n"
  },
  {
    "path": "framework/container/container.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage container\n\nimport (\n\t\"github.com/foxcpp/maddy/framework/log\"\n)\n\ntype GlobalConfig struct {\n\t// StateDirectory contains the path to the directory that\n\t// should be used to store any data that should be\n\t// preserved between sessions.\n\t//\n\t// Value of this variable must not change after initialization\n\t// in cmd/maddy/main.go.\n\tStateDirectory string\n\n\t// RuntimeDirectory contains the path to the directory that\n\t// should be used to store any temporary data.\n\t//\n\t// It should be preferred over os.TempDir, which is\n\t// global and world-readable on most systems, while\n\t// RuntimeDirectory can be dedicated for maddy.\n\t//\n\t// Value of this variable must not change after initialization\n\t// in cmd/maddy/main.go.\n\tRuntimeDirectory string\n\n\t// LibexecDirectory contains the path to the directory\n\t// where helper binaries should be searched.\n\t//\n\t// Value of this variable must not change after initialization\n\t// in cmd/maddy/main.go.\n\tLibexecDirectory string\n}\n\ntype C struct {\n\tConfig        GlobalConfig\n\tDefaultLogger *log.Logger\n\tModules       *Registry\n\tLifetime      *LifetimeTracker\n}\n\nfunc New() *C {\n\trootLog := log.DefaultLogger.Sublogger(\"\")\n\treturn &C{\n\t\tDefaultLogger: rootLog,\n\t\tModules:       NewRegistry(rootLog.Sublogger(\"registry\")),\n\t\tLifetime:      NewLifetime(rootLog.Sublogger(\"lifetime\")),\n\t}\n}\n\n// Global is the default instance while refactoring is in progress.\nvar Global *C\n"
  },
  {
    "path": "framework/container/lifetime.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2025 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage container\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/foxcpp/maddy/framework/log\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n)\n\n// LifetimeModule is a stateful module that needs to have post-configuration\n// startup and graceful shutdown functionality.\ntype LifetimeModule interface {\n\tmodule.Module\n\tStart() error\n\tStop() error\n}\n\ntype ReloadModule interface {\n\tmodule.Module\n\tReload() error\n}\n\n// EarlyStopModule is a LifetimeModule that needs to do some bookkeeping\n// before new server instance starts during reload.\ntype EarlyStopModule interface {\n\tLifetimeModule\n\tEarlyStop() error\n}\n\ntype LifetimeTracker struct {\n\tlogger    *log.Logger\n\tinstances []*struct {\n\t\tmod          LifetimeModule\n\t\tstarted      bool\n\t\tearlyStopped bool\n\t}\n}\n\nfunc (lt *LifetimeTracker) Add(mod LifetimeModule) {\n\tlt.instances = append(lt.instances, &struct {\n\t\tmod          LifetimeModule\n\t\tstarted      bool\n\t\tearlyStopped bool\n\t}{mod: mod, started: false})\n}\n\n// StartAll calls Start for all registered LifetimeModule instances.\nfunc (lt *LifetimeTracker) StartAll() error {\n\tfor _, entry := range lt.instances {\n\t\tif entry.started {\n\t\t\tcontinue\n\t\t}\n\n\t\tlt.logger.DebugMsg(\"starting module\",\n\t\t\t\"mod_name\", entry.mod.Name(), \"inst_name\", entry.mod.InstanceName())\n\n\t\tif err := entry.mod.Start(); err != nil {\n\t\t\tif err := lt.StopAll(); err != nil {\n\t\t\t\tlt.logger.Error(\"StopAll failed after Start fail\", err)\n\t\t\t}\n\t\t\treturn fmt.Errorf(\"failed to start module %v: %w\",\n\t\t\t\tentry.mod.InstanceName(), err)\n\t\t}\n\t\tlt.logger.DebugMsg(\"module started\",\n\t\t\t\"mod_name\", entry.mod.Name(), \"inst_name\", entry.mod.InstanceName())\n\t\tentry.started = true\n\t}\n\treturn nil\n}\n\nfunc (lt *LifetimeTracker) ReloadAll() error {\n\tfor _, entry := range lt.instances {\n\t\tif !entry.started {\n\t\t\tcontinue\n\t\t}\n\n\t\trm, ok := entry.mod.(ReloadModule)\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\n\t\tif err := rm.Reload(); err != nil {\n\t\t\tlt.logger.Error(\"module reload failed\", err,\n\t\t\t\t\"mod_name\", entry.mod.Name(), \"inst_name\", entry.mod.InstanceName())\n\t\t\tcontinue\n\t\t}\n\n\t\tlt.logger.DebugMsg(\"module reloaded\",\n\t\t\t\"mod_name\", entry.mod.Name(), \"inst_name\", entry.mod.InstanceName())\n\t}\n\treturn nil\n}\n\nfunc (lt *LifetimeTracker) EarlyStopAll() error {\n\tfor i := len(lt.instances) - 1; i >= 0; i-- {\n\t\tentry := lt.instances[i]\n\n\t\tif !entry.started {\n\t\t\tcontinue\n\t\t}\n\n\t\trsm, ok := entry.mod.(EarlyStopModule)\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\n\t\tif err := rsm.EarlyStop(); err != nil {\n\t\t\tlt.logger.Error(\"module early stop failed\", err,\n\t\t\t\t\"mod_name\", entry.mod.Name(), \"inst_name\", entry.mod.InstanceName())\n\t\t\tcontinue\n\t\t}\n\t\tlt.logger.DebugMsg(\"module early stopped\",\n\t\t\t\"mod_name\", entry.mod.Name(), \"inst_name\", entry.mod.InstanceName())\n\n\t\tentry.earlyStopped = true\n\t}\n\treturn nil\n}\n\n// StopAll calls Stop for all registered LifetimeModule instances.\nfunc (lt *LifetimeTracker) StopAll() error {\n\tfor i := len(lt.instances) - 1; i >= 0; i-- {\n\t\tentry := lt.instances[i]\n\n\t\tif !entry.started {\n\t\t\tcontinue\n\t\t}\n\n\t\tif err := entry.mod.Stop(); err != nil {\n\t\t\tlt.logger.Error(\"module stop failed\", err,\n\t\t\t\t\"mod_name\", entry.mod.Name(), \"inst_name\", entry.mod.InstanceName())\n\t\t\tcontinue\n\t\t}\n\t\tlt.logger.DebugMsg(\"module stopped\",\n\t\t\t\"mod_name\", entry.mod.Name(), \"inst_name\", entry.mod.InstanceName())\n\n\t\tentry.started = false\n\t}\n\treturn nil\n}\n\nfunc NewLifetime(log *log.Logger) *LifetimeTracker {\n\treturn &LifetimeTracker{\n\t\tlogger: log,\n\t}\n}\n"
  },
  {
    "path": "framework/container/registry.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage container\n\nimport (\n\t\"errors\"\n\n\t\"github.com/foxcpp/maddy/framework/log\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n)\n\nvar (\n\tErrInstanceNameDuplicate = errors.New(\"instance name already registered\")\n\tErrInstanceUnknown       = errors.New(\"no such instance registered\")\n)\n\ntype registryEntry struct {\n\tMod      module.Module\n\tLazyInit func() error\n}\n\ntype Registry struct {\n\tlogger      *log.Logger\n\tinstances   map[string]registryEntry\n\tinitialized map[string]struct{}\n\tstarted     map[string]struct{}\n\taliases     map[string]string\n}\n\nfunc NewRegistry(log *log.Logger) *Registry {\n\treturn &Registry{\n\t\tlogger:      log,\n\t\tinstances:   make(map[string]registryEntry),\n\t\tinitialized: make(map[string]struct{}),\n\t\tstarted:     make(map[string]struct{}),\n\t\taliases:     make(map[string]string),\n\t}\n}\n\n// Register adds not-initialized (configured) module into registry.\n//\n// lazyInit function will be called on first request to get the module from\n// registry.\nfunc (r *Registry) Register(mod module.Module, lazyInit func() error) error {\n\tinstName := mod.InstanceName()\n\tif instName == \"\" {\n\t\tpanic(\"module with empty instance name cannot be added to the registry\")\n\t}\n\n\t_, ok := r.instances[instName]\n\tif ok {\n\t\treturn ErrInstanceNameDuplicate\n\t}\n\n\tr.instances[instName] = registryEntry{\n\t\tMod:      mod,\n\t\tLazyInit: lazyInit,\n\t}\n\treturn nil\n}\n\nfunc (r *Registry) AddAlias(instanceName string, alias string) error {\n\tif instanceName == \"\" {\n\t\tpanic(\"cannot add an alias for empty instance name\")\n\t}\n\tif alias == \"\" {\n\t\tpanic(\"cannot add an empty alias\")\n\t}\n\t_, ok := r.aliases[alias]\n\tif ok {\n\t\treturn ErrInstanceNameDuplicate\n\t}\n\t_, ok = r.instances[instanceName]\n\tif ok {\n\t\treturn ErrInstanceNameDuplicate\n\t}\n\n\tr.aliases[alias] = instanceName\n\treturn nil\n}\n\nfunc (r *Registry) ensureInitialized(name string, entry *registryEntry) error {\n\t_, ok := r.initialized[name]\n\tif ok {\n\t\treturn nil\n\t}\n\tif entry.LazyInit == nil {\n\t\treturn nil\n\t}\n\n\tr.logger.DebugMsg(\"module configure\",\n\t\t\"mod_name\", entry.Mod.Name(), \"inst_name\", entry.Mod.InstanceName())\n\tr.initialized[name] = struct{}{}\n\terr := entry.LazyInit()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (r *Registry) Get(name string) (module.Module, error) {\n\tif name == \"\" {\n\t\tpanic(\"cannot get module with empty name\")\n\t}\n\taliasedName := r.aliases[name]\n\tif aliasedName != \"\" {\n\t\tname = aliasedName\n\t}\n\n\tmod, ok := r.instances[name]\n\tif !ok {\n\t\treturn nil, ErrInstanceUnknown\n\t}\n\n\tif err := r.ensureInitialized(name, &mod); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn mod.Mod, nil\n}\n\nfunc (r *Registry) NotInitialized() []module.Module {\n\tnotinit := make([]module.Module, 0, len(r.instances)-len(r.initialized))\n\tfor name, mod := range r.instances {\n\t\tif _, ok := r.initialized[name]; ok {\n\t\t\tcontinue\n\t\t}\n\t\tnotinit = append(notinit, mod.Mod)\n\t}\n\treturn notinit\n}\n"
  },
  {
    "path": "framework/dns/debugflags.go",
    "content": "//go:build debugflags\n// +build debugflags\n\n/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage dns\n\nimport (\n\tmaddycli \"github.com/foxcpp/maddy/internal/cli\"\n\t\"github.com/urfave/cli/v2\"\n)\n\nfunc init() {\n\tmaddycli.AddGlobalFlag(&cli.StringFlag{\n\t\tName:        \"debug.dnsoverride\",\n\t\tUsage:       \"replace the DNS resolver address\",\n\t\tValue:       \"system-default\",\n\t\tDestination: &overrideServ,\n\t\tAction: func(context *cli.Context, s string) error {\n\t\t\tif s != \"\" && s != \"system-default\" {\n\t\t\t\toverride(s)\n\t\t\t}\n\t\t\toverrideServ = s\n\t\t\treturn nil\n\t\t},\n\t})\n}\n"
  },
  {
    "path": "framework/dns/dnssec.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage dns\n\nimport (\n\t\"context\"\n\t\"net\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/foxcpp/maddy/framework/log\"\n\t\"github.com/miekg/dns\"\n)\n\ntype TLSA = dns.TLSA\n\n// ExtResolver is a convenience wrapper for miekg/dns library that provides\n// access to certain low-level functionality (notably, AD flag in responses,\n// indicating whether DNSSEC verification was performed by the server).\ntype ExtResolver struct {\n\tcl  *dns.Client\n\tCfg *dns.ClientConfig\n}\n\n// RCodeError is returned by ExtResolver when the RCODE in response is not\n// NOERROR.\ntype RCodeError struct {\n\tName string\n\tCode int\n}\n\nfunc (err RCodeError) Temporary() bool {\n\treturn err.Code == dns.RcodeServerFailure\n}\n\nfunc (err RCodeError) Error() string {\n\tswitch err.Code {\n\tcase dns.RcodeFormatError:\n\t\treturn \"dns: rcode FORMERR when looking up \" + err.Name\n\tcase dns.RcodeServerFailure:\n\t\treturn \"dns: rcode SERVFAIL when looking up \" + err.Name\n\tcase dns.RcodeNameError:\n\t\treturn \"dns: rcode NXDOMAIN when looking up \" + err.Name\n\tcase dns.RcodeNotImplemented:\n\t\treturn \"dns: rcode NOTIMP when looking up \" + err.Name\n\tcase dns.RcodeRefused:\n\t\treturn \"dns: rcode REFUSED when looking up \" + err.Name\n\t}\n\treturn \"dns: non-success rcode: \" + strconv.Itoa(err.Code) + \" when looking up \" + err.Name\n}\n\nfunc IsNotFound(err error) bool {\n\tif dnsErr, ok := err.(*net.DNSError); ok {\n\t\treturn dnsErr.IsNotFound\n\t}\n\tif rcodeErr, ok := err.(RCodeError); ok {\n\t\treturn rcodeErr.Code == dns.RcodeNameError\n\t}\n\treturn false\n}\n\nfunc isLoopback(addr string) bool {\n\tip := net.ParseIP(addr)\n\tif ip == nil {\n\t\treturn false\n\t}\n\treturn ip.IsLoopback()\n}\n\nfunc (e ExtResolver) exchange(ctx context.Context, msg *dns.Msg) (*dns.Msg, error) {\n\tvar resp *dns.Msg\n\tvar lastErr error\n\tfor _, srv := range e.Cfg.Servers {\n\t\tresp, _, lastErr = e.cl.ExchangeContext(ctx, msg, net.JoinHostPort(srv, e.Cfg.Port))\n\t\tif lastErr != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tif resp.Rcode != dns.RcodeSuccess {\n\t\t\tlastErr = RCodeError{msg.Question[0].Name, resp.Rcode}\n\t\t\tcontinue\n\t\t}\n\n\t\t// Diregard AD flags from non-local resolvers, likely they are\n\t\t// communicated with using an insecure channel and so flags can be\n\t\t// tampered with.\n\t\tif !isLoopback(srv) {\n\t\t\tresp.AuthenticatedData = false\n\t\t}\n\n\t\tbreak\n\t}\n\treturn resp, lastErr\n}\n\nfunc (e ExtResolver) AuthLookupAddr(ctx context.Context, addr string) (ad bool, names []string, err error) {\n\trevAddr, err := dns.ReverseAddr(addr)\n\tif err != nil {\n\t\treturn false, nil, err\n\t}\n\n\tmsg := new(dns.Msg)\n\tmsg.SetQuestion(revAddr, dns.TypePTR)\n\tmsg.SetEdns0(4096, false)\n\tmsg.AuthenticatedData = true\n\n\tresp, err := e.exchange(ctx, msg)\n\tif err != nil {\n\t\treturn false, nil, err\n\t}\n\n\tad = resp.AuthenticatedData\n\tnames = make([]string, 0, len(resp.Answer))\n\tfor _, rr := range resp.Answer {\n\t\tptrRR, ok := rr.(*dns.PTR)\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\n\t\tnames = append(names, ptrRR.Ptr)\n\t}\n\treturn\n}\n\nfunc (e ExtResolver) AuthLookupHost(ctx context.Context, host string) (ad bool, addrs []string, err error) {\n\tad, addrParsed, err := e.AuthLookupIPAddr(ctx, host)\n\tif err != nil {\n\t\treturn false, nil, err\n\t}\n\n\taddrs = make([]string, 0, len(addrParsed))\n\tfor _, addr := range addrParsed {\n\t\taddrs = append(addrs, addr.String())\n\t}\n\treturn ad, addrs, nil\n}\n\nfunc (e ExtResolver) AuthLookupMX(ctx context.Context, name string) (ad bool, mxs []*net.MX, err error) {\n\tmsg := new(dns.Msg)\n\tmsg.SetQuestion(dns.Fqdn(name), dns.TypeMX)\n\tmsg.SetEdns0(4096, false)\n\tmsg.AuthenticatedData = true\n\n\tresp, err := e.exchange(ctx, msg)\n\tif err != nil {\n\t\treturn false, nil, err\n\t}\n\n\tad = resp.AuthenticatedData\n\tmxs = make([]*net.MX, 0, len(resp.Answer))\n\tfor _, rr := range resp.Answer {\n\t\tmxRR, ok := rr.(*dns.MX)\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\n\t\tmxs = append(mxs, &net.MX{\n\t\t\tHost: mxRR.Mx,\n\t\t\tPref: mxRR.Preference,\n\t\t})\n\t}\n\treturn\n}\n\nfunc (e ExtResolver) AuthLookupTXT(ctx context.Context, name string) (ad bool, recs []string, err error) {\n\tmsg := new(dns.Msg)\n\tmsg.SetQuestion(dns.Fqdn(name), dns.TypeTXT)\n\tmsg.SetEdns0(4096, false)\n\tmsg.AuthenticatedData = true\n\n\tresp, err := e.exchange(ctx, msg)\n\tif err != nil {\n\t\treturn false, nil, err\n\t}\n\n\tad = resp.AuthenticatedData\n\trecs = make([]string, 0, len(resp.Answer))\n\tfor _, rr := range resp.Answer {\n\t\ttxtRR, ok := rr.(*dns.TXT)\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\n\t\trecs = append(recs, strings.Join(txtRR.Txt, \"\"))\n\t}\n\treturn\n}\n\n// CheckCNAMEAD is a special function for use in DANE lookups. It attempts to determine final\n// (canonical) name of the host and also reports whether the whole chain of CNAME's and final zone\n// are \"secure\".\n//\n// If there are no A or AAAA records for host, rname = \"\" is returned.\nfunc (e ExtResolver) CheckCNAMEAD(ctx context.Context, host string) (ad bool, rname string, err error) {\n\tmsg := new(dns.Msg)\n\tmsg.SetQuestion(dns.Fqdn(host), dns.TypeA)\n\tmsg.SetEdns0(4096, false)\n\tmsg.AuthenticatedData = true\n\tresp, err := e.exchange(ctx, msg)\n\tif err != nil {\n\t\treturn false, \"\", err\n\t}\n\n\tfor _, r := range resp.Answer {\n\t\tswitch r := r.(type) {\n\t\tcase *dns.A:\n\t\t\trname = r.Hdr.Name\n\t\t\tad = resp.AuthenticatedData // Use AD flag from response we used to determine rname\n\t\t}\n\t}\n\n\tif rname == \"\" {\n\t\t// IPv6-only host? Try to find out rname using AAAA lookup.\n\t\tmsg := new(dns.Msg)\n\t\tmsg.SetQuestion(dns.Fqdn(host), dns.TypeAAAA)\n\t\tmsg.SetEdns0(4096, false)\n\t\tmsg.AuthenticatedData = true\n\t\tresp, err := e.exchange(ctx, msg)\n\t\tif err == nil {\n\t\t\tfor _, r := range resp.Answer {\n\t\t\t\tswitch r := r.(type) {\n\t\t\t\tcase *dns.AAAA:\n\t\t\t\t\trname = r.Hdr.Name\n\t\t\t\t\tad = resp.AuthenticatedData\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn ad, rname, nil\n}\n\nfunc (e ExtResolver) AuthLookupCNAME(ctx context.Context, host string) (ad bool, cname string, err error) {\n\tmsg := new(dns.Msg)\n\tmsg.SetQuestion(dns.Fqdn(host), dns.TypeCNAME)\n\tmsg.SetEdns0(4096, false)\n\tmsg.AuthenticatedData = true\n\tresp, err := e.exchange(ctx, msg)\n\tif err != nil {\n\t\treturn false, \"\", err\n\t}\n\n\tfor _, r := range resp.Answer {\n\t\tcnameR, ok := r.(*dns.CNAME)\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\t\treturn resp.AuthenticatedData, cnameR.Target, nil\n\t}\n\n\treturn resp.AuthenticatedData, \"\", nil\n}\n\nfunc (e ExtResolver) AuthLookupIPAddr(ctx context.Context, host string) (ad bool, addrs []net.IPAddr, err error) {\n\t// First, query IPv6.\n\tmsg := new(dns.Msg)\n\tmsg.SetQuestion(dns.Fqdn(host), dns.TypeAAAA)\n\tmsg.SetEdns0(4096, false)\n\tmsg.AuthenticatedData = true\n\n\tresp, err := e.exchange(ctx, msg)\n\taaaaFailed := false\n\tvar (\n\t\tv6ad    bool\n\t\tv6addrs []net.IPAddr\n\t)\n\tif err != nil {\n\t\t// Disregard the error for AAAA lookups.\n\t\taaaaFailed = true\n\t\tlog.DefaultLogger.Error(\"Network I/O error during AAAA lookup\", err, \"host\", host)\n\t} else {\n\t\tv6addrs = make([]net.IPAddr, 0, len(resp.Answer))\n\t\tv6ad = resp.AuthenticatedData\n\t\tfor _, rr := range resp.Answer {\n\t\t\taaaaRR, ok := rr.(*dns.AAAA)\n\t\t\tif !ok {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tv6addrs = append(v6addrs, net.IPAddr{IP: aaaaRR.AAAA})\n\t\t}\n\t}\n\n\t// Then repeat query with IPv4.\n\tmsg = new(dns.Msg)\n\tmsg.SetQuestion(dns.Fqdn(host), dns.TypeA)\n\tmsg.SetEdns0(4096, false)\n\tmsg.AuthenticatedData = true\n\n\tresp, err = e.exchange(ctx, msg)\n\tvar (\n\t\tv4ad    bool\n\t\tv4addrs []net.IPAddr\n\t)\n\tif err != nil {\n\t\tif aaaaFailed {\n\t\t\treturn false, nil, err\n\t\t}\n\t\t// Disregard A lookup error if AAAA succeeded.\n\t\tlog.DefaultLogger.Error(\"Network I/O error during A lookup, using AAAA records\", err, \"host\", host)\n\t} else {\n\t\tv4ad = resp.AuthenticatedData\n\t\tv4addrs = make([]net.IPAddr, 0, len(resp.Answer))\n\t\tfor _, rr := range resp.Answer {\n\t\t\taRR, ok := rr.(*dns.A)\n\t\t\tif !ok {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tv4addrs = append(v4addrs, net.IPAddr{IP: aRR.A})\n\t\t}\n\t}\n\n\t// A little bit of careful handling is required if AD is inconsistent\n\t// for A and AAAA queries. This unfortunatenly happens in practice. For\n\t// purposes of DANE handling (A/AAAA check) we disregard AAAA records\n\t// if they are not authenctiated and return only A records with AD=true.\n\n\taddrs = make([]net.IPAddr, 0, len(v4addrs)+len(v6addrs))\n\tif !v6ad && !v4ad {\n\t\taddrs = append(addrs, v6addrs...)\n\t\taddrs = append(addrs, v4addrs...)\n\t} else {\n\t\tif v6ad {\n\t\t\taddrs = append(addrs, v6addrs...)\n\t\t}\n\t\taddrs = append(addrs, v4addrs...)\n\t}\n\treturn v4ad, addrs, nil\n}\n\nfunc (e ExtResolver) AuthLookupTLSA(ctx context.Context, service, network, domain string) (ad bool, recs []TLSA, err error) {\n\tname, err := dns.TLSAName(domain, service, network)\n\tif err != nil {\n\t\treturn false, nil, err\n\t}\n\n\tmsg := new(dns.Msg)\n\tmsg.SetQuestion(dns.Fqdn(name), dns.TypeTLSA)\n\tmsg.SetEdns0(4096, false)\n\tmsg.AuthenticatedData = true\n\n\tresp, err := e.exchange(ctx, msg)\n\tif err != nil {\n\t\treturn false, nil, err\n\t}\n\n\tad = resp.AuthenticatedData\n\trecs = make([]dns.TLSA, 0, len(resp.Answer))\n\tfor _, rr := range resp.Answer {\n\t\trr, ok := rr.(*dns.TLSA)\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\n\t\trecs = append(recs, *rr)\n\t}\n\treturn\n}\n\nfunc NewExtResolver() (*ExtResolver, error) {\n\tcfg, err := dns.ClientConfigFromFile(\"/etc/resolv.conf\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif overrideServ != \"\" && overrideServ != \"system-default\" {\n\t\thost, port, err := net.SplitHostPort(overrideServ)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tcfg.Servers = []string{host}\n\t\tcfg.Port = port\n\t}\n\n\tif len(cfg.Servers) == 0 {\n\t\tcfg.Servers = []string{\"127.0.0.1\"}\n\t}\n\n\tcl := new(dns.Client)\n\tcl.Dialer = &net.Dialer{\n\t\tTimeout: time.Duration(cfg.Timeout) * time.Second,\n\t}\n\treturn &ExtResolver{\n\t\tcl:  cl,\n\t\tCfg: cfg,\n\t}, nil\n}\n"
  },
  {
    "path": "framework/dns/dnssec_test.go",
    "content": "package dns\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net\"\n\t\"reflect\"\n\t\"strconv\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/foxcpp/maddy/framework/log\"\n\t\"github.com/miekg/dns\"\n\t\"github.com/stretchr/testify/require\"\n)\n\ntype TestSrvAction int\n\nconst (\n\tTestSrvTimeout TestSrvAction = iota\n\tTestSrvServfail\n\tTestSrvNoAddr\n\tTestSrvOk\n)\n\nfunc (a TestSrvAction) String() string {\n\tswitch a {\n\tcase TestSrvTimeout:\n\t\treturn \"SrvTimeout\"\n\tcase TestSrvServfail:\n\t\treturn \"SrvServfail\"\n\tcase TestSrvNoAddr:\n\t\treturn \"SrvNoAddr\"\n\tcase TestSrvOk:\n\t\treturn \"SrvOk\"\n\tdefault:\n\t\tpanic(\"wtf action\")\n\t}\n}\n\ntype IPAddrTestServer struct {\n\tudpServ    dns.Server\n\taAction    TestSrvAction\n\taAD        bool\n\taaaaAction TestSrvAction\n\taaaaAD     bool\n}\n\nfunc (s *IPAddrTestServer) Run() {\n\tpconn, err := net.ListenPacket(\"udp4\", \"127.0.0.1:0\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\ts.udpServ.PacketConn = pconn\n\ts.udpServ.Handler = s\n\tgo s.udpServ.ActivateAndServe() //nolint:errcheck\n}\n\nfunc (s *IPAddrTestServer) Close() error {\n\treturn s.udpServ.PacketConn.Close()\n}\n\nfunc (s *IPAddrTestServer) Addr() *net.UDPAddr {\n\treturn s.udpServ.PacketConn.LocalAddr().(*net.UDPAddr)\n}\n\nfunc (s *IPAddrTestServer) ServeDNS(w dns.ResponseWriter, m *dns.Msg) {\n\tq := m.Question[0]\n\n\tvar (\n\t\tact TestSrvAction\n\t\tad  bool\n\t)\n\tswitch q.Qtype {\n\tcase dns.TypeA:\n\t\tact = s.aAction\n\t\tad = s.aAD\n\tcase dns.TypeAAAA:\n\t\tact = s.aaaaAction\n\t\tad = s.aaaaAD\n\tdefault:\n\t\tpanic(\"wtf qtype\")\n\t}\n\n\treply := new(dns.Msg)\n\treply.SetReply(m)\n\treply.RecursionAvailable = true\n\treply.AuthenticatedData = ad\n\n\tswitch act {\n\tcase TestSrvTimeout:\n\t\treturn // no nobody heard from him since...\n\tcase TestSrvServfail:\n\t\treply.Rcode = dns.RcodeServerFailure\n\tcase TestSrvNoAddr:\n\tcase TestSrvOk:\n\t\tswitch q.Qtype {\n\t\tcase dns.TypeA:\n\t\t\treply.Answer = append(reply.Answer, &dns.A{\n\t\t\t\tHdr: dns.RR_Header{\n\t\t\t\t\tName:   q.Name,\n\t\t\t\t\tRrtype: dns.TypeA,\n\t\t\t\t\tClass:  dns.ClassINET,\n\t\t\t\t\tTtl:    9999,\n\t\t\t\t},\n\t\t\t\tA: net.ParseIP(\"127.0.0.1\"),\n\t\t\t})\n\t\tcase dns.TypeAAAA:\n\t\t\treply.Answer = append(reply.Answer, &dns.AAAA{\n\t\t\t\tHdr: dns.RR_Header{\n\t\t\t\t\tName:   q.Name,\n\t\t\t\t\tRrtype: dns.TypeAAAA,\n\t\t\t\t\tClass:  dns.ClassINET,\n\t\t\t\t\tTtl:    9999,\n\t\t\t\t},\n\t\t\t\tAAAA: net.ParseIP(\"::1\"),\n\t\t\t})\n\t\t}\n\t}\n\n\tif err := w.WriteMsg(reply); err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc TestExtResolver_AuthLookupIPAddr(t *testing.T) {\n\t// AuthLookupIPAddr has a rather convoluted logic for combined A/AAAA\n\t// lookups that return the best-effort result and also has some nuanced in\n\t// AD flag handling for use in DANE algorithms.\n\n\t// Silence log messages about disregarded I/O errors.\n\toldLog := log.DefaultLogger\n\tlog.DefaultLogger = log.NopLogger\n\tt.Cleanup(func() {\n\t\tlog.DefaultLogger = oldLog\n\t})\n\n\ttest := func(aAct, aaaaAct TestSrvAction, aAD, aaaaAD, ad bool, addrs []net.IP, err bool) {\n\t\tt.Helper()\n\t\tt.Run(fmt.Sprintln(aAct, aaaaAct, aAD, aaaaAD), func(t *testing.T) {\n\t\t\tt.Helper()\n\n\t\t\ts := IPAddrTestServer{}\n\t\t\ts.aAction = aAct\n\t\t\ts.aaaaAction = aaaaAct\n\t\t\ts.aAD = aAD\n\t\t\ts.aaaaAD = aaaaAD\n\t\t\ts.Run()\n\t\t\tdefer func() {\n\t\t\t\trequire.NoError(t, s.Close())\n\t\t\t}()\n\t\t\tres := ExtResolver{\n\t\t\t\tcl: new(dns.Client),\n\t\t\t\tCfg: &dns.ClientConfig{\n\t\t\t\t\tServers: []string{\"127.0.0.1\"},\n\t\t\t\t\tPort:    strconv.Itoa(s.Addr().Port),\n\t\t\t\t\tTimeout: 1,\n\t\t\t\t},\n\t\t\t}\n\t\t\tres.cl.Dialer = &net.Dialer{\n\t\t\t\tTimeout: 500 * time.Millisecond,\n\t\t\t}\n\n\t\t\tctx, cancel := context.WithCancel(context.Background())\n\t\t\tdefer cancel()\n\n\t\t\tactualAd, actualAddrs, actualErr := res.AuthLookupIPAddr(ctx, \"maddy.test\")\n\t\t\tif (actualErr != nil) != err {\n\t\t\t\tt.Fatal(\"actualErr:\", actualErr, \"expectedErr:\", err)\n\t\t\t}\n\t\t\tif actualAd != ad {\n\t\t\t\tt.Error(\"actualAd:\", actualAd, \"expectedAd:\", ad)\n\t\t\t}\n\t\t\tipAddrs := make([]net.IPAddr, 0, len(addrs))\n\t\t\tif len(addrs) == 0 {\n\t\t\t\tipAddrs = nil // lookup returns nil addrs for error cases\n\t\t\t}\n\t\t\tfor _, a := range addrs {\n\t\t\t\tipAddrs = append(ipAddrs, net.IPAddr{IP: a, Zone: \"\"})\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(actualAddrs, ipAddrs) {\n\t\t\t\tt.Logf(\"actualAddrs: %#+v\", actualAddrs)\n\t\t\t\tt.Logf(\"addrs: %#+v\", ipAddrs)\n\t\t\t\tt.Fail()\n\t\t\t}\n\t\t})\n\t}\n\n\ttest(TestSrvOk, TestSrvOk, true, true, true, []net.IP{net.ParseIP(\"::1\"), net.ParseIP(\"127.0.0.1\").To4()}, false)\n\ttest(TestSrvOk, TestSrvOk, true, false, true, []net.IP{net.ParseIP(\"127.0.0.1\").To4()}, false)\n\ttest(TestSrvOk, TestSrvOk, false, true, false, []net.IP{net.ParseIP(\"::1\"), net.ParseIP(\"127.0.0.1\").To4()}, false)\n\ttest(TestSrvOk, TestSrvOk, false, false, false, []net.IP{net.ParseIP(\"::1\"), net.ParseIP(\"127.0.0.1\").To4()}, false)\n\ttest(TestSrvOk, TestSrvTimeout, true, true, true, []net.IP{net.ParseIP(\"127.0.0.1\").To4()}, false)\n\ttest(TestSrvOk, TestSrvServfail, true, true, true, []net.IP{net.ParseIP(\"127.0.0.1\").To4()}, false)\n\ttest(TestSrvOk, TestSrvNoAddr, true, true, true, []net.IP{net.ParseIP(\"127.0.0.1\").To4()}, false)\n\ttest(TestSrvNoAddr, TestSrvOk, true, true, true, []net.IP{net.ParseIP(\"::1\")}, false)\n\ttest(TestSrvServfail, TestSrvServfail, true, true, false, nil, true)\n\n\t// actualAd is false, we don't want to risk reporting positive AD result if\n\t// something is wrong with IPv4 lookup.\n\ttest(TestSrvTimeout, TestSrvOk, true, true, false, []net.IP{net.ParseIP(\"::1\")}, false)\n\ttest(TestSrvServfail, TestSrvOk, true, true, false, []net.IP{net.ParseIP(\"::1\")}, false)\n}\n"
  },
  {
    "path": "framework/dns/idna.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage dns\n\nimport (\n\t\"golang.org/x/net/idna\"\n\t\"golang.org/x/text/unicode/norm\"\n)\n\n// SelectIDNA is a convenience function for encoding to/from Punycode.\n//\n// If ulabel is true, it returns U-label encoded domain in the Unicode NFC\n// form.\n// If ulabel is false, it returns A-label encoded domain.\nfunc SelectIDNA(ulabel bool, domain string) (string, error) {\n\tif ulabel {\n\t\tuDomain, err := idna.ToUnicode(domain)\n\t\treturn norm.NFC.String(uDomain), err\n\t}\n\treturn idna.ToASCII(domain)\n}\n"
  },
  {
    "path": "framework/dns/norm.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage dns\n\nimport (\n\t\"strings\"\n\n\t\"github.com/miekg/dns\"\n\t\"golang.org/x/net/idna\"\n\t\"golang.org/x/text/unicode/norm\"\n)\n\nfunc FQDN(domain string) string {\n\treturn dns.Fqdn(domain)\n}\n\n// ForLookup converts the domain into a canonical form suitable for table\n// lookups and other comparisons.\n//\n// TL;DR Use this instead of strings.ToLower to prepare domain for lookups.\n//\n// Domains that contain invalid UTF-8 or invalid A-label\n// domains are simply converted to local-case using strings.ToLower, but the\n// error is also returned.\nfunc ForLookup(domain string) (string, error) {\n\tuDomain, err := idna.ToUnicode(domain)\n\tif err != nil {\n\t\treturn strings.ToLower(domain), err\n\t}\n\n\t// Side note: strings.ToLower does not support full case-folding, so it is\n\t// important to apply NFC normalization first.\n\tuDomain = norm.NFC.String(uDomain)\n\tuDomain = strings.ToLower(uDomain)\n\tuDomain = strings.TrimSuffix(uDomain, \".\")\n\treturn uDomain, nil\n}\n\n// Equal reports whether domain1 and domain2 are equivalent as defined by\n// IDNA2008 (RFC 5890).\n//\n// TL;DR Use this instead of strings.EqualFold to compare domains.\n//\n// Equivalence for malformed A-label domains is defined using regular\n// byte-string comparison with case-folding applied.\nfunc Equal(domain1, domain2 string) bool {\n\t// Short circult. If they are bit-equivalent, then they are also semantically\n\t// equivalent.\n\tif domain1 == domain2 {\n\t\treturn true\n\t}\n\n\tuDomain1, _ := ForLookup(domain1)\n\tuDomain2, _ := ForLookup(domain2)\n\treturn uDomain1 == uDomain2\n}\n"
  },
  {
    "path": "framework/dns/override.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage dns\n\nimport (\n\t\"context\"\n\t\"net\"\n\t\"time\"\n)\n\nvar overrideServ string\n\n// override globally overrides the used DNS server address with one provided.\n// This function is meant only for testing. It should be called before any modules are\n// initialized to have full effect.\n//\n// The server argument is in form of \"IP:PORT\". It is expected that the server\n// will be available both using TCP and UDP on the same port.\nfunc override(server string) { // nolint: unused // used in debugflags.go\n\tnet.DefaultResolver.PreferGo = true\n\tnet.DefaultResolver.Dial = func(ctx context.Context, network, _ string) (net.Conn, error) {\n\t\tdialer := net.Dialer{\n\t\t\t// This is localhost, it is either running or not. Fail quickly if\n\t\t\t// we can't connect.\n\t\t\tTimeout: 1 * time.Second,\n\t\t}\n\n\t\tswitch network {\n\t\tcase \"udp\", \"udp4\", \"udp6\":\n\t\t\treturn dialer.DialContext(ctx, \"udp4\", server)\n\t\tcase \"tcp\", \"tcp4\", \"tcp6\":\n\t\t\treturn dialer.DialContext(ctx, \"tcp4\", server)\n\t\tdefault:\n\t\t\tpanic(\"OverrideDNS.Dial: unknown network\")\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "framework/dns/resolver.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\n// Package dns defines interfaces used by maddy modules to perform DNS\n// lookups.\n//\n// Currently, there is only Resolver interface which is implemented\n// by dns.DefaultResolver(). In the future, DNSSEC-enabled stub resolver\n// implementation will be added here.\npackage dns\n\nimport (\n\t\"context\"\n\t\"net\"\n\t\"strings\"\n)\n\n// Resolver is an interface that describes DNS-related methods used by maddy.\n//\n// It is implemented by dns.DefaultResolver(). Methods behave the same way.\ntype Resolver interface {\n\tLookupAddr(ctx context.Context, addr string) (names []string, err error)\n\tLookupHost(ctx context.Context, host string) (addrs []string, err error)\n\tLookupMX(ctx context.Context, name string) ([]*net.MX, error)\n\tLookupTXT(ctx context.Context, name string) ([]string, error)\n\tLookupIPAddr(ctx context.Context, host string) ([]net.IPAddr, error)\n}\n\n// LookupAddr is a convenience wrapper for Resolver.LookupAddr.\n//\n// It returns the first name with trailing dot stripped.\nfunc LookupAddr(ctx context.Context, r Resolver, ip net.IP) (string, error) {\n\tnames, err := r.LookupAddr(ctx, ip.String())\n\tif err != nil || len(names) == 0 {\n\t\treturn \"\", err\n\t}\n\treturn strings.TrimRight(names[0], \".\"), nil\n}\n\nfunc DefaultResolver() Resolver {\n\treturn net.DefaultResolver\n}\n"
  },
  {
    "path": "framework/exterrors/dns.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage exterrors\n\nimport (\n\t\"net\"\n)\n\nfunc UnwrapDNSErr(err error) (reason string, misc map[string]interface{}) {\n\tdnsErr, ok := err.(*net.DNSError)\n\tif !ok {\n\t\t// Return non-nil in case the user will try to 'extend' it with its own\n\t\t// values.\n\t\treturn \"\", map[string]interface{}{}\n\t}\n\n\t// Nor server name, nor DNS name are usually useful, so exclude them.\n\treturn dnsErr.Err, map[string]interface{}{}\n}\n"
  },
  {
    "path": "framework/exterrors/exterrors.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\n// Package errors defines error-handling and primitives\n// used across maddy, notably to pass additional error\n// information across module boundaries.\npackage exterrors\n"
  },
  {
    "path": "framework/exterrors/fields.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage exterrors\n\ntype fieldsErr interface {\n\tFields() map[string]interface{}\n}\n\ntype unwrapper interface {\n\tUnwrap() error\n}\n\ntype fieldsWrap struct {\n\terr    error\n\tfields map[string]interface{}\n}\n\nfunc (fw fieldsWrap) Error() string {\n\treturn fw.err.Error()\n}\n\nfunc (fw fieldsWrap) Unwrap() error {\n\treturn fw.err\n}\n\nfunc (fw fieldsWrap) Fields() map[string]interface{} {\n\treturn fw.fields\n}\n\nfunc Fields(err error) map[string]interface{} {\n\tfields := make(map[string]interface{}, 5)\n\n\tfor err != nil {\n\t\terrFields, ok := err.(fieldsErr)\n\t\tif ok {\n\t\t\tfor k, v := range errFields.Fields() {\n\t\t\t\t// Outer errors override fields of the inner ones.\n\t\t\t\t// Not the reverse.\n\t\t\t\tif fields[k] != nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tfields[k] = v\n\t\t\t}\n\t\t}\n\n\t\tunwrap, ok := err.(unwrapper)\n\t\tif !ok {\n\t\t\tbreak\n\t\t}\n\t\terr = unwrap.Unwrap()\n\t}\n\n\treturn fields\n}\n\nfunc WithFields(err error, fields map[string]interface{}) error {\n\treturn fieldsWrap{err: err, fields: fields}\n}\n"
  },
  {
    "path": "framework/exterrors/smtp.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage exterrors\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/emersion/go-smtp\"\n)\n\ntype EnhancedCode smtp.EnhancedCode\n\nfunc (ec EnhancedCode) FormatLog() string {\n\treturn fmt.Sprintf(\"%d.%d.%d\", ec[0], ec[1], ec[2])\n}\n\n// SMTPError type is a copy of emersion/go-smtp.SMTPError type\n// that extends it with Fields method for logging and reporting\n// in maddy. It should be used instead of the go-smtp library type for all\n// errors.\ntype SMTPError struct {\n\t// SMTP status code. Most of these codes are overly generic and are barely\n\t// useful. Nonetheless, take a look at the 'Associated basic status code'\n\t// in the SMTP Enhanced Status Codes registry (below), then check RFC 5321\n\t// (Section 4.3.2) and pick what you like. Stick to 451 and 554 if there are\n\t// no useful codes.\n\tCode int\n\n\t// Enhanced SMTP status code. If you are unsure, take a look at\n\t// https://www.iana.org/assignments/smtp-enhanced-status-codes/smtp-enhanced-status-codes.xhtml\n\tEnhancedCode EnhancedCode\n\n\t// Error message that should be returned to the SMTP client.\n\t// Usually, it should be a short and generic description of the error\n\t// that excludes any details. Especially, for checks, avoid\n\t// mentioning the exact policy mechanism used to avoid disclosing the\n\t// server configuration details. Don't say \"DNS error during DMARC check\",\n\t// say \"DNS error during policy check\". Same goes for network and file I/O\n\t// errors. ESPECIALLY, don't include any configuration variables or object\n\t// identifiers in it.\n\tMessage string\n\n\t// If the error was generated by a message check\n\t// this field includes module name.\n\tCheckName string\n\n\t// If the error was generated by a delivery target\n\t// this field includes module name.\n\tTargetName string\n\n\t// If the error was generated by a message modifier\n\t// this field includes module name.\n\tModifierName string\n\n\t// If the error was generated as a result of another\n\t// error - this field contains the original error object.\n\t//\n\t// Err.Error() will be copied into the 'reason' field returned\n\t// by the Fields method unless a different values is specified\n\t// using the Reason field below.\n\tErr error\n\n\t// Textual explanation of the actual error reason. Defaults to the\n\t// Err.Error() value if Err is not nil, empty string otherwise.\n\tReason string\n\n\tMisc map[string]interface{}\n}\n\nfunc (se *SMTPError) Unwrap() error {\n\treturn se.Err\n}\n\nfunc (se *SMTPError) Fields() map[string]interface{} {\n\tctx := make(map[string]interface{}, len(se.Misc)+3)\n\tfor k, v := range se.Misc {\n\t\tctx[k] = v\n\t}\n\tctx[\"smtp_code\"] = se.Code\n\tctx[\"smtp_enchcode\"] = se.EnhancedCode\n\tctx[\"smtp_msg\"] = se.Message\n\tif se.CheckName != \"\" {\n\t\tctx[\"check\"] = se.CheckName\n\t}\n\tif se.TargetName != \"\" {\n\t\tctx[\"target\"] = se.TargetName\n\t}\n\tif se.Reason != \"\" {\n\t\tctx[\"reason\"] = se.Reason\n\t} else if se.Err != nil {\n\t\tctx[\"reason\"] = se.Err.Error()\n\t}\n\treturn ctx\n}\n\n// Temporary reports whether\nfunc (se *SMTPError) Temporary() bool {\n\treturn se.Code/100 == 4\n}\n\nfunc (se *SMTPError) Error() string {\n\tif se.Reason != \"\" {\n\t\treturn se.Reason\n\t}\n\tif se.Err != nil {\n\t\treturn se.Err.Error()\n\t}\n\treturn se.Message\n}\n\n// SMTPCode is a convenience function that returns one of its arguments\n// depending on the result of exterrors.IsTemporary for the specified error\n// object.\nfunc SMTPCode(err error, temporaryCode, permanentCode int) int {\n\tif IsTemporary(err) {\n\t\treturn temporaryCode\n\t}\n\treturn permanentCode\n}\n\n// SMTPEnchCode is a convenience function changes the first number of the SMTP enhanced\n// status code based on the value exterrors.IsTemporary returns for the specified\n// error object.\nfunc SMTPEnchCode(err error, code EnhancedCode) EnhancedCode {\n\tif IsTemporary(err) {\n\t\tcode[0] = 4\n\t}\n\tcode[0] = 5\n\treturn code\n}\n"
  },
  {
    "path": "framework/exterrors/temporary.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage exterrors\n\nimport (\n\t\"errors\"\n)\n\ntype TemporaryErr interface {\n\tTemporary() bool\n}\n\n// IsTemporaryOrUnspec is similar to IsTemporary except that it returns true\n// if error does not have a Temporary() method. Basically, it assumes that\n// errors are temporary by default compared to IsTemporary that assumes\n// errors are permanent by default.\nfunc IsTemporaryOrUnspec(err error) bool {\n\tvar temp TemporaryErr\n\tif errors.As(err, &temp) {\n\t\treturn temp.Temporary()\n\t}\n\treturn true\n}\n\n// IsTemporary returns true whether the passed error object\n// have a Temporary() method and it returns true.\nfunc IsTemporary(err error) bool {\n\tvar temp TemporaryErr\n\tif errors.As(err, &temp) {\n\t\treturn temp.Temporary()\n\t}\n\treturn false\n}\n\ntype temporaryErr struct {\n\terr  error\n\ttemp bool\n}\n\nfunc (t temporaryErr) Unwrap() error {\n\treturn t.err\n}\n\nfunc (t temporaryErr) Error() string {\n\treturn t.err.Error()\n}\n\nfunc (t temporaryErr) Temporary() bool {\n\treturn t.temp\n}\n\n// WithTemporary wraps the passed error object with the implementation of the\n// Temporary() method that will return the specified value.\n//\n// Original error value can be obtained using errors.Unwrap.\nfunc WithTemporary(err error, temporary bool) error {\n\treturn temporaryErr{err, temporary}\n}\n"
  },
  {
    "path": "framework/future/future.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage future\n\nimport (\n\t\"context\"\n\t\"runtime/debug\"\n\t\"sync\"\n\n\t\"github.com/foxcpp/maddy/framework/log\"\n)\n\n// The Future object implements a container for (value, error) pair that \"will\n// be populated later\" and allows multiple users to wait for it to be set.\n//\n// It should not be copied after first use.\ntype Future struct {\n\tmu  sync.RWMutex\n\tset bool\n\tval interface{}\n\terr error\n\n\tnotify chan struct{}\n}\n\nfunc New() *Future {\n\treturn &Future{notify: make(chan struct{})}\n}\n\n// Set sets the Future (value, error) pair. All currently blocked and future\n// Get calls will return it.\nfunc (f *Future) Set(val interface{}, err error) {\n\tif f == nil {\n\t\tpanic(\"nil future used\")\n\t}\n\n\tf.mu.Lock()\n\tdefer f.mu.Unlock()\n\n\tif f.set {\n\t\tstack := debug.Stack()\n\t\tlog.Println(\"Future.Set called multiple times\", stack)\n\t\tlog.Println(\"value=\", val, \"err=\", err)\n\t\treturn\n\t}\n\n\tf.set = true\n\tf.val = val\n\tf.err = err\n\n\tclose(f.notify)\n}\n\nfunc (f *Future) Get() (interface{}, error) {\n\tif f == nil {\n\t\tpanic(\"nil future used\")\n\t}\n\n\treturn f.GetContext(context.Background())\n}\n\nfunc (f *Future) GetContext(ctx context.Context) (interface{}, error) {\n\tif f == nil {\n\t\tpanic(\"nil future used\")\n\t}\n\n\tf.mu.RLock()\n\tif f.set {\n\t\tval := f.val\n\t\terr := f.err\n\t\tf.mu.RUnlock()\n\t\treturn val, err\n\t}\n\n\tf.mu.RUnlock()\n\tselect {\n\tcase <-f.notify:\n\tcase <-ctx.Done():\n\t\treturn nil, ctx.Err()\n\t}\n\n\tf.mu.RLock()\n\tdefer f.mu.RUnlock()\n\tif !f.set {\n\t\tpanic(\"future: Notification received, but value is not set\")\n\t}\n\n\treturn f.val, f.err\n}\n"
  },
  {
    "path": "framework/future/future_test.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage future\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestFuture_SetBeforeGet(t *testing.T) {\n\tf := New()\n\n\tf.Set(1, errors.New(\"1\"))\n\tval, err := f.Get()\n\tif err.Error() != \"1\" {\n\t\tt.Error(\"Wrong error:\", err)\n\t}\n\n\tif val, _ := val.(int); val != 1 {\n\t\tt.Fatal(\"wrong val received from Get\")\n\t}\n}\n\nfunc TestFuture_Wait(t *testing.T) {\n\tf := New()\n\n\tgo func() {\n\t\ttime.Sleep(500 * time.Millisecond)\n\t\tf.Set(1, errors.New(\"1\"))\n\t}()\n\n\tval, err := f.Get()\n\tif val, _ := val.(int); val != 1 {\n\t\tt.Fatal(\"wrong val received from Get\")\n\t}\n\tif err.Error() != \"1\" {\n\t\tt.Error(\"Wrong error:\", err)\n\t}\n\n\tval, err = f.Get()\n\tif val, _ := val.(int); val != 1 {\n\t\tt.Fatal(\"wrong val received from Get on second try\")\n\t}\n\tif err.Error() != \"1\" {\n\t\tt.Error(\"Wrong error:\", err)\n\t}\n}\n\nfunc TestFuture_WaitCtx(t *testing.T) {\n\tf := New()\n\tctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)\n\tdefer cancel()\n\t_, err := f.GetContext(ctx)\n\tif !errors.Is(err, context.DeadlineExceeded) {\n\t\tt.Fatal(\"context is not cancelled\")\n\t}\n}\n"
  },
  {
    "path": "framework/hooks/hooks.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage hooks\n\nimport \"sync\"\n\ntype Event int\n\nconst (\n\t// EventShutdown is triggered when the server process is about to stop.\n\tEventShutdown Event = iota\n\n\t// EventReload is triggered when the server process receives the SIGUSR2\n\t// signal (on POSIX platforms) and indicates the request to reload the\n\t// server configuration from persistent storage.\n\t//\n\t// Since it is by design problematic to reload the modules configuration,\n\t// this event only applies to secondary files such as aliases mapping and\n\t// TLS certificates.\n\tEventReload\n\n\t// EventLogRotate is triggered when the server process receives the SIGUSR1\n\t// signal (on POSIX platforms) and indicates the request to reopen used log\n\t// files since they might have rotated.\n\tEventLogRotate\n)\n\nvar (\n\thooks    = make(map[Event][]func())\n\thooksLck sync.Mutex\n)\n\nfunc hooksToRun(eventName Event) []func() {\n\thooksLck.Lock()\n\tdefer hooksLck.Unlock()\n\thooksEv := hooks[eventName]\n\tif hooksEv == nil {\n\t\treturn nil\n\t}\n\n\t// The slice is copied so hooks can be run without holding the lock what\n\t// might be important since they are likely to do a lot of I/O.\n\thooksEvCpy := make([]func(), 0, len(hooksEv))\n\thooksEvCpy = append(hooksEvCpy, hooksEv...)\n\n\treturn hooksEvCpy\n}\n\n// RunHooks runs the hooks installed for the specified eventName in the reverse\n// order.\nfunc RunHooks(eventName Event) {\n\thooks := hooksToRun(eventName)\n\tfor i := len(hooks) - 1; i >= 0; i-- {\n\t\thooks[i]()\n\t}\n}\n\n// AddHook installs the hook to be executed when certain event occurs.\nfunc AddHook(eventName Event, f func()) {\n\thooksLck.Lock()\n\tdefer hooksLck.Unlock()\n\n\thooks[eventName] = append(hooks[eventName], f)\n}\n"
  },
  {
    "path": "framework/log/log.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\n// Package log implements a minimalistic logging library.\npackage log\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/foxcpp/maddy/framework/exterrors\"\n\t\"go.uber.org/zap\"\n)\n\n// Logger is the structure that writes formatted output to the underlying\n// log.Output object.\n//\n// Logger is stateless and can be copied freely.  However, consider that\n// underlying log.Output will not be copied.\n//\n// Each log message is prefixed with logger name.  Timestamp and debug flag\n// formatting is done by log.Output.\n//\n// No serialization is provided by Logger, its log.Output responsibility to\n// ensure goroutine-safety if necessary.\ntype Logger struct {\n\tParent *Logger\n\n\tOut   Output\n\tName  string\n\tDebug bool\n\n\t// Additional fields that will be added\n\t// to the Msg output.\n\tFields map[string]interface{}\n}\n\nfunc (l *Logger) Zap() *zap.Logger {\n\t// TODO: Migrate to using zap natively.\n\treturn zap.New(zapLogger{L: l})\n}\n\nfunc (l *Logger) IsDebug() bool {\n\treturn l.Debug || (l.Parent != nil && l.Parent.IsDebug())\n}\n\nfunc (l *Logger) Debugf(format string, val ...interface{}) {\n\tif !l.IsDebug() {\n\t\treturn\n\t}\n\tl.log(true, l.formatMsg(fmt.Sprintf(format, val...), nil))\n}\n\nfunc (l *Logger) Debugln(val ...interface{}) {\n\tif !l.IsDebug() {\n\t\treturn\n\t}\n\tl.log(true, l.formatMsg(strings.TrimRight(fmt.Sprintln(val...), \"\\n\"), nil))\n}\n\nfunc (l *Logger) Printf(format string, val ...interface{}) {\n\tl.log(false, l.formatMsg(fmt.Sprintf(format, val...), nil))\n}\n\nfunc (l *Logger) Println(val ...interface{}) {\n\tl.log(false, l.formatMsg(strings.TrimRight(fmt.Sprintln(val...), \"\\n\"), nil))\n}\n\n// Msg writes an event log message in a machine-readable format (currently\n// JSON).\n//\n//\tname: msg\\t{\"key\":\"value\",\"key2\":\"value2\"}\n//\n// Key-value pairs are built from fields slice which should contain key strings\n// followed by corresponding values.  That is, for example, []interface{\"key\",\n// \"value\", \"key2\", \"value2\"}.\n//\n// If value in fields implements Formatter, it will be represented by the\n// string returned by FormatLog method. Same goes for fmt.Stringer and error\n// interfaces.\n//\n// Additionally, time.Time is written as a string in ISO 8601 format.\n// time.Duration follows fmt.Stringer rule above.\nfunc (l *Logger) Msg(msg string, fields ...interface{}) {\n\tm := make(map[string]interface{}, len(fields)/2)\n\tfieldsToMap(fields, m)\n\tl.log(false, l.formatMsg(msg, m))\n}\n\n// Error writes an event log message in a machine-readable format (currently\n// JSON) containing information about the error. If err does have a Fields\n// method that returns map[string]interface{}, its result will be added to the\n// message.\n//\n//\tname: msg\\t{\"key\":\"value\",\"key2\":\"value2\"}\n//\n// Additionally, values from fields will be added to it, as handled by\n// Logger.Msg.\n//\n// In the context of Error method, \"msg\" typically indicates the top-level\n// context in which the error is *handled*. For example, if error leads to\n// rejection of SMTP DATA command, msg will probably be \"DATA error\".\nfunc (l *Logger) Error(msg string, err error, fields ...interface{}) {\n\tif err == nil {\n\t\treturn\n\t}\n\n\terrFields := exterrors.Fields(err)\n\tallFields := make(map[string]interface{}, len(fields)+len(errFields)+2)\n\tfor k, v := range errFields {\n\t\tallFields[k] = v\n\t}\n\n\t// If there is already a 'reason' field - use it, it probably\n\t// provides a better explanation than error text itself.\n\tif allFields[\"reason\"] == nil {\n\t\tallFields[\"reason\"] = err.Error()\n\t}\n\tfieldsToMap(fields, allFields)\n\n\tl.log(false, l.formatMsg(msg, allFields))\n}\n\nfunc (l *Logger) DebugMsg(kind string, fields ...interface{}) {\n\tif !l.IsDebug() {\n\t\treturn\n\t}\n\tm := make(map[string]interface{}, len(fields)/2)\n\tfieldsToMap(fields, m)\n\tl.log(true, l.formatMsg(kind, m))\n}\n\nfunc fieldsToMap(fields []interface{}, out map[string]interface{}) {\n\tvar lastKey string\n\tfor i, val := range fields {\n\t\tif i%2 == 0 {\n\t\t\t// Key\n\t\t\tkey, ok := val.(string)\n\t\t\tif !ok {\n\t\t\t\t// Misformatted arguments, attempt to provide useful message\n\t\t\t\t// anyway.\n\t\t\t\tout[fmt.Sprint(\"field\", i)] = key\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tlastKey = key\n\t\t} else {\n\t\t\t// Value\n\t\t\tout[lastKey] = val\n\t\t}\n\t}\n}\n\nfunc (l *Logger) formatMsg(msg string, fields map[string]interface{}) string {\n\tformatted := strings.Builder{}\n\n\tformatted.WriteString(msg)\n\tformatted.WriteRune('\\t')\n\n\tif len(l.Fields)+len(fields) != 0 {\n\t\tif fields == nil {\n\t\t\tfields = make(map[string]interface{})\n\t\t}\n\t\tfor k, v := range l.Fields {\n\t\t\tfields[k] = v\n\t\t}\n\t\tif err := marshalOrderedJSON(&formatted, fields); err != nil {\n\t\t\t// Fallback to printing the message with minimal processing.\n\t\t\treturn fmt.Sprintf(\"[BROKEN FORMATTING: %v] %v %+v\", err, msg, fields)\n\t\t}\n\t}\n\n\treturn formatted.String()\n}\n\ntype Formatter interface {\n\tFormatLog() string\n}\n\n// Write implements io.Writer, all bytes sent\n// to it will be written as a separate log messages.\n// No line-buffering is done.\nfunc (l *Logger) Write(s []byte) (int, error) {\n\tif !l.IsDebug() {\n\t\treturn len(s), nil\n\t}\n\tl.log(false, strings.TrimRight(string(s), \"\\n\"))\n\treturn len(s), nil\n}\n\n// DebugWriter returns a writer that will act like Logger.Write\n// but will use debug flag on messages. If Logger.Debug is false,\n// Write method of returned object will be no-op.\nfunc (l *Logger) DebugWriter() io.Writer {\n\tl2 := l.Sublogger(\"\")\n\tl2.Debug = true\n\treturn l2\n}\n\nfunc (l *Logger) output() Output {\n\tif l.Out != nil {\n\t\treturn l.Out\n\t}\n\tif l.Parent != nil {\n\t\treturn l.Parent.output()\n\t}\n\n\tif DefaultLogger.Out == nil {\n\t\tpanic(\"DefaultLogger.Out is not set\")\n\t}\n\tif l.Parent == nil && l != &DefaultLogger {\n\t\tDefaultLogger.Out.Write(time.Now(), true, \"logger \"+l.Name+\" has no parent, this is a bug\")\n\t}\n\treturn DefaultLogger.Out\n}\n\nfunc (l *Logger) log(debug bool, s string) {\n\tif l.Name != \"\" {\n\t\ts = l.Name + \": \" + s\n\t}\n\n\tout := l.output()\n\tout.Write(time.Now(), debug, s)\n\n\t// Logging is disabled - do nothing.\n}\n\nfunc (l *Logger) Sublogger(name string) *Logger {\n\tif l.Name != \"\" && name != \"\" {\n\t\tname = l.Name + \"/\" + name\n\t}\n\treturn &Logger{\n\t\tParent: l,\n\t\tName:   name,\n\t}\n}\n\n// DefaultLogger is the global Logger object that is used by\n// package-level logging functions.\n//\n// As with all other Loggers, it is not gorountine-safe on its own,\n// however underlying log.Output may provide necessary serialization.\nvar DefaultLogger = Logger{Out: WriterOutput(os.Stderr, false)}\n\n// NopLogger is the logger that discards all messages written to it.\nvar NopLogger = Logger{\n\tParent: &DefaultLogger,\n\tOut:    NopOutput{},\n}\n\nfunc Debugf(format string, val ...interface{}) { DefaultLogger.Debugf(format, val...) }\nfunc Debugln(val ...interface{})               { DefaultLogger.Debugln(val...) }\nfunc Printf(format string, val ...interface{}) { DefaultLogger.Printf(format, val...) }\nfunc Println(val ...interface{})               { DefaultLogger.Println(val...) }\n"
  },
  {
    "path": "framework/log/orderedjson.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage log\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"sort\"\n\t\"strings\"\n\t\"time\"\n)\n\n// To support ad-hoc parsing in a better way we want to make order of fields in\n// output JSON documents determistics. Additionally, this will make them more\n// human-readable when values from multiple messages are lined up to each\n// other.\n\ntype module interface {\n\tName() string\n\tInstanceName() string\n}\n\nfunc marshalOrderedJSON(output *strings.Builder, m map[string]interface{}) error {\n\torder := make([]string, 0, len(m))\n\tfor k := range m {\n\t\torder = append(order, k)\n\t}\n\tsort.Strings(order)\n\n\toutput.WriteRune('{')\n\tfor i, key := range order {\n\t\tif i != 0 {\n\t\t\toutput.WriteRune(',')\n\t\t}\n\n\t\tjsonKey, err := json.Marshal(key)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\toutput.Write(jsonKey)\n\t\toutput.WriteString(\":\")\n\n\t\tval := m[key]\n\t\tswitch casted := val.(type) {\n\t\tcase time.Time:\n\t\t\tval = casted.Format(\"2006-01-02T15:04:05.000\")\n\t\tcase time.Duration:\n\t\t\tval = casted.String()\n\t\tcase Formatter:\n\t\t\tval = casted.FormatLog()\n\t\tcase fmt.Stringer:\n\t\t\tval = casted.String()\n\t\tcase module:\n\t\t\tval = casted.Name() + \"/\" + casted.InstanceName()\n\t\tcase error:\n\t\t\tval = casted.Error()\n\t\t}\n\n\t\tjsonValue, err := json.Marshal(val)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\toutput.Write(jsonValue)\n\t}\n\toutput.WriteRune('}')\n\n\treturn nil\n}\n"
  },
  {
    "path": "framework/log/output.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage log\n\nimport (\n\t\"time\"\n)\n\ntype Output interface {\n\tWrite(stamp time.Time, debug bool, msg string)\n\tClose() error\n}\n\ntype multiOut struct {\n\touts []Output\n}\n\nfunc (m multiOut) Write(stamp time.Time, debug bool, msg string) {\n\tfor _, out := range m.outs {\n\t\tout.Write(stamp, debug, msg)\n\t}\n}\n\nfunc (m multiOut) Close() error {\n\tfor _, out := range m.outs {\n\t\tif err := out.Close(); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc MultiOutput(outputs ...Output) Output {\n\treturn multiOut{outputs}\n}\n\ntype funcOut struct {\n\tout   func(time.Time, bool, string)\n\tclose func() error\n}\n\nfunc (f funcOut) Write(stamp time.Time, debug bool, msg string) {\n\tf.out(stamp, debug, msg)\n}\n\nfunc (f funcOut) Close() error {\n\treturn f.close()\n}\n\nfunc FuncOutput(f func(time.Time, bool, string), close func() error) Output {\n\treturn funcOut{f, close}\n}\n\ntype NopOutput struct{}\n\nfunc (NopOutput) Write(time.Time, bool, string) {}\n\nfunc (NopOutput) Close() error { return nil }\n"
  },
  {
    "path": "framework/log/syslog.go",
    "content": "//go:build !windows && !plan9\n// +build !windows,!plan9\n\n/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage log\n\nimport (\n\t\"fmt\"\n\t\"log/syslog\"\n\t\"os\"\n\t\"time\"\n)\n\ntype syslogOut struct {\n\tw *syslog.Writer\n}\n\nfunc (s syslogOut) Write(stamp time.Time, debug bool, msg string) {\n\tvar err error\n\tif debug {\n\t\terr = s.w.Debug(msg + \"\\n\")\n\t} else {\n\t\terr = s.w.Info(msg + \"\\n\")\n\t}\n\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"!!! Failed to send message to syslog daemon: %v\\n\", err)\n\t}\n}\n\nfunc (s syslogOut) Close() error {\n\treturn s.w.Close()\n}\n\n// SyslogOutput returns a log.Output implementation that will send\n// messages to the system syslog daemon.\n//\n// Regular messages will be written with INFO priority,\n// debug messages will be written with DEBUG priority.\n//\n// Returned log.Output object is goroutine-safe.\nfunc SyslogOutput() (Output, error) {\n\tw, err := syslog.New(syslog.LOG_MAIL|syslog.LOG_INFO, \"maddy\")\n\treturn syslogOut{w}, err\n}\n"
  },
  {
    "path": "framework/log/syslog_stub.go",
    "content": "//go:build windows || plan9\n// +build windows plan9\n\n/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage log\n\nimport (\n\t\"errors\"\n)\n\n// SyslogOutput returns a log.Output implementation that will send\n// messages to the system syslog daemon.\n//\n// Regular messages will be written with INFO priority,\n// debug messages will be written with DEBUG priority.\n//\n// Returned log.Output object is goroutine-safe.\nfunc SyslogOutput() (Output, error) {\n\treturn nil, errors.New(\"log: syslog output is not supported on windows\")\n}\n"
  },
  {
    "path": "framework/log/writer.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage log\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n)\n\ntype wcOutput struct {\n\ttimestamps bool\n\twc         io.WriteCloser\n}\n\nfunc (w wcOutput) Write(stamp time.Time, debug bool, msg string) {\n\tbuilder := strings.Builder{}\n\tif w.timestamps {\n\t\tbuilder.WriteString(stamp.UTC().Format(\"2006-01-02T15:04:05.000Z \"))\n\t}\n\tif debug {\n\t\tbuilder.WriteString(\"[debug] \")\n\t}\n\tbuilder.WriteString(msg)\n\tbuilder.WriteRune('\\n')\n\tif _, err := io.WriteString(w.wc, builder.String()); err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"!!! Failed to write message to log: %v\\n\", err)\n\t}\n}\n\nfunc (w wcOutput) Close() error {\n\treturn w.wc.Close()\n}\n\n// WriteCloserOutput returns a log.Output implementation that\n// will write formatted messages to the provided io.Writer.\n//\n// Closing returned log.Output object will close the underlying\n// io.WriteCloser.\n//\n// Written messages will include timestamp formatted with millisecond\n// precision and [debug] prefix for debug messages.\n// If timestamps argument is false, timestamps will not be added.\n//\n// Returned log.Output does not provide its own serialization\n// so goroutine-safety depends on the io.Writer. Most operating\n// systems have atomic (read: thread-safe) implementations for\n// stream I/O, so it should be safe to use WriterOutput with os.File.\nfunc WriteCloserOutput(wc io.WriteCloser, timestamps bool) Output {\n\treturn wcOutput{timestamps, wc}\n}\n\ntype nopCloser struct {\n\tio.Writer\n}\n\nfunc (nc nopCloser) Close() error {\n\treturn nil\n}\n\n// WriterOutput returns a log.Output implementation that\n// will write formatted messages to the provided io.Writer.\n//\n// Closing returned log.Output object will have no effect on the\n// underlying io.Writer.\n//\n// Written messages will include timestamp formatted with millisecond\n// precision and [debug] prefix for debug messages.\n// If timestamps argument is false, timestamps will not be added.\n//\n// Returned log.Output does not provide its own serialization\n// so goroutine-safety depends on the io.Writer. Most operating\n// systems have atomic (read: thread-safe) implementations for\n// stream I/O, so it should be safe to use WriterOutput with os.File.\nfunc WriterOutput(w io.Writer, timestamps bool) Output {\n\treturn wcOutput{timestamps, nopCloser{os.Stderr}}\n}\n"
  },
  {
    "path": "framework/log/zap.go",
    "content": "package log\n\nimport (\n\t\"go.uber.org/zap/zapcore\"\n)\n\n// TODO: Migrate to using actual zapcore to improve logging performance\n\ntype zapLogger struct {\n\tL *Logger\n}\n\nfunc (l zapLogger) Enabled(level zapcore.Level) bool {\n\tif l.L.Debug {\n\t\treturn true\n\t}\n\treturn level > zapcore.DebugLevel\n}\n\nfunc (l zapLogger) With(fields []zapcore.Field) zapcore.Core {\n\tenc := zapcore.NewMapObjectEncoder()\n\tfor _, f := range fields {\n\t\tf.AddTo(enc)\n\t}\n\tnewF := make(map[string]interface{}, len(l.L.Fields)+len(enc.Fields))\n\tfor k, v := range l.L.Fields {\n\t\tnewF[k] = v\n\t}\n\tfor k, v := range enc.Fields {\n\t\tnewF[k] = v\n\t}\n\tl.L.Fields = newF\n\treturn l\n}\n\nfunc (l zapLogger) Check(entry zapcore.Entry, ce *zapcore.CheckedEntry) *zapcore.CheckedEntry {\n\tif l.Enabled(entry.Level) {\n\t\treturn ce.AddCore(entry, l)\n\t}\n\treturn ce\n}\n\nfunc (l zapLogger) Write(entry zapcore.Entry, fields []zapcore.Field) error {\n\tenc := zapcore.NewMapObjectEncoder()\n\tfor _, f := range fields {\n\t\tf.AddTo(enc)\n\t}\n\tif entry.LoggerName != \"\" {\n\t\tl.L.Name += \"/\" + entry.LoggerName\n\t}\n\tl.L.log(entry.Level == zapcore.DebugLevel, l.L.formatMsg(entry.Message, enc.Fields))\n\treturn nil\n}\n\nfunc (zapLogger) Sync() error {\n\treturn nil\n}\n"
  },
  {
    "path": "framework/logparser/parse.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\n// Package parser provides utilities for parsing of structured log messsages\n// generated by maddy.\npackage parser\n\nimport (\n\t\"encoding/json\"\n\t\"strings\"\n\t\"time\"\n\t\"unicode\"\n)\n\ntype (\n\tMsg struct {\n\t\tStamp   time.Time\n\t\tDebug   bool\n\t\tModule  string\n\t\tMessage string\n\t\tContext map[string]interface{}\n\t}\n\n\tMalformedMsg struct {\n\t\tDesc string\n\t\tErr  error\n\t}\n)\n\nconst (\n\tISO8601_UTC = \"2006-01-02T15:04:05.000Z\"\n)\n\nfunc (m MalformedMsg) Error() string {\n\tif m.Err != nil {\n\t\treturn \"parse: \" + m.Desc + \": \" + m.Err.Error()\n\t}\n\treturn \"parse: \" + m.Desc\n}\n\n// Parse parses the message from the maddy log file.\n//\n// It assumes standard file output, including the [debug] tag and\n// ISO 8601 timestamp at the start of each line. Timestamp is assumed to be in\n// the UTC, as it is enforced by maddy.\n//\n// JSON context values are unmarshalled without any additional processing,\n// notably that means that all numbers are represented as float64.\nfunc Parse(line string) (Msg, error) {\n\tparts := strings.Split(line, \"\\t\")\n\tif len(parts) != 2 {\n\t\t// All messages even without a Context have a trailing \\t,\n\t\t// so this one is obviously malformed.\n\t\treturn Msg{}, MalformedMsg{Desc: \"missing a tab separator\"}\n\t}\n\n\tm := Msg{\n\t\tContext: map[string]interface{}{},\n\t}\n\n\t// After that, the second part is the context. It can be empty, so don't fail\n\t// if there is none.\n\tif len(parts[1]) != 0 {\n\t\tif err := json.Unmarshal([]byte(parts[1]), &m.Context); err != nil {\n\t\t\treturn Msg{}, MalformedMsg{Desc: \"context unmarshal\", Err: err}\n\t\t}\n\t}\n\n\t// Okay, the first one might contain the timestamp at start.\n\t// Cut it away.\n\tmsgParts := strings.SplitN(parts[0], \" \", 2)\n\tif len(msgParts) == 1 {\n\t\treturn Msg{}, MalformedMsg{Desc: \"missing a timestamp\"}\n\t}\n\n\tvar err error\n\tm.Stamp, err = time.ParseInLocation(ISO8601_UTC, msgParts[0], time.UTC)\n\tif err != nil {\n\t\treturn Msg{}, MalformedMsg{Desc: \"timestamp parse\", Err: err}\n\t}\n\n\tmsgText := msgParts[1]\n\tif strings.HasPrefix(msgText, \"[debug] \") {\n\t\tmsgText = strings.TrimPrefix(msgText, \"[debug] \")\n\t\tm.Debug = true\n\t}\n\n\tmoduleText := strings.SplitN(msgText, \": \", 2)\n\tif len(moduleText) == 1 {\n\t\t// No module prefix, that's fine.\n\t\tm.Message = msgText\n\t\treturn m, nil\n\t}\n\n\tfor _, ch := range moduleText[0] {\n\t\tswitch {\n\t\tcase unicode.IsDigit(ch), unicode.IsLetter(ch), ch == '/':\n\t\tdefault:\n\t\t\t// This is not a module prefix, don't treat it as such.\n\t\t\tm.Message = msgText\n\t\t\treturn m, nil\n\t\t}\n\t}\n\n\tm.Module = moduleText[0]\n\tm.Message = moduleText[1]\n\n\treturn m, nil\n}\n"
  },
  {
    "path": "framework/logparser/parse_test.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage parser\n\nimport (\n\t\"reflect\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestParse(t *testing.T) {\n\ttest := func(line string, msg Msg, errDesc string) {\n\t\tt.Helper()\n\n\t\tparsed, err := Parse(line)\n\t\tif errDesc != \"\" {\n\t\t\tif err == nil {\n\t\t\t\tt.Errorf(\"Expected an error, got none\")\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif err.(MalformedMsg).Desc != errDesc {\n\t\t\t\tt.Errorf(\"Wrong error desc returned: %v\", err.(MalformedMsg).Desc)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\tif errDesc == \"\" && err != nil {\n\t\t\tt.Errorf(\"Unexpected error: %v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tif !reflect.DeepEqual(parsed, msg) {\n\t\t\tt.Errorf(\"Wrong Parse result,\\n got  %#+v\\n want %#+v\", parsed, msg)\n\t\t}\n\t}\n\n\ttest(\"2006-01-02T15:04:05.000Z module: hello\\t\", Msg{\n\t\tStamp:   time.Date(2006, time.January, 2, 15, 4, 5, 0, time.UTC),\n\t\tModule:  \"module\",\n\t\tMessage: \"hello\",\n\t\tContext: map[string]interface{}{},\n\t}, \"\")\n\ttest(\"2006-01-02T15:04:05.000Z module: hello: whatever\\t\", Msg{\n\t\tStamp:   time.Date(2006, time.January, 2, 15, 4, 5, 0, time.UTC),\n\t\tModule:  \"module\",\n\t\tMessage: \"hello: whatever\",\n\t\tContext: map[string]interface{}{},\n\t}, \"\")\n\ttest(\"2006-01-02T15:04:05.000Z module: hello: whatever\\t{\\\"a\\\":1}\", Msg{\n\t\tStamp:   time.Date(2006, time.January, 2, 15, 4, 5, 0, time.UTC),\n\t\tModule:  \"module\",\n\t\tMessage: \"hello: whatever\",\n\t\tContext: map[string]interface{}{\n\t\t\t\"a\": float64(1),\n\t\t},\n\t}, \"\")\n\ttest(\"2006-01-02T15:04:05.000Z module: hello: whatever\\t{\\\"a\\\":1,\\\"b\\\":\\\"bbb\\\"}\", Msg{\n\t\tStamp:   time.Date(2006, time.January, 2, 15, 4, 5, 0, time.UTC),\n\t\tModule:  \"module\",\n\t\tMessage: \"hello: whatever\",\n\t\tContext: map[string]interface{}{\n\t\t\t\"a\": float64(1),\n\t\t\t\"b\": \"bbb\",\n\t\t},\n\t}, \"\")\n\ttest(\"2006-01-02T15:04:05.000Z [debug] module: hello: whatever\\t{\\\"a\\\":1,\\\"b\\\":\\\"bbb\\\"}\", Msg{\n\t\tStamp:   time.Date(2006, time.January, 2, 15, 4, 5, 0, time.UTC),\n\t\tDebug:   true,\n\t\tModule:  \"module\",\n\t\tMessage: \"hello: whatever\",\n\t\tContext: map[string]interface{}{\n\t\t\t\"a\": float64(1),\n\t\t\t\"b\": \"bbb\",\n\t\t},\n\t}, \"\")\n\ttest(\"2006-01-02T15:04:05.000Z [debug] oink oink: hello: whatever\\t{\\\"a\\\":1,\\\"b\\\":\\\"bbb\\\"}\", Msg{\n\t\tStamp:   time.Date(2006, time.January, 2, 15, 4, 5, 0, time.UTC),\n\t\tDebug:   true,\n\t\tMessage: \"oink oink: hello: whatever\",\n\t\tContext: map[string]interface{}{\n\t\t\t\"a\": float64(1),\n\t\t\t\"b\": \"bbb\",\n\t\t},\n\t}, \"\")\n\ttest(\"2006-01-02T15:04:05.000Z [debug] whatever\\t{\\\"a\\\":1,\\\"b\\\":\\\"bbb\\\"}\", Msg{\n\t\tStamp:   time.Date(2006, time.January, 2, 15, 4, 5, 0, time.UTC),\n\t\tDebug:   true,\n\t\tMessage: \"whatever\",\n\t\tContext: map[string]interface{}{\n\t\t\t\"a\": float64(1),\n\t\t\t\"b\": \"bbb\",\n\t\t},\n\t}, \"\")\n\ttest(\"module: hello\\t\", Msg{}, \"timestamp parse\")\n\ttest(\"hello\\t\", Msg{}, \"missing a timestamp\")\n\ttest(\"2006-01-02T15:04:05.000Z module: hello\", Msg{}, \"missing a tab separator\")\n\ttest(\"2006-01-02T15:04:05.000Z [BROKEN FORMATTING: json: wtf lol omg]: hello map[stringasdasd]\", Msg{}, \"missing a tab separator\")\n}\n"
  },
  {
    "path": "framework/module/auth.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage module\n\nimport \"errors\"\n\n// ErrUnknownCredentials should be returned by auth. provider if supplied\n// credentials are valid for it but are not recognized (e.g. not found in\n// used DB).\nvar ErrUnknownCredentials = errors.New(\"unknown credentials\")\n\n// PlainAuth is the interface implemented by modules providing authentication using\n// username:password pairs.\n//\n// Modules implementing this interface should be registered with \"auth.\" prefix in name.\ntype PlainAuth interface {\n\tAuthPlain(username, password string) error\n}\n\n// PlainUserDB is a local credentials store that can be managed using maddy command\n// utility.\ntype PlainUserDB interface {\n\tPlainAuth\n\tListUsers() ([]string, error)\n\tCreateUser(username, password string) error\n\tSetUserPassword(username, password string) error\n\tDeleteUser(username string) error\n}\n"
  },
  {
    "path": "framework/module/blob_store.go",
    "content": "package module\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"io\"\n)\n\ntype Blob interface {\n\tSync() error\n\tio.Writer\n\tio.Closer\n}\n\nvar ErrNoSuchBlob = errors.New(\"blob_store: no such object\")\n\nconst UnknownBlobSize int64 = -1\n\n// BlobStore is the interface used by modules providing large binary object\n// storage.\ntype BlobStore interface {\n\t// Create creates a new blob for writing.\n\t//\n\t// Sync will be called on the returned Blob object after -all- data has\n\t// been successfully written.\n\t//\n\t// Close without Sync can be assumed to happen due to an unrelated error\n\t// and stored data can be discarded.\n\t//\n\t// blobSize indicates the exact amount of bytes that will be written\n\t// If -1 is passed - it is unknown and implementation will not make\n\t// any assumptions about the blob size. Error can be returned by any\n\t// Blob method if more than than blobSize bytes get written.\n\t//\n\t// Passed context will cover the entire blob write operation.\n\tCreate(ctx context.Context, key string, blobSize int64) (Blob, error)\n\n\t// Open returns the reader for the object specified by\n\t// passed key.\n\t//\n\t// If no such object exists - ErrNoSuchBlob is returned.\n\tOpen(ctx context.Context, key string) (io.ReadCloser, error)\n\n\t// Delete removes a set of keys from store. Non-existent keys are ignored.\n\tDelete(ctx context.Context, keys []string) error\n}\n"
  },
  {
    "path": "framework/module/check.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage module\n\nimport (\n\t\"context\"\n\n\t\"github.com/emersion/go-message/textproto\"\n\t\"github.com/emersion/go-msgauth/authres\"\n\t\"github.com/foxcpp/maddy/framework/buffer\"\n)\n\n// Check is the module interface that is meant for read-only (with the\n// exception of the message header modifications) (meta-)data checking.\n//\n// Modules implementing this interface should be registered with \"check.\"\n// prefix in name.\ntype Check interface {\n\t// CheckStateForMsg initializes the \"internal\" check state required for\n\t// processing of the new message.\n\t//\n\t// NOTE: Returned CheckState object must be hashable (usable as a map key).\n\t// This is used to deduplicate Check* calls, the easiest way to achieve\n\t// this is to have CheckState as a pointer to some struct, all pointers\n\t// are hashable.\n\tCheckStateForMsg(ctx context.Context, msgMeta *MsgMetadata) (CheckState, error)\n}\n\n// EarlyCheck is an optional module interface that can be implemented\n// by module implementing Check.\n//\n// It is used as an optimization to reject obviously malicious connections\n// before allocating resources for SMTP session.\n//\n// The Status of this check is accept (no error) or reject (error) only, no\n// advanced handling is available (such as 'quarantine' action and headers\n// prepending).\n//\n// If it s necessary to defer or affect further message processing\n// without outright killing the session, ConnState.ModData can be\n// used to store necessary information.\n//\n// It may be called multiple times for the same connection if TLS is negotiated\n// via STARTTLS. In this case, no state will be passed between before-TLS\n// context to the TLS one.\ntype EarlyCheck interface {\n\tCheckConnection(ctx context.Context, state *ConnState) error\n}\n\ntype CheckState interface {\n\t// CheckConnection is executed once when client sends a new message.\n\tCheckConnection(ctx context.Context) CheckResult\n\n\t// CheckSender is executed once when client sends the message sender\n\t// information (e.g. on the MAIL FROM command).\n\tCheckSender(ctx context.Context, mailFrom string) CheckResult\n\n\t// CheckRcpt is executed for each recipient when its address is received\n\t// from the client (e.g. on the RCPT TO command).\n\tCheckRcpt(ctx context.Context, rcptTo string) CheckResult\n\n\t// CheckBody is executed once after the message body is received and\n\t// buffered in memory or on disk.\n\t//\n\t// Check code should use passed mutex when working with the message header.\n\t// Body can be read without locking it since it is read-only.\n\tCheckBody(ctx context.Context, header textproto.Header, body buffer.Buffer) CheckResult\n\n\t// Close is called after the message processing ends, even if any of the\n\t// Check* functions return an error.\n\tClose() error\n}\n\ntype CheckResult struct {\n\t// Reason is the error that is reported to the message source\n\t// if check decided that the message should be rejected.\n\tReason error\n\n\t// Reject is the flag that specifies that the message\n\t// should be rejected.\n\tReject bool\n\n\t// Quarantine is the flag that specifies that the message\n\t// is considered \"possibly malicious\" and should be\n\t// put into Junk mailbox.\n\t//\n\t// This value is copied into MsgMetadata by the msgpipeline.\n\tQuarantine bool\n\n\t// AuthResult is the information that is supposed to\n\t// be included in Authentication-Results header.\n\tAuthResult []authres.Result\n\n\t// Header is the header fields that should be\n\t// added to the header after all checks.\n\tHeader textproto.Header\n}\n"
  },
  {
    "path": "framework/module/delivery_target.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage module\n\nimport (\n\t\"context\"\n\n\t\"github.com/emersion/go-message/textproto\"\n\t\"github.com/emersion/go-smtp\"\n\t\"github.com/foxcpp/maddy/framework/buffer\"\n)\n\n// DeliveryTarget interface represents abstract storage for the message data\n// (typically persistent) or other kind of component that can be used as a\n// final destination for the message.\n//\n// Modules implementing this interface should be registered with \"target.\"\n// prefix in name.\ntype DeliveryTarget interface {\n\t// StartDelivery starts the delivery of a new message.\n\t//\n\t// The domain part of the MAIL FROM address is assumed to be U-labels with\n\t// NFC normalization and case-folding applied. The message source should\n\t// ensure that by calling address.CleanDomain if necessary.\n\tStartDelivery(ctx context.Context, msgMeta *MsgMetadata, mailFrom string) (Delivery, error)\n}\n\ntype Delivery interface {\n\t// AddRcpt adds the target address for the message.\n\t//\n\t// The domain part of the address is assumed to be U-labels with NFC normalization\n\t// and case-folding applied. The message source should ensure that by\n\t// calling address.CleanDomain if necessary.\n\t//\n\t// Implementation should assume that no case-folding or deduplication was\n\t// done by caller code. Its implementation responsibility to do so if it is\n\t// necessary. It is not recommended to reject duplicated recipients,\n\t// however. They should be silently ignored.\n\t//\n\t// Implementation should do as much checks as possible here and reject\n\t// recipients that can't be used.  Note: MsgMetadata object passed to StartDelivery\n\t// contains BodyLength field. If it is non-zero, it can be used to check\n\t// storage quota for the user before Body.\n\tAddRcpt(ctx context.Context, rcptTo string, opts smtp.RcptOptions) error\n\n\t// Body sets the body and header contents for the message.\n\t// If this method fails, message is assumed to be undeliverable\n\t// to all recipients.\n\t//\n\t// Implementation should avoid doing any persistent changes to the\n\t// underlying storage until Commit is called. If that is not possible,\n\t// Abort should (attempt to) rollback any such changes.\n\t//\n\t// If Body can't be implemented without per-recipient failures,\n\t// then delivery object should also implement PartialDelivery interface\n\t// for use by message sources that are able to make sense of per-recipient\n\t// errors.\n\t//\n\t// Here is the example of possible implementation for maildir-based\n\t// storage:\n\t// Calling Body creates a file in tmp/ directory.\n\t// Commit moves the created file to new/ directory.\n\t// Abort removes the created file.\n\tBody(ctx context.Context, header textproto.Header, body buffer.Buffer) error\n\n\t// Abort cancels message delivery.\n\t//\n\t// All changes made to the underlying storage should be aborted at this\n\t// point, if possible.\n\tAbort(ctx context.Context) error\n\n\t// Commit completes message delivery.\n\t//\n\t// It generally should never fail, since failures here jeopardize\n\t// atomicity of the delivery if multiple targets are used.\n\tCommit(ctx context.Context) error\n}\n"
  },
  {
    "path": "framework/module/imap_filter.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage module\n\nimport (\n\t\"github.com/emersion/go-message/textproto\"\n\t\"github.com/foxcpp/maddy/framework/buffer\"\n)\n\n// IMAPFilter is interface used by modules that want to modify IMAP-specific message\n// attributes on delivery.\n//\n// Modules implementing this interface should be registered with namespace prefix\n// \"imap.filter\".\ntype IMAPFilter interface {\n\t// IMAPFilter is called when message is about to be stored in IMAP-compatible\n\t// storage. It is called only for messages delivered over SMTP, hdr and body\n\t// contain the message exactly how it will be stored.\n\t//\n\t// Filter can change the target directory by returning non-empty folder value.\n\t// Additionally it can add additional IMAP flags to the message by returning\n\t// them.\n\t//\n\t// Errors returned by IMAPFilter will be just logged and will not cause delivery\n\t// to fail.\n\tIMAPFilter(accountName string, rcptTo string, meta *MsgMetadata, hdr textproto.Header, body buffer.Buffer) (folder string, flags []string, err error)\n}\n"
  },
  {
    "path": "framework/module/modifier.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage module\n\nimport (\n\t\"context\"\n\n\t\"github.com/emersion/go-message/textproto\"\n\t\"github.com/foxcpp/maddy/framework/buffer\"\n)\n\n// Modifier is the module interface for modules that can mutate the\n// processed message or its meta-data.\n//\n// Currently, the message body can't be mutated for efficiency and\n// correctness reasons: It would require \"rebuffering\" (see buffer.Buffer doc),\n// can invalidate assertions made on the body contents before modification and\n// will break DKIM signatures.\n//\n// Only message header can be modified. Furthermore, it is highly discouraged for\n// modifiers to remove or change existing fields to prevent issues outlined\n// above.\n//\n// Calls on ModifierState are always strictly ordered.\n// RewriteRcpt is newer called before RewriteSender and RewriteBody is never called\n// before RewriteRcpts. This allows modificator code to save values\n// passed to previous calls for use in later operations.\n//\n// Modules implementing this interface should be registered with \"modify.\" prefix in name.\ntype Modifier interface {\n\t// ModStateForMsg initializes modifier \"internal\" state\n\t// required for processing of the message.\n\tModStateForMsg(ctx context.Context, msgMeta *MsgMetadata) (ModifierState, error)\n}\n\ntype ModifierState interface {\n\t// RewriteSender allows modifier to replace MAIL FROM value.\n\t// If no changes are required, this method returns its\n\t// argument, otherwise it returns a new value.\n\t//\n\t// Note that per-source/per-destination modifiers are executed\n\t// after routing decision is made so changed value will have no\n\t// effect on it.\n\t//\n\t// Also note that MsgMeta.OriginalFrom will still contain the original value\n\t// for purposes of tracing. It should not be modified by this method.\n\tRewriteSender(ctx context.Context, mailFrom string) (string, error)\n\n\t// RewriteRcpt replaces RCPT TO value.\n\t// If no changed are required, this method returns its argument as slice,\n\t// otherwise it returns a slice with 1 or more new values.\n\t//\n\t// MsgPipeline will take of populating MsgMeta.OriginalRcpts. RewriteRcpt\n\t// doesn't do it.\n\tRewriteRcpt(ctx context.Context, rcptTo string) ([]string, error)\n\n\t// RewriteBody modifies passed Header argument and may optionally\n\t// inspect the passed body buffer to make a decision on new header field values.\n\t//\n\t// There is no way to modify the body and RewriteBody should avoid\n\t// removing existing header fields and changing their values.\n\tRewriteBody(ctx context.Context, h *textproto.Header, body buffer.Buffer) error\n\n\t// Close is called after the message processing ends, even if any of the\n\t// Rewrite* functions return an error.\n\tClose() error\n}\n"
  },
  {
    "path": "framework/module/module.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\n// Package module contains modules registry and interfaces implemented\n// by modules.\n//\n// Interfaces are placed here to prevent circular dependencies.\n//\n// Each interface required by maddy for operation is provided by some object\n// called \"module\".  This includes authentication, storage backends, DKIM,\n// email filters, etc.  Each module may serve multiple functions. I.e. it can\n// be IMAP storage backend, SMTP downstream and authentication provider at the\n// same moment.\n//\n// Each module gets its own unique name (sql for go-imap-sql, proxy for\n// proxy module, local for local delivery perhaps, etc). Each module instance\n// also can have its own unique name can be used to refer to it in\n// configuration.\npackage module\n\nimport (\n\t\"github.com/foxcpp/maddy/framework/config\"\n)\n\n// Module is the interface implemented by all maddy module instances.\ntype Module interface {\n\tConfigure(inlineArgs []string, config *config.Map) error\n\n\t// Name method reports module name.\n\t//\n\t// It is used to reference module in the configuration and in logs.\n\tName() string\n\n\t// InstanceName method reports unique name of this module instance or empty\n\t// string if module instance is unnamed.\n\tInstanceName() string\n}\n"
  },
  {
    "path": "framework/module/module_specific_data.go",
    "content": "package module\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"sync\"\n)\n\n// ModSpecificData is a container that allows modules to attach\n// additional context data to framework objects such as SMTP connections\n// without conflicting with each other and ensuring each module\n// gets its own namespace.\n//\n// It must not be used to store stateful objects that may need\n// a specific cleanup routine as ModSpecificData does not provide\n// any lifetime management.\n//\n// Stored data must be serializable to JSON for state persistence\n// e.g. when message is stored in a on-disk queue.\ntype ModSpecificData struct {\n\tmodDataLck sync.RWMutex\n\tmodData    map[string]interface{}\n}\n\nfunc (msd *ModSpecificData) modKey(m Module, perInstance bool) string {\n\tif !perInstance {\n\t\treturn m.Name()\n\t}\n\tinstName := m.InstanceName()\n\tif instName == \"\" {\n\t\tinstName = fmt.Sprintf(\"%x\", m)\n\t}\n\treturn m.Name() + \"/\" + instName\n}\n\nfunc (msd *ModSpecificData) MarshalJSON() ([]byte, error) {\n\tmsd.modDataLck.RLock()\n\tdefer msd.modDataLck.RUnlock()\n\treturn json.Marshal(msd.modData)\n}\n\nfunc (msd *ModSpecificData) UnmarshalJSON(b []byte) error {\n\tmsd.modDataLck.Lock()\n\tdefer msd.modDataLck.Unlock()\n\treturn json.Unmarshal(b, &msd.modData)\n}\n\nfunc (msd *ModSpecificData) Set(m Module, perInstance bool, value interface{}) {\n\tkey := msd.modKey(m, perInstance)\n\tmsd.modDataLck.Lock()\n\tdefer msd.modDataLck.Unlock()\n\tif msd.modData == nil {\n\t\tmsd.modData = make(map[string]interface{})\n\t}\n\tmsd.modData[key] = value\n}\n\nfunc (msd *ModSpecificData) Get(m Module, perInstance bool) interface{} {\n\tkey := msd.modKey(m, perInstance)\n\tmsd.modDataLck.RLock()\n\tdefer msd.modDataLck.RUnlock()\n\treturn msd.modData[key]\n}\n"
  },
  {
    "path": "framework/module/modules/dummy.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage modules\n\nimport (\n\t\"context\"\n\n\t\"github.com/emersion/go-message/textproto\"\n\t\"github.com/emersion/go-smtp\"\n\t\"github.com/foxcpp/maddy/framework/buffer\"\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n)\n\n// Dummy is a struct that implements PlainAuth and DeliveryTarget\n// interfaces but does nothing. Useful for testing.\n//\n// It is always registered under the 'dummy' name and can be used in both tests\n// and the actual server code (but the latter is kinda pointless).\ntype Dummy struct{ instName string }\n\nfunc (d *Dummy) AuthPlain(username, _ string) error {\n\treturn nil\n}\n\nfunc (d *Dummy) Lookup(_ context.Context, _ string) (string, bool, error) {\n\treturn \"\", false, nil\n}\n\nfunc (d *Dummy) LookupMulti(_ context.Context, _ string) ([]string, error) {\n\treturn []string{\"\"}, nil\n}\n\nfunc (d *Dummy) Name() string {\n\treturn \"dummy\"\n}\n\nfunc (d *Dummy) InstanceName() string {\n\treturn d.instName\n}\n\nfunc (d *Dummy) Configure(_ []string, _ *config.Map) error {\n\treturn nil\n}\n\nfunc (d *Dummy) StartDelivery(ctx context.Context, msgMeta *module.MsgMetadata, mailFrom string) (module.Delivery, error) {\n\treturn dummyDelivery{}, nil\n}\n\ntype dummyDelivery struct{}\n\nfunc (dd dummyDelivery) AddRcpt(ctx context.Context, rcptTo string, opts smtp.RcptOptions) error {\n\treturn nil\n}\n\nfunc (dd dummyDelivery) Body(ctx context.Context, header textproto.Header, body buffer.Buffer) error {\n\treturn nil\n}\n\nfunc (dd dummyDelivery) Abort(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (dd dummyDelivery) Commit(ctx context.Context) error {\n\treturn nil\n}\n\nfunc NewDummy(_ *container.C, _, instName string) (module.Module, error) {\n\treturn &Dummy{instName: instName}, nil\n}\n"
  },
  {
    "path": "framework/module/modules/modules.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage modules\n\nimport (\n\t\"sync\"\n\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/log\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n)\n\n// FuncNewModule is function that creates new instance of module with specified name.\n//\n// Module.InstanceName() of the returned module object should return instName.\n// If module is defined inline, instName will be empty.\n//\n// Returned Module may additionally implement LifetimeModule.\ntype FuncNewModule func(c *container.C, modName, instName string) (module.Module, error)\n\n// FuncNewEndpoint is a function that creates new instance of endpoint\n// module.\n//\n// Compared to regular modules, endpoint module instances are:\n// - Not registered in the global registry.\n// - Can't be defined inline.\n// - Don't have an unique name\n// - All config arguments are always passed as an 'addrs' slice and not used as\n// names.\n//\n// As a consequence of having no per-instance name, InstanceName of the module\n// object always returns the same value as Name.\ntype FuncNewEndpoint func(c *container.C, modName string, addrs []string) (container.LifetimeModule, error)\n\nvar (\n\tmodules     = make(map[string]FuncNewModule)\n\tendpoints   = make(map[string]FuncNewEndpoint)\n\tmodulesLock sync.RWMutex\n)\n\n// Register adds module factory function to global registry.\n//\n// name must be unique. Register will panic if module with specified name\n// already exists in registry.\n//\n// You probably want to call this function from func init() of module package.\nfunc Register(name string, factory FuncNewModule) {\n\tmodulesLock.Lock()\n\tdefer modulesLock.Unlock()\n\n\tif _, ok := modules[name]; ok {\n\t\tpanic(\"Register: module with specified name is already registered: \" + name)\n\t}\n\n\tmodules[name] = factory\n}\n\n// RegisterDeprecated adds module factory function to global registry.\n//\n// It prints warning to the log about name being deprecated and suggests using\n// a new name.\nfunc RegisterDeprecated(name, newName string, factory FuncNewModule) {\n\tRegister(name, func(c *container.C, modName, instName string) (module.Module, error) {\n\t\tlog.Printf(\"module initialized via deprecated name %s, %s should be used instead; deprecated name may be removed in the next version\", name, newName)\n\t\treturn factory(c, modName, instName)\n\t})\n}\n\n// Get returns module from global registry.\n//\n// This function does not return endpoint-type modules, use GetEndpoint for\n// that.\n// Nil is returned if no module with specified name is registered.\nfunc Get(name string) FuncNewModule {\n\tmodulesLock.RLock()\n\tdefer modulesLock.RUnlock()\n\n\treturn modules[name]\n}\n\n// GetEndpoint returns an endpoint module from global registry.\n//\n// Nil is returned if no module with specified name is registered.\nfunc GetEndpoint(name string) FuncNewEndpoint {\n\tmodulesLock.RLock()\n\tdefer modulesLock.RUnlock()\n\n\treturn endpoints[name]\n}\n\n// RegisterEndpoint registers an endpoint module.\n//\n// See FuncNewEndpoint for information about\n// differences of endpoint modules from regular modules.\nfunc RegisterEndpoint(name string, factory FuncNewEndpoint) {\n\tmodulesLock.Lock()\n\tdefer modulesLock.Unlock()\n\n\tif _, ok := endpoints[name]; ok {\n\t\tpanic(\"Register: module with specified name is already registered: \" + name)\n\t}\n\n\tendpoints[name] = factory\n}\n\nfunc init() {\n\tRegister(\"dummy\", NewDummy)\n}\n"
  },
  {
    "path": "framework/module/msgmetadata.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage module\n\nimport (\n\t\"crypto/rand\"\n\t\"crypto/tls\"\n\t\"encoding/hex\"\n\t\"io\"\n\t\"net\"\n\n\t\"github.com/emersion/go-smtp\"\n\t\"github.com/foxcpp/maddy/framework/future\"\n)\n\n// ConnState structure holds the state information of the protocol used to\n// accept this message.\ntype ConnState struct {\n\t// IANA name (ESMTP, ESMTPS, etc) of the protocol message was received\n\t// over. If the message was generated locally, this field is empty.\n\tProto string\n\n\t// Information about the SMTP connection, including HELO hostname and\n\t// source IP. Valid only if Proto refers the SMTP protocol or its variant\n\t// (e.g. LMTP).\n\tHostname   string\n\tLocalAddr  net.Addr\n\tRemoteAddr net.Addr\n\tTLS        tls.ConnectionState\n\n\t// The RDNSName field contains the result of Reverse DNS lookup on the\n\t// client IP.\n\t//\n\t// The underlying type is the string or untyped nil value. It is the\n\t// message source responsibility to populate this field.\n\t//\n\t// Valid values of this field consumers need to be aware of:\n\t// RDNSName = nil\n\t//   The reverse DNS lookup is not applicable for that message source.\n\t//   Typically the case for messages generated locally.\n\t// RDNSName != nil, but Get returns nil\n\t//   The reverse DNS lookup was attempted, but resulted in an error.\n\t//   Consumers should assume that the PTR record doesn't exist.\n\tRDNSName *future.Future\n\n\t// If the client successfully authenticated using a username/password pair.\n\t// This field contains the username.\n\tAuthUser string\n\n\t// If the client successfully authenticated using a username/password pair.\n\t// This field should be cleaned if the ConnState object is serialized\n\tAuthPassword string\n\n\tModData ModSpecificData\n}\n\n// MsgMetadata structure contains all information about the origin of\n// the message and all associated flags indicating how it should be handled\n// by components.\n//\n// All fields should be considered read-only except when otherwise is noted.\n// Module instances should avoid keeping reference to the instance passed to it\n// and copy the structure using DeepCopy method instead.\n//\n// Compatibility with older values should be considered when changing this\n// structure since it is serialized to the disk by the queue module using\n// JSON. Modules should correctly handle missing or invalid values.\ntype MsgMetadata struct {\n\t// Unique identifier for this message. Randomly generated by the\n\t// message source module.\n\tID string\n\n\t// Original message sender address as it was received by the message source.\n\t//\n\t// Note that this field is meant for use for tracing purposes.\n\t// All routing and other decisions should be made based on the sender address\n\t// passed separately (for example, mailFrom argument for CheckSender function)\n\t// Note that addresses may contain unescaped Unicode characters.\n\tOriginalFrom string\n\n\t// If set - no SrcHostname and SrcAddr will be added to Received\n\t// header. These fields are still written to the server log.\n\tDontTraceSender bool\n\n\t// Quarantine is a message flag that is should be set if message is\n\t// considered \"suspicious\" and should be put into \"Junk\" folder\n\t// in the storage.\n\t//\n\t// This field should not be modified by the checks that verify\n\t// the message. It is set only by the message pipeline.\n\tQuarantine bool\n\n\t// OriginalRcpts contains the mapping from the final recipient to the\n\t// recipient that was presented by the client.\n\t//\n\t// MsgPipeline will update that field when recipient modifiers\n\t// are executed.\n\t//\n\t// It should be used when reporting information back to client (via DSN,\n\t// for example) to prevent disclosing information about aliases\n\t// which is usually unwanted.\n\tOriginalRcpts map[string]string\n\n\t// SMTPOpts contains the SMTP MAIL FROM command arguments, if the message\n\t// was accepted over SMTP or SMTP-like protocol (such as LMTP).\n\t//\n\t// Note that the Size field should not be used as source of information about\n\t// the body size. Especially since it counts the header too whereas\n\t// Buffer.Len does not.\n\tSMTPOpts smtp.MailOptions\n\n\t// Conn contains the information about the underlying protocol connection\n\t// that was used to accept this message. The referenced instance may be shared\n\t// between multiple messages.\n\t//\n\t// It can be nil for locally generated messages.\n\tConn *ConnState\n\n\t// This is set by endpoint/smtp to indicate that body contains \"TLS-Required: No\"\n\t// header. It is only meaningful if server has seen the body at least once\n\t// (e.g. the message was passed via queue).\n\tTLSRequireOverride bool\n}\n\n// DeepCopy creates a copy of the MsgMetadata structure, also\n// copying contents of the maps and slices.\n//\n// There are a few exceptions, however:\n// - SrcAddr is not copied and copy field references original value.\nfunc (msgMeta *MsgMetadata) DeepCopy() *MsgMetadata {\n\tcpy := *msgMeta\n\t// There is no good way to copy net.Addr, but it should not be\n\t// modified by anything anyway so we are safe.\n\treturn &cpy\n}\n\n// GenerateMsgID generates a string usable as MsgID field in module.MsgMeta.\nfunc GenerateMsgID() (string, error) {\n\trawID := make([]byte, 4)\n\t_, err := io.ReadFull(rand.Reader, rawID)\n\treturn hex.EncodeToString(rawID), err\n}\n"
  },
  {
    "path": "framework/module/mxauth.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage module\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n)\n\nconst (\n\tAuthDisabled     = \"off\"\n\tAuthMTASTS       = \"mtasts\"\n\tAuthDNSSEC       = \"dnssec\"\n\tAuthCommonDomain = \"common_domain\"\n)\n\ntype (\n\tTLSLevel int\n\tMXLevel  int\n)\n\nconst (\n\tTLSNone TLSLevel = iota\n\tTLSEncrypted\n\tTLSAuthenticated\n)\n\nconst (\n\tMXNone MXLevel = iota\n\tMX_MTASTS\n\tMX_DNSSEC\n)\n\nfunc (l TLSLevel) String() string {\n\tswitch l {\n\tcase TLSNone:\n\t\treturn \"none\"\n\tcase TLSEncrypted:\n\t\treturn \"encrypted\"\n\tcase TLSAuthenticated:\n\t\treturn \"authenticated\"\n\t}\n\treturn \"???\"\n}\n\nfunc (l MXLevel) String() string {\n\tswitch l {\n\tcase MXNone:\n\t\treturn \"none\"\n\tcase MX_MTASTS:\n\t\treturn \"mtasts\"\n\tcase MX_DNSSEC:\n\t\treturn \"dnssec\"\n\t}\n\treturn \"???\"\n}\n\ntype (\n\t// MXAuthPolicy is an object that provides security check for outbound connections.\n\t// It can do one of the following:\n\t//\n\t// - Check effective TLS level or MX level against some configured or\n\t// discovered value.\n\t// E.g. local policy.\n\t//\n\t// - Raise the security level if certain condition about used MX or\n\t// connection is met.\n\t// E.g. DANE MXAuthPolicy raises TLS level to Authenticated if a matching\n\t// TLSA record is discovered.\n\t//\n\t// - Reject the connection if certain condition about used MX or\n\t// connection is _not_ met.\n\t// E.g. An enforced MTA-STS MXAuthPolicy rejects MX records not matching it.\n\t//\n\t// It is not recommended to mix different types of behavior described above\n\t// in the same implementation.\n\t// Specifically, the first type is used mostly for local policies and is not\n\t// really practical.\n\t//\n\t// Modules implementing this interface should be registered with \"mx_auth.\"\n\t// prefix in name.\n\tMXAuthPolicy interface {\n\t\tStartDelivery(*MsgMetadata) DeliveryMXAuthPolicy\n\n\t\t// Weight is an integer in range 0-1000 that represents relative\n\t\t// ordering of policy application.\n\t\tWeight() int\n\t}\n\n\t// DeliveryMXAuthPolicy is an interface of per-delivery object that estabilishes\n\t// and verifies required and effective security for MX records and TLS\n\t// connections.\n\tDeliveryMXAuthPolicy interface {\n\t\t// PrepareDomain is called before DNS MX lookup and may asynchronously\n\t\t// start additional lookups necessary for policy application in CheckMX\n\t\t// or CheckConn.\n\t\t//\n\t\t// If there any errors - they should be deferred to the CheckMX or\n\t\t// CheckConn call.\n\t\tPrepareDomain(ctx context.Context, domain string)\n\n\t\t// PrepareConn is called before connection and may asynchronously\n\t\t// start additional lookups necessary for policy application in\n\t\t// CheckConn.\n\t\t//\n\t\t// If there are any errors - they should be deferred to the CheckConn\n\t\t// call.\n\t\tPrepareConn(ctx context.Context, mx string)\n\n\t\t// CheckMX is called to check whether the policy permits to use a MX.\n\t\t//\n\t\t// mxLevel contains the MX security level estabilished by checks\n\t\t// executed before.\n\t\t//\n\t\t// domain is passed to the CheckMX to allow simpler implementation\n\t\t// of stateless policy objects.\n\t\t//\n\t\t// dnssec is true if the MX lookup was performed using DNSSEC-enabled\n\t\t// resolver and the zone is signed and its signature is valid.\n\t\tCheckMX(ctx context.Context, mxLevel MXLevel, domain, mx string, dnssec bool) (MXLevel, error)\n\n\t\t// CheckConn is called to check whether the policy permits to use this\n\t\t// connection.\n\t\t//\n\t\t// tlsLevel and mxLevel contain the TLS security level estabilished by\n\t\t// checks executed before.\n\t\t//\n\t\t// domain is passed to the CheckConn to allow simpler implementation\n\t\t// of stateless policy objects.\n\t\t//\n\t\t// If tlsState.HandshakeCompleted is false, TLS is not used. If\n\t\t// tlsState.VerifiedChains is nil, InsecureSkipVerify was used (no\n\t\t// ServerName or PKI check was done).\n\t\tCheckConn(ctx context.Context, mxLevel MXLevel, tlsLevel TLSLevel, domain, mx string, tlsState tls.ConnectionState) (TLSLevel, error)\n\n\t\t// Reset cleans the internal object state for use with another message.\n\t\t// newMsg may be nil if object is not needed anymore.\n\t\tReset(newMsg *MsgMetadata)\n\t}\n)\n"
  },
  {
    "path": "framework/module/partial_delivery.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage module\n\nimport (\n\t\"context\"\n\n\t\"github.com/emersion/go-message/textproto\"\n\t\"github.com/foxcpp/maddy/framework/buffer\"\n)\n\n// StatusCollector is an object that is passed by message source\n// that is interested in intermediate status reports about partial\n// delivery failures.\ntype StatusCollector interface {\n\t// SetStatus sets the error associated with the recipient.\n\t//\n\t// rcptTo should match exactly the value that was passed to the\n\t// AddRcpt, i.e. if any translations was made by the target,\n\t// they should not affect the rcptTo argument here.\n\t//\n\t// It should not be called multiple times for the same\n\t// value of rcptTo. It also should not be called\n\t// after BodyNonAtomic returns.\n\t//\n\t// SetStatus is goroutine-safe. Implementations\n\t// provide necessary serialization.\n\tSetStatus(rcptTo string, err error)\n}\n\n// PartialDelivery is an optional interface that may be implemented\n// by the object returned by DeliveryTarget.StartDelivery. See PartialDelivery.BodyNonAtomic\n// documentation for details.\ntype PartialDelivery interface {\n\t// BodyNonAtomic is similar to Body method of the regular Delivery interface\n\t// with the except that it allows target to reject the body only for some\n\t// recipients by setting statuses using passed collector object.\n\t//\n\t// This interface is preferred by the LMTP endpoint and queue implementation\n\t// to ensure correct handling of partial failures.\n\tBodyNonAtomic(ctx context.Context, c StatusCollector, header textproto.Header, body buffer.Buffer)\n}\n"
  },
  {
    "path": "framework/module/storage.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage module\n\nimport (\n\timapbackend \"github.com/emersion/go-imap/backend\"\n)\n\n// Storage interface is a slightly modified go-imap's Backend interface\n// (authentication is removed).\n//\n// Modules implementing this interface should be registered with prefix\n// \"storage.\" in name.\ntype Storage interface {\n\t// GetOrCreateIMAPAcct returns User associated with storage account specified by\n\t// the name.\n\t//\n\t// If it doesn't exists - it should be created.\n\tGetOrCreateIMAPAcct(username string) (imapbackend.User, error)\n\tGetIMAPAcct(username string) (imapbackend.User, error)\n\n\t// Extensions returns list of IMAP extensions supported by backend.\n\tIMAPExtensions() []string\n}\n\n// ManageableStorage is an extended Storage interface that allows to\n// list existing accounts, create and delete them.\ntype ManageableStorage interface {\n\tStorage\n\n\tListIMAPAccts() ([]string, error)\n\tCreateIMAPAcct(username string) error\n\tDeleteIMAPAcct(username string) error\n}\n"
  },
  {
    "path": "framework/module/table.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage module\n\nimport \"context\"\n\n// Table is the interface implemented by module that implementation string-to-string\n// translation.\n//\n// Modules implementing this interface should be registered with prefix\n// \"table.\" in name.\ntype Table interface {\n\tLookup(ctx context.Context, s string) (string, bool, error)\n}\n\n// MultiTable is the interface that module can implement in addition to Table\n// if it can provide multiple values as a lookup result.\ntype MultiTable interface {\n\tLookupMulti(ctx context.Context, s string) ([]string, error)\n}\n\ntype MutableTable interface {\n\tTable\n\tKeys() ([]string, error)\n\tRemoveKey(k string) error\n\tSetKey(k, v string) error\n}\n"
  },
  {
    "path": "framework/module/tls_loader.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage module\n\nimport (\n\t\"crypto/tls\"\n)\n\n// TLSLoader interface is module interface that can be used to supply TLS\n// certificates to TLS-enabled endpoints.\n//\n// The interface is intentionally kept simple, all configuration and parameters\n// necessary are to be provided using conventional module configuration.\n//\n// If loader returns multiple certificate chains - endpoint will serve them\n// based on SNI matching.\n//\n// Note that loading function will be called for each connections - it is\n// highly recommended to cache parsed form.\n//\n// Modules implementing this interface should be registered with prefix\n// \"tls.loader.\" in name.\ntype TLSLoader interface {\n\tConfigureTLS(c *tls.Config) error\n}\n"
  },
  {
    "path": "framework/resource/netresource/dup.go",
    "content": "package netresource\n\nimport \"net\"\n\nfunc dupTCPListener(l *net.TCPListener) (*net.TCPListener, error) {\n\tf, err := l.File()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tl2, err := net.FileListener(f)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn l2.(*net.TCPListener), nil\n}\n\nfunc dupUnixListener(l *net.UnixListener) (*net.UnixListener, error) {\n\tf, err := l.File()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tl2, err := net.FileListener(f)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn l2.(*net.UnixListener), nil\n}\n"
  },
  {
    "path": "framework/resource/netresource/fd.go",
    "content": "package netresource\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n)\n\nfunc ListenFD(fd uint) (net.Listener, error) {\n\tfile := os.NewFile(uintptr(fd), strconv.FormatUint(uint64(fd), 10))\n\tdefer func() {\n\t\tif err := file.Close(); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}()\n\treturn net.FileListener(file)\n}\n\nfunc ListenFDName(name string) (net.Listener, error) {\n\tlistenPDStr := os.Getenv(\"LISTEN_PID\")\n\tif listenPDStr == \"\" {\n\t\treturn nil, errors.New(\"$LISTEN_PID is not set\")\n\t}\n\tlistenPid, err := strconv.Atoi(listenPDStr)\n\tif err != nil {\n\t\treturn nil, errors.New(\"$LISTEN_PID is not integer\")\n\t}\n\tif listenPid != os.Getpid() {\n\t\treturn nil, fmt.Errorf(\"$LISTEN_PID (%d) is not our PID (%d)\", listenPid, os.Getpid())\n\t}\n\n\tnames := strings.Split(os.Getenv(\"LISTEN_FDNAMES\"), \":\")\n\tfd := uintptr(0)\n\tfor i, fdName := range names {\n\t\tif fdName == name {\n\t\t\tfd = uintptr(3 + i)\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif fd == 0 {\n\t\treturn nil, fmt.Errorf(\"name %s not found in $LISTEN_FDNAMES\", name)\n\t}\n\n\tfile := os.NewFile(3+fd, name)\n\tdefer func() {\n\t\tif err := file.Close(); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}()\n\treturn net.FileListener(file)\n}\n"
  },
  {
    "path": "framework/resource/netresource/listen.go",
    "content": "package netresource\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\t\"strconv\"\n\n\t\"github.com/foxcpp/maddy/framework/log\"\n)\n\nvar (\n\ttracker = NewListenerTracker(log.DefaultLogger.Sublogger(\"netresource\"))\n)\n\nfunc CloseUnusedListeners() error {\n\treturn tracker.CloseUnused()\n}\n\nfunc CloseAllListeners() error {\n\treturn tracker.Close()\n}\n\nfunc ResetListenersUsage() {\n\ttracker.ResetUsage()\n}\n\nfunc Listen(network, addr string) (net.Listener, error) {\n\tswitch network {\n\tcase \"fd\":\n\t\tfd, err := strconv.ParseUint(addr, 10, strconv.IntSize)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"invalid FD number: %v\", addr)\n\t\t}\n\t\treturn ListenFD(uint(fd))\n\tcase \"fdname\":\n\t\treturn ListenFDName(addr)\n\tcase \"tcp\", \"tcp4\", \"tcp6\", \"unix\":\n\t\treturn tracker.Get(network, addr)\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported network: %v\", network)\n\t}\n}\n"
  },
  {
    "path": "framework/resource/netresource/tracker.go",
    "content": "package netresource\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\t\"net/netip\"\n\n\t\"github.com/foxcpp/maddy/framework/log\"\n\t\"github.com/foxcpp/maddy/framework/resource\"\n)\n\ntype ListenerTracker struct {\n\tlogger *log.Logger\n\ttcp    *resource.Tracker[*net.TCPListener]\n\tunix   *resource.Tracker[*net.UnixListener]\n}\n\nfunc (lt *ListenerTracker) Get(network, addr string) (net.Listener, error) {\n\tswitch network {\n\tcase \"tcp\", \"tcp4\", \"tcp6\":\n\t\tl, err := lt.tcp.GetOpen(addr, func() (*net.TCPListener, error) {\n\t\t\taddrPort, err := netip.ParseAddrPort(addr)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tlt.logger.DebugMsg(\"new listener\", \"network\", network, \"address\", addr)\n\t\t\treturn net.ListenTCP(network, net.TCPAddrFromAddrPort(addrPort))\n\t\t})\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\t// We return duplicated listener so when listener is closed by user endpoint\n\t\t// the tracked resource remains available and listening on the port doesn't\n\t\t// actually stop.\n\t\tl2, err := dupTCPListener(l)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn l2, nil\n\tcase \"unix\":\n\t\tl, err := lt.unix.GetOpen(addr, func() (*net.UnixListener, error) {\n\t\t\taddr, err := net.ResolveUnixAddr(network, addr)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tlt.logger.DebugMsg(\"new listener\", \"network\", network, \"address\", addr)\n\t\t\treturn net.ListenUnix(network, addr)\n\t\t})\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tl2, err := dupUnixListener(l)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn l2, nil\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported network type: %s\", network)\n\t}\n}\n\nfunc (lt *ListenerTracker) ResetUsage() {\n\tlt.tcp.MarkAllUnused()\n\tlt.unix.MarkAllUnused()\n}\n\nfunc (lt *ListenerTracker) CloseUnused() error {\n\tif err := lt.tcp.CloseUnused(func(key string) bool { return true }); err != nil {\n\t\tlt.logger.Error(\"CloseUnused for TCP failed\", err)\n\t}\n\tif err := lt.unix.CloseUnused(func(key string) bool { return true }); err != nil {\n\t\tlt.logger.Error(\"CloseUnused for Unix failed\", err)\n\t}\n\treturn nil\n}\n\nfunc (lt *ListenerTracker) Close() error {\n\tif err := lt.tcp.Close(); err != nil {\n\t\tlt.logger.Error(\"Close for TCP failed\", err)\n\t}\n\tif err := lt.unix.Close(); err != nil {\n\t\tlt.logger.Error(\"Close for Unix failed\", err)\n\t}\n\treturn nil\n}\n\nfunc NewListenerTracker(log *log.Logger) *ListenerTracker {\n\tlt := &ListenerTracker{\n\t\tlogger: log,\n\t\ttcp:    resource.NewTracker[*net.TCPListener](resource.NewSingleton[*net.TCPListener](log.Sublogger(\"tcp\"))),\n\t\tunix:   resource.NewTracker[*net.UnixListener](resource.NewSingleton[*net.UnixListener](log.Sublogger(\"unix\"))),\n\t}\n\n\treturn lt\n}\n"
  },
  {
    "path": "framework/resource/resource.go",
    "content": "package resource\n\nimport (\n\t\"io\"\n)\n\ntype Resource = io.Closer\n\ntype CheckableResource interface {\n\tResource\n\tIsUsable() bool\n}\n\ntype Container[T Resource] interface {\n\tio.Closer\n\tGetOpen(key string, open func() (T, error)) (T, error)\n\tCloseUnused(isUsed func(key string) bool) error\n}\n"
  },
  {
    "path": "framework/resource/singleton.go",
    "content": "package resource\n\nimport (\n\t\"sync\"\n\n\t\"github.com/foxcpp/maddy/framework/log\"\n)\n\n// Singleton represents a set of resources identified by an unique key.\ntype Singleton[T Resource] struct {\n\tlog       *log.Logger\n\tlock      sync.RWMutex\n\tresources map[string]T\n}\n\nfunc NewSingleton[T Resource](log *log.Logger) *Singleton[T] {\n\treturn &Singleton[T]{\n\t\tlog:       log,\n\t\tresources: make(map[string]T),\n\t}\n}\n\nfunc (s *Singleton[T]) GetOpen(key string, open func() (T, error)) (T, error) {\n\ts.lock.Lock()\n\tdefer s.lock.Unlock()\n\n\texisting, ok := s.resources[key]\n\tif ok {\n\t\ts.log.DebugMsg(\"resource reused\", \"key\", key)\n\t\treturn existing, nil\n\t}\n\n\tres, err := open()\n\tif err != nil {\n\t\tvar empty T\n\t\treturn empty, err\n\t}\n\n\ts.log.DebugMsg(\"new resource\", \"key\", key)\n\ts.resources[key] = res\n\n\treturn res, nil\n}\n\nfunc (s *Singleton[T]) CloseUnused(isUsed func(key string) bool) error {\n\ts.lock.Lock()\n\tdefer s.lock.Unlock()\n\n\tfor key, res := range s.resources {\n\t\tif isUsed(key) {\n\t\t\tcontinue\n\t\t}\n\t\tif err := res.Close(); err != nil {\n\t\t\ts.log.Error(\"resource close failed\", err, \"key\", key)\n\t\t}\n\t\ts.log.DebugMsg(\"resource released\", \"key\", key)\n\t\tdelete(s.resources, key)\n\t}\n\n\treturn nil\n}\n\nfunc (s *Singleton[T]) Close() error {\n\ts.lock.Lock()\n\tdefer s.lock.Unlock()\n\n\tfor key, res := range s.resources {\n\t\tif err := res.Close(); err != nil {\n\t\t\ts.log.Error(\"resource close failed\", err, \"key\", key)\n\t\t}\n\t\ts.log.DebugMsg(\"resource released\", \"key\", key)\n\t\tdelete(s.resources, key)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "framework/resource/tracker.go",
    "content": "package resource\n\nimport (\n\t\"sync\"\n)\n\n// Tracker is a container wrapper that tracks whether resources were used since\n// last MarkAllUnused call.\ntype Tracker[T Resource] struct {\n\tC Container[T]\n\n\tusedLock sync.Mutex\n\tused     map[string]bool\n}\n\nfunc NewTracker[T Resource](c Container[T]) *Tracker[T] {\n\treturn &Tracker[T]{C: c, used: make(map[string]bool)}\n}\n\nfunc (t *Tracker[T]) Close() error {\n\treturn t.C.Close()\n}\n\nfunc (t *Tracker[T]) MarkAllUnused() {\n\tt.usedLock.Lock()\n\tdefer t.usedLock.Unlock()\n\n\tt.used = make(map[string]bool)\n}\n\nfunc (t *Tracker[T]) GetOpen(key string, open func() (T, error)) (T, error) {\n\tt.usedLock.Lock()\n\tt.used[key] = true\n\tt.usedLock.Unlock()\n\n\treturn t.C.GetOpen(key, open)\n}\n\nfunc (t *Tracker[T]) CloseUnused(isUsed func(key string) bool) error {\n\tt.usedLock.Lock()\n\tdefer t.usedLock.Unlock()\n\n\treturn t.C.CloseUnused(func(key string) bool {\n\t\tused := t.used[key]\n\t\tused = used && isUsed(key)\n\t\tif !used {\n\t\t\tdelete(t.used, key)\n\t\t}\n\t\treturn used\n\t})\n}\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/foxcpp/maddy\n\ngo 1.23.1\n\ntoolchain go1.23.5\n\nrequire (\n\tblitiri.com.ar/go/spf v1.5.1\n\tgithub.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5\n\tgithub.com/c0va23/go-proxyprotocol v0.9.1\n\tgithub.com/caddyserver/certmagic v0.21.7\n\tgithub.com/emersion/go-imap v1.2.2-0.20220928192137-6fac715be9cf\n\tgithub.com/emersion/go-imap-compress v0.0.0-20201103190257-14809af1d1b9\n\tgithub.com/emersion/go-imap-sortthread v1.2.0\n\tgithub.com/emersion/go-message v0.18.2\n\tgithub.com/emersion/go-milter v0.4.1\n\tgithub.com/emersion/go-msgauth v0.6.8\n\tgithub.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6\n\tgithub.com/emersion/go-smtp v0.21.3\n\tgithub.com/foxcpp/go-dovecot-sasl v0.0.0-20260303144336-f7632c6ec0ba\n\tgithub.com/foxcpp/go-imap-backend-tests v0.0.0-20220105184719-e80aa29a5e16\n\tgithub.com/foxcpp/go-imap-i18nlevel v0.0.0-20200208001533-d6ec88553005\n\tgithub.com/foxcpp/go-imap-mess v0.0.0-20230108134257-b7ec3a649613\n\tgithub.com/foxcpp/go-imap-namespace v0.0.0-20200802091432-08496dd8e0ed\n\tgithub.com/foxcpp/go-imap-sql v0.5.1-0.20260412184517-b5e85e90f14d\n\tgithub.com/foxcpp/go-mockdns v1.1.0\n\tgithub.com/foxcpp/go-mtasts v0.0.0-20240130093538-1438da2e5932\n\tgithub.com/go-ldap/ldap/v3 v3.4.10\n\tgithub.com/go-sql-driver/mysql v1.8.1\n\tgithub.com/google/uuid v1.6.0\n\tgithub.com/hashicorp/go-hclog v1.6.3\n\tgithub.com/johannesboyne/gofakes3 v0.0.0-20210704111953-6a9f95c2941c\n\tgithub.com/lib/pq v1.10.9\n\tgithub.com/libdns/acmedns v0.2.0\n\tgithub.com/libdns/alidns v1.0.3\n\tgithub.com/libdns/cloudflare v0.1.1\n\tgithub.com/libdns/digitalocean v0.0.0-20230728223659-4f9064657aea\n\tgithub.com/libdns/gandi v1.0.3\n\tgithub.com/libdns/gcore v0.0.0-20250127070537-4a9d185c9d20\n\tgithub.com/libdns/googleclouddns v1.1.0\n\tgithub.com/libdns/hetzner v0.0.1\n\tgithub.com/libdns/leaseweb v0.4.0\n\tgithub.com/libdns/libdns v0.2.2\n\tgithub.com/libdns/metaname v0.3.0\n\tgithub.com/libdns/namecheap v0.0.0-20211109042440-fc7440785c8e\n\tgithub.com/libdns/namedotcom v0.3.3\n\tgithub.com/libdns/rfc2136 v0.1.1\n\tgithub.com/libdns/route53 v1.5.1\n\tgithub.com/libdns/vultr v1.0.0\n\tgithub.com/mattn/go-sqlite3 v1.14.24\n\tgithub.com/miekg/dns v1.1.63\n\tgithub.com/minio/minio-go/v7 v7.0.84\n\tgithub.com/netauth/netauth v0.6.2\n\tgithub.com/prometheus/client_golang v1.20.5\n\tgithub.com/stretchr/testify v1.10.0\n\tgithub.com/urfave/cli/v2 v2.27.5\n\tgo.uber.org/zap v1.27.0\n\tgolang.org/x/crypto v0.32.0\n\tgolang.org/x/net v0.34.0\n\tgolang.org/x/sync v0.10.0\n\tgolang.org/x/text v0.21.0\n\tmodernc.org/sqlite v1.34.5\n)\n\nrequire (\n\tcloud.google.com/go/auth v0.14.0 // indirect\n\tcloud.google.com/go/auth/oauth2adapt v0.2.7 // indirect\n\tcloud.google.com/go/compute/metadata v0.6.0 // indirect\n\tfilippo.io/edwards25519 v1.1.0 // indirect\n\tgithub.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect\n\tgithub.com/G-Core/gcore-dns-sdk-go v0.2.9 // indirect\n\tgithub.com/aws/aws-sdk-go v1.44.40 // indirect\n\tgithub.com/aws/aws-sdk-go-v2 v1.33.0 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/config v1.29.1 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/credentials v1.17.54 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.24 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/configsources v1.3.28 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.28 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.9 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/route53 v1.48.2 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/sso v1.24.11 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.10 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/sts v1.33.9 // indirect\n\tgithub.com/aws/smithy-go v1.22.2 // indirect\n\tgithub.com/beorn7/perks v1.0.1 // indirect\n\tgithub.com/caddyserver/zerossl v0.1.3 // indirect\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect\n\tgithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect\n\tgithub.com/digitalocean/godo v1.134.0 // indirect\n\tgithub.com/dustin/go-humanize v1.0.1 // indirect\n\tgithub.com/fatih/color v1.18.0 // indirect\n\tgithub.com/felixge/httpsnoop v1.0.4 // indirect\n\tgithub.com/fsnotify/fsnotify v1.8.0 // indirect\n\tgithub.com/go-asn1-ber/asn1-ber v1.5.7 // indirect\n\tgithub.com/go-ini/ini v1.67.0 // indirect\n\tgithub.com/go-logr/logr v1.4.2 // indirect\n\tgithub.com/go-logr/stdr v1.2.2 // indirect\n\tgithub.com/goccy/go-json v0.10.4 // indirect\n\tgithub.com/google/go-cmp v0.6.0 // indirect\n\tgithub.com/google/go-querystring v1.1.0 // indirect\n\tgithub.com/google/s2a-go v0.1.9 // indirect\n\tgithub.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect\n\tgithub.com/googleapis/gax-go/v2 v2.14.1 // indirect\n\tgithub.com/hashicorp/go-cleanhttp v0.5.2 // indirect\n\tgithub.com/hashicorp/go-retryablehttp v0.7.7 // indirect\n\tgithub.com/hashicorp/hcl v1.0.0 // indirect\n\tgithub.com/jimlambrt/gldap v0.1.14 // indirect\n\tgithub.com/jmespath/go-jmespath v0.4.0 // indirect\n\tgithub.com/josharian/intern v1.0.0 // indirect\n\tgithub.com/klauspost/compress v1.17.11 // indirect\n\tgithub.com/klauspost/cpuid/v2 v2.2.9 // indirect\n\tgithub.com/magiconair/properties v1.8.9 // indirect\n\tgithub.com/mailru/easyjson v0.9.0 // indirect\n\tgithub.com/mattn/go-colorable v0.1.14 // indirect\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgithub.com/mholt/acmez/v3 v3.0.1 // indirect\n\tgithub.com/minio/md5-simd v1.1.2 // indirect\n\tgithub.com/mitchellh/mapstructure v1.5.0 // indirect\n\tgithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect\n\tgithub.com/ncruces/go-strftime v0.1.9 // indirect\n\tgithub.com/netauth/protocol v0.0.0-20210918062754-7fee492ffcbd // indirect\n\tgithub.com/pelletier/go-toml/v2 v2.2.3 // indirect\n\tgithub.com/pierrec/lz4 v2.6.1+incompatible // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect\n\tgithub.com/prometheus/client_model v0.6.1 // indirect\n\tgithub.com/prometheus/common v0.62.0 // indirect\n\tgithub.com/prometheus/procfs v0.15.1 // indirect\n\tgithub.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect\n\tgithub.com/rs/xid v1.6.0 // indirect\n\tgithub.com/russross/blackfriday/v2 v2.1.0 // indirect\n\tgithub.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 // indirect\n\tgithub.com/sagikazarmark/locafero v0.7.0 // indirect\n\tgithub.com/sagikazarmark/slog-shim v0.1.0 // indirect\n\tgithub.com/shabbyrobe/gocovmerge v0.0.0-20180507124511-f6ea450bfb63 // indirect\n\tgithub.com/sourcegraph/conc v0.3.0 // indirect\n\tgithub.com/spf13/afero v1.12.0 // indirect\n\tgithub.com/spf13/cast v1.7.1 // indirect\n\tgithub.com/spf13/pflag v1.0.5 // indirect\n\tgithub.com/spf13/viper v1.19.0 // indirect\n\tgithub.com/subosito/gotenv v1.6.0 // indirect\n\tgithub.com/vultr/govultr/v3 v3.14.1 // indirect\n\tgithub.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect\n\tgithub.com/zeebo/blake3 v0.2.4 // indirect\n\tgo.opentelemetry.io/auto/sdk v1.1.0 // indirect\n\tgo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect\n\tgo.opentelemetry.io/otel v1.34.0 // indirect\n\tgo.opentelemetry.io/otel/metric v1.34.0 // indirect\n\tgo.opentelemetry.io/otel/trace v1.34.0 // indirect\n\tgo.uber.org/multierr v1.11.0 // indirect\n\tgo.uber.org/zap/exp v0.3.0 // indirect\n\tgolang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect\n\tgolang.org/x/mod v0.22.0 // indirect\n\tgolang.org/x/oauth2 v0.25.0 // indirect\n\tgolang.org/x/sys v0.29.0 // indirect\n\tgolang.org/x/time v0.9.0 // indirect\n\tgolang.org/x/tools v0.29.0 // indirect\n\tgoogle.golang.org/api v0.218.0 // indirect\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20250124145028-65684f501c47 // indirect\n\tgoogle.golang.org/grpc v1.70.0 // indirect\n\tgoogle.golang.org/protobuf v1.36.4 // indirect\n\tgopkg.in/ini.v1 v1.67.0 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n\tgotest.tools v2.2.0+incompatible // indirect\n\tmodernc.org/libc v1.61.9 // indirect\n\tmodernc.org/mathutil v1.7.1 // indirect\n\tmodernc.org/memory v1.8.2 // indirect\n)\n\nreplace github.com/emersion/go-imap => github.com/foxcpp/go-imap v1.0.0-beta.1.0.20220623182312-df940c324887\n\nreplace github.com/emersion/go-smtp => github.com/foxcpp/go-smtp v1.21.4-0.20250124171104-c8519ae4fb23 // v1.21.3+maddy.1\n\nreplace github.com/libdns/gandi => github.com/foxcpp/libdns-gandi v1.0.4-0.20240127130558-4782f9d5ce3e // v1.0.3+maddy.1\n"
  },
  {
    "path": "go.sum",
    "content": "blitiri.com.ar/go/spf v1.5.1 h1:CWUEasc44OrANJD8CzceRnRn1Jv0LttY68cYym2/pbE=\nblitiri.com.ar/go/spf v1.5.1/go.mod h1:E71N92TfL4+Yyd5lpKuE9CAF2pd4JrUq1xQfkTxoNdk=\ncloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=\ncloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=\ncloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=\ncloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=\ncloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=\ncloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=\ncloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=\ncloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=\ncloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=\ncloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=\ncloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=\ncloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=\ncloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=\ncloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=\ncloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=\ncloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=\ncloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=\ncloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=\ncloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY=\ncloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM=\ncloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY=\ncloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ=\ncloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI=\ncloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4=\ncloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc=\ncloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA=\ncloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A=\ncloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc=\ncloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU=\ncloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRYtA=\ncloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE=\ncloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U=\ncloud.google.com/go/aiplatform v1.22.0/go.mod h1:ig5Nct50bZlzV6NvKaTwmplLLddFx0YReh9WfTO5jKw=\ncloud.google.com/go/aiplatform v1.24.0/go.mod h1:67UUvRBKG6GTayHKV8DBv2RtR1t93YRu5B1P3x99mYY=\ncloud.google.com/go/analytics v0.11.0/go.mod h1:DjEWCu41bVbYcKyvlws9Er60YE4a//bK6mnhWvQeFNI=\ncloud.google.com/go/analytics v0.12.0/go.mod h1:gkfj9h6XRf9+TS4bmuhPEShsh3hH8PAZzm/41OOhQd4=\ncloud.google.com/go/area120 v0.5.0/go.mod h1:DE/n4mp+iqVyvxHN41Vf1CR602GiHQjFPusMFW6bGR4=\ncloud.google.com/go/area120 v0.6.0/go.mod h1:39yFJqWVgm0UZqWTOdqkLhjoC7uFfgXRC8g/ZegeAh0=\ncloud.google.com/go/artifactregistry v1.6.0/go.mod h1:IYt0oBPSAGYj/kprzsBjZ/4LnG/zOcHyFHjWPCi6SAQ=\ncloud.google.com/go/artifactregistry v1.7.0/go.mod h1:mqTOFOnGZx8EtSqK/ZWcsm/4U8B77rbcLP6ruDU2Ixk=\ncloud.google.com/go/asset v1.5.0/go.mod h1:5mfs8UvcM5wHhqtSv8J1CtxxaQq3AdBxxQi2jGW/K4o=\ncloud.google.com/go/asset v1.7.0/go.mod h1:YbENsRK4+xTiL+Ofoj5Ckf+O17kJtgp3Y3nn4uzZz5s=\ncloud.google.com/go/asset v1.8.0/go.mod h1:mUNGKhiqIdbr8X7KNayoYvyc4HbbFO9URsjbytpUaW0=\ncloud.google.com/go/assuredworkloads v1.5.0/go.mod h1:n8HOZ6pff6re5KYfBXcFvSViQjDwxFkAkmUFffJRbbY=\ncloud.google.com/go/assuredworkloads v1.6.0/go.mod h1:yo2YOk37Yc89Rsd5QMVECvjaMKymF9OP+QXWlKXUkXw=\ncloud.google.com/go/assuredworkloads v1.7.0/go.mod h1:z/736/oNmtGAyU47reJgGN+KVoYoxeLBoj4XkKYscNI=\ncloud.google.com/go/auth v0.14.0 h1:A5C4dKV/Spdvxcl0ggWwWEzzP7AZMJSEIgrkngwhGYM=\ncloud.google.com/go/auth v0.14.0/go.mod h1:CYsoRL1PdiDuqeQpZE0bP2pnPrGqFcOkI0nldEQis+A=\ncloud.google.com/go/auth/oauth2adapt v0.2.7 h1:/Lc7xODdqcEw8IrZ9SvwnlLX6j9FHQM74z6cBk9Rw6M=\ncloud.google.com/go/auth/oauth2adapt v0.2.7/go.mod h1:NTbTTzfvPl1Y3V1nPpOgl2w6d/FjO7NNUQaWSox6ZMc=\ncloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0=\ncloud.google.com/go/automl v1.6.0/go.mod h1:ugf8a6Fx+zP0D59WLhqgTDsQI9w07o64uf/Is3Nh5p8=\ncloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=\ncloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=\ncloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=\ncloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=\ncloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=\ncloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=\ncloud.google.com/go/bigquery v1.42.0/go.mod h1:8dRTJxhtG+vwBKzE5OseQn/hiydoQN3EedCaOdYmxRA=\ncloud.google.com/go/billing v1.4.0/go.mod h1:g9IdKBEFlItS8bTtlrZdVLWSSdSyFUZKXNS02zKMOZY=\ncloud.google.com/go/billing v1.5.0/go.mod h1:mztb1tBc3QekhjSgmpf/CV4LzWXLzCArwpLmP2Gm88s=\ncloud.google.com/go/binaryauthorization v1.1.0/go.mod h1:xwnoWu3Y84jbuHa0zd526MJYmtnVXn0syOjaJgy4+dM=\ncloud.google.com/go/binaryauthorization v1.2.0/go.mod h1:86WKkJHtRcv5ViNABtYMhhNWRrD1Vpi//uKEy7aYEfI=\ncloud.google.com/go/cloudtasks v1.5.0/go.mod h1:fD92REy1x5woxkKEkLdvavGnPJGEn8Uic9nWuLzqCpY=\ncloud.google.com/go/cloudtasks v1.6.0/go.mod h1:C6Io+sxuke9/KNRkbQpihnW93SWDU3uXt92nu85HkYI=\ncloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow=\ncloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM=\ncloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M=\ncloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s=\ncloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU=\ncloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U=\ncloud.google.com/go/compute v1.10.0/go.mod h1:ER5CLbMxl90o2jtNbGSbtfOpQKR0t15FOtRsugnLrlU=\ncloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=\ncloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=\ncloud.google.com/go/containeranalysis v0.5.1/go.mod h1:1D92jd8gRR/c0fGMlymRgxWD3Qw9C1ff6/T7mLgVL8I=\ncloud.google.com/go/containeranalysis v0.6.0/go.mod h1:HEJoiEIu+lEXM+k7+qLCci0h33lX3ZqoYFdmPcoO7s4=\ncloud.google.com/go/datacatalog v1.3.0/go.mod h1:g9svFY6tuR+j+hrTw3J2dNcmI0dzmSiyOzm8kpLq0a0=\ncloud.google.com/go/datacatalog v1.5.0/go.mod h1:M7GPLNQeLfWqeIm3iuiruhPzkt65+Bx8dAKvScX8jvs=\ncloud.google.com/go/datacatalog v1.6.0/go.mod h1:+aEyF8JKg+uXcIdAmmaMUmZ3q1b/lKLtXCmXdnc0lbc=\ncloud.google.com/go/dataflow v0.6.0/go.mod h1:9QwV89cGoxjjSR9/r7eFDqqjtvbKxAK2BaYU6PVk9UM=\ncloud.google.com/go/dataflow v0.7.0/go.mod h1:PX526vb4ijFMesO1o202EaUmouZKBpjHsTlCtB4parQ=\ncloud.google.com/go/dataform v0.3.0/go.mod h1:cj8uNliRlHpa6L3yVhDOBrUXH+BPAO1+KFMQQNSThKo=\ncloud.google.com/go/dataform v0.4.0/go.mod h1:fwV6Y4Ty2yIFL89huYlEkwUPtS7YZinZbzzj5S9FzCE=\ncloud.google.com/go/datalabeling v0.5.0/go.mod h1:TGcJ0G2NzcsXSE/97yWjIZO0bXj0KbVlINXMG9ud42I=\ncloud.google.com/go/datalabeling v0.6.0/go.mod h1:WqdISuk/+WIGeMkpw/1q7bK/tFEZxsrFJOJdY2bXvTQ=\ncloud.google.com/go/dataqna v0.5.0/go.mod h1:90Hyk596ft3zUQ8NkFfvICSIfHFh1Bc7C4cK3vbhkeo=\ncloud.google.com/go/dataqna v0.6.0/go.mod h1:1lqNpM7rqNLVgWBJyk5NF6Uen2PHym0jtVJonplVsDA=\ncloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=\ncloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=\ncloud.google.com/go/datastream v1.2.0/go.mod h1:i/uTP8/fZwgATHS/XFu0TcNUhuA0twZxxQ3EyCUQMwo=\ncloud.google.com/go/datastream v1.3.0/go.mod h1:cqlOX8xlyYF/uxhiKn6Hbv6WjwPPuI9W2M9SAXwaLLQ=\ncloud.google.com/go/dialogflow v1.15.0/go.mod h1:HbHDWs33WOGJgn6rfzBW1Kv807BE3O1+xGbn59zZWI4=\ncloud.google.com/go/dialogflow v1.16.1/go.mod h1:po6LlzGfK+smoSmTBnbkIZY2w8ffjz/RcGSS+sh1el0=\ncloud.google.com/go/dialogflow v1.17.0/go.mod h1:YNP09C/kXA1aZdBgC/VtXX74G/TKn7XVCcVumTflA+8=\ncloud.google.com/go/documentai v1.7.0/go.mod h1:lJvftZB5NRiFSX4moiye1SMxHx0Bc3x1+p9e/RfXYiU=\ncloud.google.com/go/documentai v1.8.0/go.mod h1:xGHNEB7CtsnySCNrCFdCyyMz44RhFEEX2Q7UD0c5IhU=\ncloud.google.com/go/domains v0.6.0/go.mod h1:T9Rz3GasrpYk6mEGHh4rymIhjlnIuB4ofT1wTxDeT4Y=\ncloud.google.com/go/domains v0.7.0/go.mod h1:PtZeqS1xjnXuRPKE/88Iru/LdfoRyEHYA9nFQf4UKpg=\ncloud.google.com/go/edgecontainer v0.1.0/go.mod h1:WgkZ9tp10bFxqO8BLPqv2LlfmQF1X8lZqwW4r1BTajk=\ncloud.google.com/go/edgecontainer v0.2.0/go.mod h1:RTmLijy+lGpQ7BXuTDa4C4ssxyXT34NIuHIgKuP4s5w=\ncloud.google.com/go/functions v1.6.0/go.mod h1:3H1UA3qiIPRWD7PeZKLvHZ9SaQhR26XIJcC0A5GbvAk=\ncloud.google.com/go/functions v1.7.0/go.mod h1:+d+QBcWM+RsrgZfV9xo6KfA1GlzJfxcfZcRPEhDDfzg=\ncloud.google.com/go/gaming v1.5.0/go.mod h1:ol7rGcxP/qHTRQE/RO4bxkXq+Fix0j6D4LFPzYTIrDM=\ncloud.google.com/go/gaming v1.6.0/go.mod h1:YMU1GEvA39Qt3zWGyAVA9bpYz/yAhTvaQ1t2sK4KPUA=\ncloud.google.com/go/gkeconnect v0.5.0/go.mod h1:c5lsNAg5EwAy7fkqX/+goqFsU1Da/jQFqArp+wGNr/o=\ncloud.google.com/go/gkeconnect v0.6.0/go.mod h1:Mln67KyU/sHJEBY8kFZ0xTeyPtzbq9StAVvEULYK16A=\ncloud.google.com/go/gkehub v0.9.0/go.mod h1:WYHN6WG8w9bXU0hqNxt8rm5uxnk8IH+lPY9J2TV7BK0=\ncloud.google.com/go/gkehub v0.10.0/go.mod h1:UIPwxI0DsrpsVoWpLB0stwKCP+WFVG9+y977wO+hBH0=\ncloud.google.com/go/grafeas v0.2.0/go.mod h1:KhxgtF2hb0P191HlY5besjYm6MqTSTj3LSI+M+ByZHc=\ncloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY=\ncloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic=\ncloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI=\ncloud.google.com/go/lifesciences v0.5.0/go.mod h1:3oIKy8ycWGPUyZDR/8RNnTOYevhaMLqh5vLUXs9zvT8=\ncloud.google.com/go/lifesciences v0.6.0/go.mod h1:ddj6tSX/7BOnhxCSd3ZcETvtNr8NZ6t/iPhY2Tyfu08=\ncloud.google.com/go/mediatranslation v0.5.0/go.mod h1:jGPUhGTybqsPQn91pNXw0xVHfuJ3leR1wj37oU3y1f4=\ncloud.google.com/go/mediatranslation v0.6.0/go.mod h1:hHdBCTYNigsBxshbznuIMFNe5QXEowAuNmmC7h8pu5w=\ncloud.google.com/go/memcache v1.4.0/go.mod h1:rTOfiGZtJX1AaFUrOgsMHX5kAzaTQ8azHiuDoTPzNsE=\ncloud.google.com/go/memcache v1.5.0/go.mod h1:dk3fCK7dVo0cUU2c36jKb4VqKPS22BTkf81Xq617aWM=\ncloud.google.com/go/metastore v1.5.0/go.mod h1:2ZNrDcQwghfdtCwJ33nM0+GrBGlVuh8rakL3vdPY3XY=\ncloud.google.com/go/metastore v1.6.0/go.mod h1:6cyQTls8CWXzk45G55x57DVQ9gWg7RiH65+YgPsNh9s=\ncloud.google.com/go/networkconnectivity v1.4.0/go.mod h1:nOl7YL8odKyAOtzNX73/M5/mGZgqqMeryi6UPZTk/rA=\ncloud.google.com/go/networkconnectivity v1.5.0/go.mod h1:3GzqJx7uhtlM3kln0+x5wyFvuVH1pIBJjhCpjzSt75o=\ncloud.google.com/go/networksecurity v0.5.0/go.mod h1:xS6fOCoqpVC5zx15Z/MqkfDwH4+m/61A3ODiDV1xmiQ=\ncloud.google.com/go/networksecurity v0.6.0/go.mod h1:Q5fjhTr9WMI5mbpRYEbiexTzROf7ZbDzvzCrNl14nyU=\ncloud.google.com/go/notebooks v1.2.0/go.mod h1:9+wtppMfVPUeJ8fIWPOq1UnATHISkGXGqTkxeieQ6UY=\ncloud.google.com/go/notebooks v1.3.0/go.mod h1:bFR5lj07DtCPC7YAAJ//vHskFBxA5JzYlH68kXVdk34=\ncloud.google.com/go/osconfig v1.7.0/go.mod h1:oVHeCeZELfJP7XLxcBGTMBvRO+1nQ5tFG9VQTmYS2Fs=\ncloud.google.com/go/osconfig v1.8.0/go.mod h1:EQqZLu5w5XA7eKizepumcvWx+m8mJUhEwiPqWiZeEdg=\ncloud.google.com/go/oslogin v1.4.0/go.mod h1:YdgMXWRaElXz/lDk1Na6Fh5orF7gvmJ0FGLIs9LId4E=\ncloud.google.com/go/oslogin v1.5.0/go.mod h1:D260Qj11W2qx/HVF29zBg+0fd6YCSjSqLUkY/qEenQU=\ncloud.google.com/go/phishingprotection v0.5.0/go.mod h1:Y3HZknsK9bc9dMi+oE8Bim0lczMU6hrX0UpADuMefr0=\ncloud.google.com/go/phishingprotection v0.6.0/go.mod h1:9Y3LBLgy0kDTcYET8ZH3bq/7qni15yVUoAxiFxnlSUA=\ncloud.google.com/go/privatecatalog v0.5.0/go.mod h1:XgosMUvvPyxDjAVNDYxJ7wBW8//hLDDYmnsNcMGq1K0=\ncloud.google.com/go/privatecatalog v0.6.0/go.mod h1:i/fbkZR0hLN29eEWiiwue8Pb+GforiEIBnV9yrRUOKI=\ncloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=\ncloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=\ncloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=\ncloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=\ncloud.google.com/go/recaptchaenterprise v1.3.1/go.mod h1:OdD+q+y4XGeAlxRaMn1Y7/GveP6zmq76byL6tjPE7d4=\ncloud.google.com/go/recaptchaenterprise/v2 v2.1.0/go.mod h1:w9yVqajwroDNTfGuhmOjPDN//rZGySaf6PtFVcSCa7o=\ncloud.google.com/go/recaptchaenterprise/v2 v2.2.0/go.mod h1:/Zu5jisWGeERrd5HnlS3EUGb/D335f9k51B/FVil0jk=\ncloud.google.com/go/recaptchaenterprise/v2 v2.3.0/go.mod h1:O9LwGCjrhGHBQET5CA7dd5NwwNQUErSgEDit1DLNTdo=\ncloud.google.com/go/recommendationengine v0.5.0/go.mod h1:E5756pJcVFeVgaQv3WNpImkFP8a+RptV6dDLGPILjvg=\ncloud.google.com/go/recommendationengine v0.6.0/go.mod h1:08mq2umu9oIqc7tDy8sx+MNJdLG0fUi3vaSVbztHgJ4=\ncloud.google.com/go/recommender v1.5.0/go.mod h1:jdoeiBIVrJe9gQjwd759ecLJbxCDED4A6p+mqoqDvTg=\ncloud.google.com/go/recommender v1.6.0/go.mod h1:+yETpm25mcoiECKh9DEScGzIRyDKpZ0cEhWGo+8bo+c=\ncloud.google.com/go/redis v1.7.0/go.mod h1:V3x5Jq1jzUcg+UNsRvdmsfuFnit1cfe3Z/PGyq/lm4Y=\ncloud.google.com/go/redis v1.8.0/go.mod h1:Fm2szCDavWzBk2cDKxrkmWBqoCiL1+Ctwq7EyqBCA/A=\ncloud.google.com/go/retail v1.8.0/go.mod h1:QblKS8waDmNUhghY2TI9O3JLlFk8jybHeV4BF19FrE4=\ncloud.google.com/go/retail v1.9.0/go.mod h1:g6jb6mKuCS1QKnH/dpu7isX253absFl6iE92nHwlBUY=\ncloud.google.com/go/scheduler v1.4.0/go.mod h1:drcJBmxF3aqZJRhmkHQ9b3uSSpQoltBPGPxGAWROx6s=\ncloud.google.com/go/scheduler v1.5.0/go.mod h1:ri073ym49NW3AfT6DZi21vLZrG07GXr5p3H1KxN5QlI=\ncloud.google.com/go/secretmanager v1.6.0/go.mod h1:awVa/OXF6IiyaU1wQ34inzQNc4ISIDIrId8qE5QGgKA=\ncloud.google.com/go/security v1.5.0/go.mod h1:lgxGdyOKKjHL4YG3/YwIL2zLqMFCKs0UbQwgyZmfJl4=\ncloud.google.com/go/security v1.7.0/go.mod h1:mZklORHl6Bg7CNnnjLH//0UlAlaXqiG7Lb9PsPXLfD0=\ncloud.google.com/go/security v1.8.0/go.mod h1:hAQOwgmaHhztFhiQ41CjDODdWP0+AE1B3sX4OFlq+GU=\ncloud.google.com/go/securitycenter v1.13.0/go.mod h1:cv5qNAqjY84FCN6Y9z28WlkKXyWsgLO832YiWwkCWcU=\ncloud.google.com/go/securitycenter v1.14.0/go.mod h1:gZLAhtyKv85n52XYWt6RmeBdydyxfPeTrpToDPw4Auc=\ncloud.google.com/go/servicedirectory v1.4.0/go.mod h1:gH1MUaZCgtP7qQiI+F+A+OpeKF/HQWgtAddhTbhL2bs=\ncloud.google.com/go/servicedirectory v1.5.0/go.mod h1:QMKFL0NUySbpZJ1UZs3oFAmdvVxhhxB6eJ/Vlp73dfg=\ncloud.google.com/go/speech v1.6.0/go.mod h1:79tcr4FHCimOp56lwC01xnt/WPJZc4v3gzyT7FoBkCM=\ncloud.google.com/go/speech v1.7.0/go.mod h1:KptqL+BAQIhMsj1kOP2la5DSEEerPDuOP/2mmkhHhZQ=\ncloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=\ncloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=\ncloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=\ncloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=\ncloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=\ncloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y=\ncloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeLgDvXzfIXc=\ncloud.google.com/go/talent v1.1.0/go.mod h1:Vl4pt9jiHKvOgF9KoZo6Kob9oV4lwd/ZD5Cto54zDRw=\ncloud.google.com/go/talent v1.2.0/go.mod h1:MoNF9bhFQbiJ6eFD3uSsg0uBALw4n4gaCaEjBw9zo8g=\ncloud.google.com/go/videointelligence v1.6.0/go.mod h1:w0DIDlVRKtwPCn/C4iwZIJdvC69yInhW0cfi+p546uU=\ncloud.google.com/go/videointelligence v1.7.0/go.mod h1:k8pI/1wAhjznARtVT9U1llUaFNPh7muw8QyOUpavru4=\ncloud.google.com/go/vision v1.2.0/go.mod h1:SmNwgObm5DpFBme2xpyOyasvBc1aPdjvMk2bBk0tKD0=\ncloud.google.com/go/vision/v2 v2.2.0/go.mod h1:uCdV4PpN1S0jyCyq8sIM42v2Y6zOLkZs+4R9LrGYwFo=\ncloud.google.com/go/vision/v2 v2.3.0/go.mod h1:UO61abBx9QRMFkNBbf1D8B1LXdS2cGiiCRx0vSpZoUo=\ncloud.google.com/go/webrisk v1.4.0/go.mod h1:Hn8X6Zr+ziE2aNd8SliSDWpEnSS1u4R9+xXZmFiHmGE=\ncloud.google.com/go/webrisk v1.5.0/go.mod h1:iPG6fr52Tv7sGk0H6qUFzmL3HHZev1htXuWDEEsqMTg=\ncloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1Vwf+KmJENM0=\ncloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoISEXH2bcHC3M=\ndmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=\nfilippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=\nfilippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=\ngithub.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=\ngithub.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=\ngithub.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=\ngithub.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=\ngithub.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=\ngithub.com/G-Core/gcore-dns-sdk-go v0.2.9 h1:LMMZIRX8y3aJJuAviNSpFmLbovZUw+6Om+8VElp1F90=\ngithub.com/G-Core/gcore-dns-sdk-go v0.2.9/go.mod h1:35t795gOfzfVanhzkFyUXEzaBuMXwETmJldPpP28MN4=\ngithub.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5 h1:IEjq88XO4PuBDcvmjQJcQGg+w+UaafSy8G5Kcb5tBhI=\ngithub.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5/go.mod h1:exZ0C/1emQJAw5tHOaUDyY1ycttqBAPcxuzf7QbY6ec=\ngithub.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=\ngithub.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=\ngithub.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=\ngithub.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=\ngithub.com/aws/aws-sdk-go v1.17.4/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=\ngithub.com/aws/aws-sdk-go v1.44.40 h1:MR0qefjBJrZuXE0VoeKMQFtjS2tUeVpbQNfb7NzQNgI=\ngithub.com/aws/aws-sdk-go v1.44.40/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=\ngithub.com/aws/aws-sdk-go-v2 v1.33.0 h1:Evgm4DI9imD81V0WwD+TN4DCwjUMdc94TrduMLbgZJs=\ngithub.com/aws/aws-sdk-go-v2 v1.33.0/go.mod h1:P5WJBrYqqbWVaOxgH0X/FYYD47/nooaPOZPlQdmiN2U=\ngithub.com/aws/aws-sdk-go-v2/config v1.29.1 h1:JZhGawAyZ/EuJeBtbQYnaoftczcb2drR2Iq36Wgz4sQ=\ngithub.com/aws/aws-sdk-go-v2/config v1.29.1/go.mod h1:7bR2YD5euaxBhzt2y/oDkt3uNRb6tjFp98GlTFueRwk=\ngithub.com/aws/aws-sdk-go-v2/credentials v1.17.54 h1:4UmqeOqJPvdvASZWrKlhzpRahAulBfyTJQUaYy4+hEI=\ngithub.com/aws/aws-sdk-go-v2/credentials v1.17.54/go.mod h1:RTdfo0P0hbbTxIhmQrOsC/PquBZGabEPnCaxxKRPSnI=\ngithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.24 h1:5grmdTdMsovn9kPZPI23Hhvp0ZyNm5cRO+IZFIYiAfw=\ngithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.24/go.mod h1:zqi7TVKTswH3Ozq28PkmBmgzG1tona7mo9G2IJg4Cis=\ngithub.com/aws/aws-sdk-go-v2/internal/configsources v1.3.28 h1:igORFSiH3bfq4lxKFkTSYDhJEUCYo6C8VKiWJjYwQuQ=\ngithub.com/aws/aws-sdk-go-v2/internal/configsources v1.3.28/go.mod h1:3So8EA/aAYm36L7XIvCVwLa0s5N0P7o2b1oqnx/2R4g=\ngithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.28 h1:1mOW9zAUMhTSrMDssEHS/ajx8JcAj/IcftzcmNlmVLI=\ngithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.28/go.mod h1:kGlXVIWDfvt2Ox5zEaNglmq0hXPHgQFNMix33Tw22jA=\ngithub.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ=\ngithub.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=\ngithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 h1:iXtILhvDxB6kPvEXgsDhGaZCSC6LQET5ZHSdJozeI0Y=\ngithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1/go.mod h1:9nu0fVANtYiAePIBh2/pFUSwtJ402hLnp854CNoDOeE=\ngithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.9 h1:TQmKDyETFGiXVhZfQ/I0cCFziqqX58pi4tKJGYGFSz0=\ngithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.9/go.mod h1:HVLPK2iHQBUx7HfZeOQSEu3v2ubZaAY2YPbAm5/WUyY=\ngithub.com/aws/aws-sdk-go-v2/service/route53 v1.48.2 h1:Rxg1R0CHxVb9ggQLufOkr4an3yFEkTDN+N5+LFU4aEg=\ngithub.com/aws/aws-sdk-go-v2/service/route53 v1.48.2/go.mod h1:TN4PcCL0lvqmYcv+AV8iZFC4Sd0FM06QDaoBXrFEftU=\ngithub.com/aws/aws-sdk-go-v2/service/sso v1.24.11 h1:kuIyu4fTT38Kj7YCC7ouNbVZSSpqkZ+LzIfhCr6Dg+I=\ngithub.com/aws/aws-sdk-go-v2/service/sso v1.24.11/go.mod h1:Ro744S4fKiCCuZECXgOi760TiYylUM8ZBf6OGiZzJtY=\ngithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.10 h1:l+dgv/64iVlQ3WsBbnn+JSbkj01jIi+SM0wYsj3y/hY=\ngithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.10/go.mod h1:Fzsj6lZEb8AkTE5S68OhcbBqeWPsR8RnGuKPr8Todl8=\ngithub.com/aws/aws-sdk-go-v2/service/sts v1.33.9 h1:BRVDbewN6VZcwr+FBOszDKvYeXY1kJ+GGMCcpghlw0U=\ngithub.com/aws/aws-sdk-go-v2/service/sts v1.33.9/go.mod h1:f6vjfZER1M17Fokn0IzssOTMT2N8ZSq+7jnNF0tArvw=\ngithub.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ=\ngithub.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=\ngithub.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=\ngithub.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=\ngithub.com/c0va23/go-proxyprotocol v0.9.1 h1:5BCkp0fDJOhzzH1lhjUgHhmZz9VvRMMif1U2D31hb34=\ngithub.com/c0va23/go-proxyprotocol v0.9.1/go.mod h1:TNjUV+llvk8TvWJxlPYAeAYZgSzT/iicNr3nWBWX320=\ngithub.com/caddyserver/certmagic v0.21.7 h1:66KJioPFJwttL43KYSWk7ErSmE6LfaJgCQuhm8Sg6fg=\ngithub.com/caddyserver/certmagic v0.21.7/go.mod h1:LCPG3WLxcnjVKl/xpjzM0gqh0knrKKKiO5WVttX2eEI=\ngithub.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA=\ngithub.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=\ngithub.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=\ngithub.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=\ngithub.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=\ngithub.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=\ngithub.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=\ngithub.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=\ngithub.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=\ngithub.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=\ngithub.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=\ngithub.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=\ngithub.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/digitalocean/godo v1.41.0/go.mod h1:p7dOjjtSBqCTUksqtA5Fd3uaKs9kyTq2xcz76ulEJRU=\ngithub.com/digitalocean/godo v1.134.0 h1:dT7aQR9jxNOQEZwzP+tAYcxlj5szFZScC33+PAYGQVM=\ngithub.com/digitalocean/godo v1.134.0/go.mod h1:PU8JB6I1XYkQIdHFop8lLAY9ojp6M0XcU0TWaQSxbrc=\ngithub.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=\ngithub.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=\ngithub.com/emersion/go-imap-appendlimit v0.0.0-20190308131241-25671c986a6a/go.mod h1:ikgISoP7pRAolqsVP64yMteJa2FIpS6ju88eBT6K1yQ=\ngithub.com/emersion/go-imap-compress v0.0.0-20201103190257-14809af1d1b9 h1:7dmV11mle4UAQ7lX+Hdzx6akKFg3hVm/UUmQ7t6VgTQ=\ngithub.com/emersion/go-imap-compress v0.0.0-20201103190257-14809af1d1b9/go.mod h1:2Ro1PbmiqYiRe5Ct2sGR5hHaKSVHeRpVZwXx8vyYt98=\ngithub.com/emersion/go-imap-move v0.0.0-20180601155324-5eb20cb834bf/go.mod h1:QuMaZcKFDVI0yCrnAbPLfbwllz1wtOrZH8/vZ5yzp4w=\ngithub.com/emersion/go-imap-sortthread v1.2.0 h1:EMVEJXPWAhXMWECjR82Rn/tza6MddcvTwGAdTu1vJKU=\ngithub.com/emersion/go-imap-sortthread v1.2.0/go.mod h1:UhenCBupR+vSYRnqJkpjSq84INUCsyAK1MLpogv14pE=\ngithub.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4=\ngithub.com/emersion/go-message v0.18.0/go.mod h1:Zi69ACvzaoV/MBnrxfVBPV3xWEuCmC2nEN39oJF4B8A=\ngithub.com/emersion/go-message v0.18.1/go.mod h1:XpJyL70LwRvq2a8rVbHXikPgKj8+aI0kGdHlg16ibYA=\ngithub.com/emersion/go-message v0.18.2 h1:rl55SQdjd9oJcIoQNhubD2Acs1E6IzlZISRTK7x/Lpg=\ngithub.com/emersion/go-message v0.18.2/go.mod h1:XpJyL70LwRvq2a8rVbHXikPgKj8+aI0kGdHlg16ibYA=\ngithub.com/emersion/go-milter v0.4.1 h1:gLs9QD0zEHF8omgEw8M+aGz6iwBNpWLAcwgSur0ra4M=\ngithub.com/emersion/go-milter v0.4.1/go.mod h1:erCQVl0mH4SX9jEvwe+wyndit0rQtmvMLH86V6NGtkI=\ngithub.com/emersion/go-msgauth v0.6.8 h1:kW/0E9E8Zx5CdKsERC/WnAvnXvX7q9wTHia1OA4944A=\ngithub.com/emersion/go-msgauth v0.6.8/go.mod h1:YDwuyTCUHu9xxmAeVj0eW4INnwB6NNZoPdLerpSxRrc=\ngithub.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k=\ngithub.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=\ngithub.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=\ngithub.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk=\ngithub.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=\ngithub.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=\ngithub.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=\ngithub.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=\ngithub.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=\ngithub.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=\ngithub.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=\ngithub.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=\ngithub.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=\ngithub.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=\ngithub.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=\ngithub.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=\ngithub.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=\ngithub.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=\ngithub.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=\ngithub.com/foxcpp/go-dovecot-sasl v0.0.0-20200522223722-c4699d7a24bf h1:rmBPY5fryjp9zLQYsUmQqqgsYq7qeVfrjtr96Tf9vD8=\ngithub.com/foxcpp/go-dovecot-sasl v0.0.0-20200522223722-c4699d7a24bf/go.mod h1:5yZUmwr851vgjyAfN7OEfnrmKOh/qLA5dbGelXYsu1E=\ngithub.com/foxcpp/go-dovecot-sasl v0.0.0-20260303144336-f7632c6ec0ba h1:yxQhqX9RQCvECZKBtqwCZoKy/6CLaozDZeWH9Lvndy0=\ngithub.com/foxcpp/go-dovecot-sasl v0.0.0-20260303144336-f7632c6ec0ba/go.mod h1:5yZUmwr851vgjyAfN7OEfnrmKOh/qLA5dbGelXYsu1E=\ngithub.com/foxcpp/go-imap v1.0.0-beta.1.0.20220623182312-df940c324887 h1:qUoaaHyrRpQw85ru6VQcC6JowdhrWl7lSbI1zRX1FTM=\ngithub.com/foxcpp/go-imap v1.0.0-beta.1.0.20220623182312-df940c324887/go.mod h1:Qlx1FSx2FTxjnjWpIlVNEuX+ylerZQNFE5NsmKFSejY=\ngithub.com/foxcpp/go-imap-backend-tests v0.0.0-20220105184719-e80aa29a5e16 h1:qheFPDpteiUy7Ym18R68OYenpk85UyKYGkhYTmddSBg=\ngithub.com/foxcpp/go-imap-backend-tests v0.0.0-20220105184719-e80aa29a5e16/go.mod h1:OPP1AgKxMPo3aHX5pcEZLQhhh5sllFcB8aUN9f6a6X8=\ngithub.com/foxcpp/go-imap-i18nlevel v0.0.0-20200208001533-d6ec88553005 h1:pfoFtkTTQ473qStSN79jhCFBWqMQt/3DQ3NGuXvT+50=\ngithub.com/foxcpp/go-imap-i18nlevel v0.0.0-20200208001533-d6ec88553005/go.mod h1:34FwxnjC2N+EFs2wMtsHevrZLWRKRuVU8wEcHWKq/nE=\ngithub.com/foxcpp/go-imap-mess v0.0.0-20230108134257-b7ec3a649613 h1:fw9OWfPxP1CK4D+XAEEg0JzhvFGo04L+F5Xw55t9s3E=\ngithub.com/foxcpp/go-imap-mess v0.0.0-20230108134257-b7ec3a649613/go.mod h1:P/O/qz4gaVkefzJ40BUtN/ZzBnaEg0YYe1no/SMp7Aw=\ngithub.com/foxcpp/go-imap-namespace v0.0.0-20200802091432-08496dd8e0ed h1:1Jo7geyvunrPSjL6F6D9EcXoNApS5v3LQaro7aUNPnE=\ngithub.com/foxcpp/go-imap-namespace v0.0.0-20200802091432-08496dd8e0ed/go.mod h1:Shows1vmkBWO40ChOClaUe6DUnZrsP1UPAuoWzIUdgQ=\ngithub.com/foxcpp/go-imap-sql v0.5.1-0.20250124140007-8da5567429d5 h1:jMxhw9qmwqg70qfMDWq0ImRHAduQjkTZOC9vBs5t2ug=\ngithub.com/foxcpp/go-imap-sql v0.5.1-0.20250124140007-8da5567429d5/go.mod h1:LMlfyNkVs7v2zE6OVeGe9qWPmKFdXDmLNddPLodPVIw=\ngithub.com/foxcpp/go-imap-sql v0.5.1-0.20260412133145-20097edd35ec h1:Jm71K60qrrnyISeLXMYKzSZe0RVco+aO/RJugJvafIM=\ngithub.com/foxcpp/go-imap-sql v0.5.1-0.20260412133145-20097edd35ec/go.mod h1:LMlfyNkVs7v2zE6OVeGe9qWPmKFdXDmLNddPLodPVIw=\ngithub.com/foxcpp/go-imap-sql v0.5.1-0.20260412184517-b5e85e90f14d h1:oiq5MLSSqd3sl4VNHKTlrwszWTHIx8+x8y/olInMJRo=\ngithub.com/foxcpp/go-imap-sql v0.5.1-0.20260412184517-b5e85e90f14d/go.mod h1:LMlfyNkVs7v2zE6OVeGe9qWPmKFdXDmLNddPLodPVIw=\ngithub.com/foxcpp/go-mockdns v0.0.0-20191216195825-5eabd8dbfe1f/go.mod h1:tPg4cp4nseejPd+UKxtCVQ2hUxNTZ7qQZJa7CLriIeo=\ngithub.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI=\ngithub.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk=\ngithub.com/foxcpp/go-mtasts v0.0.0-20240130093538-1438da2e5932 h1:p04U/s8IZEc+PVWIDWGUgdqGq3xsixI7XRZ6Bp/xZbQ=\ngithub.com/foxcpp/go-mtasts v0.0.0-20240130093538-1438da2e5932/go.mod h1:RtHIZCsScdjIzXpTTjmEljtUrIjQbPBTvw7F1tKQbKk=\ngithub.com/foxcpp/go-smtp v1.21.4-0.20250124171104-c8519ae4fb23 h1:JSnsCrRrHNBlgfKVFBxFzp3fN/wS21t8fAHcZ9B1uWI=\ngithub.com/foxcpp/go-smtp v1.21.4-0.20250124171104-c8519ae4fb23/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=\ngithub.com/foxcpp/libdns-gandi v1.0.4-0.20240127130558-4782f9d5ce3e h1:hKk+CGUtwnKDGKINPEojeo91kx0tnV6V4tlzHehJPfg=\ngithub.com/foxcpp/libdns-gandi v1.0.4-0.20240127130558-4782f9d5ce3e/go.mod h1:G6dw58Xnji2xX+lb+uZxGbtmfxKllm1CGHE2bOPG3WA=\ngithub.com/frankban/quicktest v1.5.0/go.mod h1:jaStnuzAqU1AJdCO0l53JDCJrVDKcS03DbaAcR7Ks/o=\ngithub.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=\ngithub.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=\ngithub.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=\ngithub.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=\ngithub.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=\ngithub.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk=\ngithub.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=\ngithub.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=\ngithub.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=\ngithub.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=\ngithub.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=\ngithub.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=\ngithub.com/go-ldap/ldap/v3 v3.4.10 h1:ot/iwPOhfpNVgB1o+AVXljizWZ9JTp7YF5oeyONmcJU=\ngithub.com/go-ldap/ldap/v3 v3.4.10/go.mod h1:JXh4Uxgi40P6E9rdsYqpUtbW46D9UTjJ9QSwGRznplY=\ngithub.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=\ngithub.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=\ngithub.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=\ngithub.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=\ngithub.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM=\ngithub.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=\ngithub.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=\ngithub.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=\ngithub.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=\ngithub.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=\ngithub.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=\ngithub.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=\ngithub.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=\ngithub.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=\ngithub.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=\ngithub.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=\ngithub.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=\ngithub.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=\ngithub.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=\ngithub.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=\ngithub.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=\ngithub.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=\ngithub.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=\ngithub.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=\ngithub.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=\ngithub.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=\ngithub.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=\ngithub.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=\ngithub.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=\ngithub.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=\ngithub.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=\ngithub.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=\ngithub.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=\ngithub.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=\ngithub.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw=\ngithub.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=\ngithub.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=\ngithub.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=\ngithub.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=\ngithub.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=\ngithub.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=\ngithub.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=\ngithub.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=\ngithub.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8=\ngithub.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8=\ngithub.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg=\ngithub.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw=\ngithub.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA=\ngithub.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=\ngithub.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=\ngithub.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=\ngithub.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM=\ngithub.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM=\ngithub.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM=\ngithub.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c=\ngithub.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqEF02fYlzkUCyo=\ngithub.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY=\ngithub.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q=\ngithub.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA=\ngithub.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4=\ngithub.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=\ngithub.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=\ngithub.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=\ngithub.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=\ngithub.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=\ngithub.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=\ngithub.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=\ngithub.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=\ngithub.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=\ngithub.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=\ngithub.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=\ngithub.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=\ngithub.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=\ngithub.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=\ngithub.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=\ngithub.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=\ngithub.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=\ngithub.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=\ngithub.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=\ngithub.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=\ngithub.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=\ngithub.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=\ngithub.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=\ngithub.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=\ngithub.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=\ngithub.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=\ngithub.com/jimlambrt/gldap v0.1.14 h1:InG9kldhIu6OoQK0hvfkW1Lqpc5eLJhxiiDTNmRnrDM=\ngithub.com/jimlambrt/gldap v0.1.14/go.mod h1:yobW9JIAmqe23dVNOaMWewPaff6jGaHgYjspPIIgYmg=\ngithub.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=\ngithub.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=\ngithub.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=\ngithub.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=\ngithub.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=\ngithub.com/johannesboyne/gofakes3 v0.0.0-20210704111953-6a9f95c2941c h1:lx/uPI+mUWlqEQ9e6CtNvaK/zD64s/mQ9+yMh16PgY0=\ngithub.com/johannesboyne/gofakes3 v0.0.0-20210704111953-6a9f95c2941c/go.mod h1:LIAXxPvcUXwOcTIj9LSNSUpE9/eMHalTWxsP/kmWxQI=\ngithub.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=\ngithub.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=\ngithub.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=\ngithub.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=\ngithub.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=\ngithub.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=\ngithub.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=\ngithub.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=\ngithub.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=\ngithub.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=\ngithub.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=\ngithub.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=\ngithub.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=\ngithub.com/libdns/acmedns v0.2.0 h1:zTXdHZwe3r2issdVRyqt5/4X2yHpiBVmFnTrwBA29ik=\ngithub.com/libdns/acmedns v0.2.0/go.mod h1:XlKHilQQK/IGHYY//vCb903PdG4Wc/XnDQzcMp2hV3g=\ngithub.com/libdns/alidns v1.0.3 h1:LFHuGnbseq5+HCeGa1aW8awyX/4M2psB9962fdD2+yQ=\ngithub.com/libdns/alidns v1.0.3/go.mod h1:e18uAG6GanfRhcJj6/tps2rCMzQJaYVcGKT+ELjdjGE=\ngithub.com/libdns/cloudflare v0.1.1 h1:FVPfWwP8zZCqj268LZjmkDleXlHPlFU9KC4OJ3yn054=\ngithub.com/libdns/cloudflare v0.1.1/go.mod h1:9VK91idpOjg6v7/WbjkEW49bSCxj00ALesIFDhJ8PBU=\ngithub.com/libdns/digitalocean v0.0.0-20230728223659-4f9064657aea h1:IGlMNZCUp8Ho7NYYorpP5ZJgg2mFXARs6eHs/pSqFkA=\ngithub.com/libdns/digitalocean v0.0.0-20230728223659-4f9064657aea/go.mod h1:B2TChhOTxvBflpRTHlguXWtwa1Ha5WI6JkB6aCViM+0=\ngithub.com/libdns/gcore v0.0.0-20250127070537-4a9d185c9d20 h1:bQwFw+C9sX/zYZlV53ey0KnNkxrfWYIFpvptuAVhJ1Y=\ngithub.com/libdns/gcore v0.0.0-20250127070537-4a9d185c9d20/go.mod h1:JGoT1mbmqQwtYQqN5F/vGc9j4TTTMKw/hDm5vXADHUI=\ngithub.com/libdns/googleclouddns v1.1.0 h1:murPR1LfTZZObLV2OLxUVmymWH25glkMFKpDjkk2m0E=\ngithub.com/libdns/googleclouddns v1.1.0/go.mod h1:3tzd056dfqKlf71V8Oy19En4WjJ3ybyuWx6P9bQSCIw=\ngithub.com/libdns/hetzner v0.0.1 h1:WsmcsOKnfpKmzwhfyqhGQEIlEeEaEUvb7ezoJgBKaqU=\ngithub.com/libdns/hetzner v0.0.1/go.mod h1:Jj12aJipO9Ir7OGaXueJ5J1RnerFMD0auGa6k9kujG4=\ngithub.com/libdns/leaseweb v0.4.0 h1:WG9R5AwewpYM4goymFwnG2SB0qwL8gMsSzwRHZHee/U=\ngithub.com/libdns/leaseweb v0.4.0/go.mod h1:dvTvEn11JN6+ebhAQ60l+jiaBiEqyJFs3EIo0YBcQkU=\ngithub.com/libdns/libdns v0.1.0/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=\ngithub.com/libdns/libdns v0.2.0/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=\ngithub.com/libdns/libdns v0.2.1/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=\ngithub.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s=\ngithub.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=\ngithub.com/libdns/metaname v0.3.0 h1:HJudLYthdv52TupOPczojip/nEQHW7xqk5+whGReva4=\ngithub.com/libdns/metaname v0.3.0/go.mod h1:a3hqEgj59tjWaWlF4WxQGhvMVtjz1E4Ngs1GfVS+VhQ=\ngithub.com/libdns/namecheap v0.0.0-20211109042440-fc7440785c8e h1:WCcKyxiiK/sJnST1ulVBKNg4J8luCYDdgUrp2ySMO2s=\ngithub.com/libdns/namecheap v0.0.0-20211109042440-fc7440785c8e/go.mod h1:dED6sMLZxIcilF1GjrcpwgVoCglXGMn86irqQzRhqRY=\ngithub.com/libdns/namedotcom v0.3.3 h1:R10C7+IqQGVeC4opHHMiFNBxdNBg1bi65ZwqLESl+jE=\ngithub.com/libdns/namedotcom v0.3.3/go.mod h1:GbYzsAF2yRUpI0WgIK5fs5UX+kDVUPaYCFLpTnKQm0s=\ngithub.com/libdns/rfc2136 v0.1.1 h1:GKh2r08xt4aYeGlXR9eFrJMfFKD5i9QHBOpT1FIww/U=\ngithub.com/libdns/rfc2136 v0.1.1/go.mod h1:tgXWavE+5OiAfdKxBnuG8OBEwQFAu7uuiS3+laspAGs=\ngithub.com/libdns/route53 v1.5.1 h1:dkdcc2CKY/EHBBzAKqE0Cko7MKR8uVJ3GvpzwKu/UKM=\ngithub.com/libdns/route53 v1.5.1/go.mod h1:joT4hKmaTNKHEwb7GmZ65eoDz1whTu7KKYPS8ZqIh6Q=\ngithub.com/libdns/vultr v1.0.0 h1:W8B4+k2bm9ro3bZLSZV9hMOQI+uO6Svu+GmD+Olz7ZI=\ngithub.com/libdns/vultr v1.0.0/go.mod h1:8K1HJExcbeHS4YPkFHRZpqpXZzZ+DZAA0m0VikJgEqk=\ngithub.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM=\ngithub.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=\ngithub.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=\ngithub.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=\ngithub.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=\ngithub.com/martinlindhe/base36 v1.0.0/go.mod h1:+AtEs8xrBpCeYgSLoY/aJ6Wf37jtBuR0s35750M27+8=\ngithub.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=\ngithub.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=\ngithub.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=\ngithub.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=\ngithub.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=\ngithub.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=\ngithub.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=\ngithub.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=\ngithub.com/mholt/acmez/v3 v3.0.1 h1:4PcjKjaySlgXK857aTfDuRbmnM5gb3Ruz3tvoSJAUp8=\ngithub.com/mholt/acmez/v3 v3.0.1/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ=\ngithub.com/miekg/dns v1.1.22/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=\ngithub.com/miekg/dns v1.1.25/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=\ngithub.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk=\ngithub.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY=\ngithub.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs=\ngithub.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=\ngithub.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=\ngithub.com/minio/minio-go/v7 v7.0.84 h1:D1HVmAF8JF8Bpi6IU4V9vIEj+8pc+xU88EWMs2yed0E=\ngithub.com/minio/minio-go/v7 v7.0.84/go.mod h1:57YXpvc5l3rjPdhqNrDsvVlY0qPI6UTk1bflAe+9doY=\ngithub.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=\ngithub.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=\ngithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=\ngithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=\ngithub.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=\ngithub.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=\ngithub.com/netauth/netauth v0.6.2 h1:Gtx/Xxa6YUaGny+iVvWyp+FAmtLQ1IlbB2uWTZEpWxQ=\ngithub.com/netauth/netauth v0.6.2/go.mod h1:4PEbISVqRCQaXaDAt289w3nK9UhoF8/ZOLy31Hbv7ds=\ngithub.com/netauth/protocol v0.0.0-20210918062754-7fee492ffcbd h1:4yVpQ/+li28lQ/daYCWeDB08obRmjaoAw2qfFFaCQ40=\ngithub.com/netauth/protocol v0.0.0-20210918062754-7fee492ffcbd/go.mod h1:wpK5wqysOJU1w2OxgG65du8M7UqBkxzsNaJdjwiRqAs=\ngithub.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=\ngithub.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=\ngithub.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM=\ngithub.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=\ngithub.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=\ngithub.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=\ngithub.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=\ngithub.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=\ngithub.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=\ngithub.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=\ngithub.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=\ngithub.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=\ngithub.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=\ngithub.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=\ngithub.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=\ngithub.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=\ngithub.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=\ngithub.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=\ngithub.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=\ngithub.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=\ngithub.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=\ngithub.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 h1:GHRpF1pTW19a8tTFrMLUcfWwyC0pnifVo2ClaLq+hP8=\ngithub.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8=\ngithub.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=\ngithub.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=\ngithub.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=\ngithub.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=\ngithub.com/shabbyrobe/gocovmerge v0.0.0-20180507124511-f6ea450bfb63 h1:J6qvD6rbmOil46orKqJaRPG+zTpoGlBTUdyv8ki63L0=\ngithub.com/shabbyrobe/gocovmerge v0.0.0-20180507124511-f6ea450bfb63/go.mod h1:n+VKSARF5y/tS9XFSP7vWDfS+GUC5vs/YT7M5XDTUEM=\ngithub.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=\ngithub.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=\ngithub.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=\ngithub.com/spf13/afero v1.2.1/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=\ngithub.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=\ngithub.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4=\ngithub.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=\ngithub.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=\ngithub.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=\ngithub.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=\ngithub.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=\ngithub.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngithub.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=\ngithub.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=\ngithub.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=\ngithub.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=\ngithub.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=\ngithub.com/urfave/cli v1.22.14/go.mod h1:X0eDS6pD6Exaclxm99NJ3FiCDRED7vIHpx2mDOHLvkA=\ngithub.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=\ngithub.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=\ngithub.com/vultr/govultr/v3 v3.14.1 h1:9BpyZgsWasuNoR39YVMcq44MSaF576Z4D+U3ro58eJQ=\ngithub.com/vultr/govultr/v3 v3.14.1/go.mod h1:q34Wd76upKmf+vxFMgaNMH3A8BbsPBmSYZUGC8oZa5w=\ngithub.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=\ngithub.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=\ngithub.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=\ngithub.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngithub.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=\ngithub.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=\ngithub.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI=\ngithub.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE=\ngithub.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=\ngithub.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=\ngo.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=\ngo.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=\ngo.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=\ngo.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=\ngo.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=\ngo.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=\ngo.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I=\ngo.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=\ngo.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=\ngo.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=\ngo.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=\ngo.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4=\ngo.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU=\ngo.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=\ngo.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=\ngo.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=\ngo.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=\ngo.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=\ngo.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=\ngo.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=\ngo.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=\ngo.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=\ngo.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=\ngo.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=\ngo.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U=\ngo.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=\ngolang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=\ngolang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=\ngolang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g=\ngolang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=\ngolang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=\ngolang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=\ngolang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=\ngolang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=\ngolang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=\ngolang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=\ngolang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=\ngolang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=\ngolang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=\ngolang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=\ngolang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA=\ngolang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=\ngolang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=\ngolang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=\ngolang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=\ngolang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=\ngolang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=\ngolang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=\ngolang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=\ngolang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=\ngolang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=\ngolang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=\ngolang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=\ngolang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=\ngolang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=\ngolang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=\ngolang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=\ngolang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=\ngolang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=\ngolang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=\ngolang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190310074541-c10a0554eabf/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20201216054612-986b41b23924/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=\ngolang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=\ngolang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=\ngolang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=\ngolang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=\ngolang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=\ngolang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=\ngolang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.0.0-20220617184016-355a448f1bc9/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=\ngolang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=\ngolang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=\ngolang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=\ngolang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=\ngolang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=\ngolang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=\ngolang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=\ngolang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=\ngolang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=\ngolang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=\ngolang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=\ngolang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=\ngolang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=\ngolang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=\ngolang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=\ngolang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=\ngolang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE=\ngolang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE=\ngolang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=\ngolang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=\ngolang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=\ngolang.org/x/oauth2 v0.1.0/go.mod h1:G9FE4dLTsbXUu90h/Pf85g4w1D+SSAgR+q46nJZ8M4A=\ngolang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=\ngolang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=\ngolang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=\ngolang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=\ngolang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=\ngolang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=\ngolang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=\ngolang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=\ngolang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=\ngolang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=\ngolang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=\ngolang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=\ngolang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=\ngolang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=\ngolang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww=\ngolang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=\ngolang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=\ngolang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=\ngolang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=\ngolang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=\ngolang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=\ngolang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=\ngolang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=\ngolang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=\ngolang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=\ngolang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=\ngolang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=\ngolang.org/x/tools v0.0.0-20190308174544-00c44ba9c14f/go.mod h1:25r3+/G6/xytQM8iWZKq3Hn0kr0rgFKPUNVEL/dr3z4=\ngolang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=\ngolang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=\ngolang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=\ngolang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=\ngolang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=\ngolang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=\ngolang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=\ngolang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=\ngolang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk=\ngolang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=\ngolang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=\ngolang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=\ngolang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=\ngolang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=\ngoogle.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=\ngoogle.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=\ngoogle.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=\ngoogle.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=\ngoogle.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=\ngoogle.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=\ngoogle.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=\ngoogle.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=\ngoogle.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=\ngoogle.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=\ngoogle.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=\ngoogle.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=\ngoogle.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=\ngoogle.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo=\ngoogle.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4=\ngoogle.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw=\ngoogle.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU=\ngoogle.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k=\ngoogle.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=\ngoogle.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=\ngoogle.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI=\ngoogle.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I=\ngoogle.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo=\ngoogle.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g=\ngoogle.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA=\ngoogle.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8=\ngoogle.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs=\ngoogle.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA=\ngoogle.golang.org/api v0.77.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA=\ngoogle.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw=\ngoogle.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg=\ngoogle.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o=\ngoogle.golang.org/api v0.85.0/go.mod h1:AqZf8Ep9uZ2pyTvgL+x0D3Zt0eoT9b5E8fmzfu6FO2g=\ngoogle.golang.org/api v0.90.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw=\ngoogle.golang.org/api v0.93.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw=\ngoogle.golang.org/api v0.95.0/go.mod h1:eADj+UBuxkh5zlrSntJghuNeg8HwQ1w5lTKkuqaETEI=\ngoogle.golang.org/api v0.96.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s=\ngoogle.golang.org/api v0.97.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s=\ngoogle.golang.org/api v0.98.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s=\ngoogle.golang.org/api v0.100.0/go.mod h1:ZE3Z2+ZOr87Rx7dqFsdRQkRBk36kDtp/h+QpHbB7a70=\ngoogle.golang.org/api v0.218.0 h1:x6JCjEWeZ9PFCRe9z0FBrNwj7pB7DOAqT35N+IPnAUA=\ngoogle.golang.org/api v0.218.0/go.mod h1:5VGHBAkxrA/8EFjLVEYmMUJ8/8+gWWQ3s4cFH0FxG2M=\ngoogle.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=\ngoogle.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=\ngoogle.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=\ngoogle.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=\ngoogle.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=\ngoogle.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=\ngoogle.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=\ngoogle.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=\ngoogle.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=\ngoogle.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=\ngoogle.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=\ngoogle.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=\ngoogle.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=\ngoogle.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=\ngoogle.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=\ngoogle.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=\ngoogle.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=\ngoogle.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=\ngoogle.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=\ngoogle.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w=\ngoogle.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=\ngoogle.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=\ngoogle.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=\ngoogle.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=\ngoogle.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=\ngoogle.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=\ngoogle.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=\ngoogle.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=\ngoogle.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=\ngoogle.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E=\ngoogle.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=\ngoogle.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=\ngoogle.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=\ngoogle.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=\ngoogle.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=\ngoogle.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=\ngoogle.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=\ngoogle.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=\ngoogle.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=\ngoogle.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=\ngoogle.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=\ngoogle.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=\ngoogle.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=\ngoogle.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=\ngoogle.golang.org/genproto v0.0.0-20220722212130-b98a9ff5e252/go.mod h1:GkXuJDJ6aQ7lnJcRF+SJVgFdQhypqgl3LB1C9vabdRE=\ngoogle.golang.org/genproto v0.0.0-20220801145646-83ce21fca29f/go.mod h1:iHe1svFLAZg9VWz891+QbRMwUv9O/1Ww+/mngYeThbc=\ngoogle.golang.org/genproto v0.0.0-20220815135757-37a418bb8959/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk=\ngoogle.golang.org/genproto v0.0.0-20220817144833-d7fd3f11b9b1/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk=\ngoogle.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk=\ngoogle.golang.org/genproto v0.0.0-20220829144015-23454907ede3/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk=\ngoogle.golang.org/genproto v0.0.0-20220829175752-36a9c930ecbf/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk=\ngoogle.golang.org/genproto v0.0.0-20220913154956-18f8339a66a5/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo=\ngoogle.golang.org/genproto v0.0.0-20220914142337-ca0e39ece12f/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo=\ngoogle.golang.org/genproto v0.0.0-20220915135415-7fd63a7952de/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo=\ngoogle.golang.org/genproto v0.0.0-20220916172020-2692e8806bfa/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo=\ngoogle.golang.org/genproto v0.0.0-20220919141832-68c03719ef51/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo=\ngoogle.golang.org/genproto v0.0.0-20220920201722-2b89144ce006/go.mod h1:ht8XFiar2npT/g4vkk7O0WYS1sHOHbdujxbEp7CJWbw=\ngoogle.golang.org/genproto v0.0.0-20220926165614-551eb538f295/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI=\ngoogle.golang.org/genproto v0.0.0-20220926220553-6981cbe3cfce/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI=\ngoogle.golang.org/genproto v0.0.0-20221010155953-15ba04fc1c0e/go.mod h1:3526vdqwhZAwq4wsRUaVG555sVgsNmIjRtO7t/JH29U=\ngoogle.golang.org/genproto v0.0.0-20221014173430-6e2ab493f96b/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM=\ngoogle.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM=\ngoogle.golang.org/genproto v0.0.0-20221018160656-63c7b68cfc55/go.mod h1:45EK0dUbEZ2NHjCeAd2LXmyjAgGUGrpGROgjhC3ADck=\ngoogle.golang.org/genproto v0.0.0-20241118233622-e639e219e697 h1:ToEetK57OidYuqD4Q5w+vfEnPvPpuTwedCNVohYJfNk=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20250124145028-65684f501c47 h1:91mG8dNTpkC0uChJUQ9zCiRqx3GEEFOWaRZ0mI6Oj2I=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20250124145028-65684f501c47/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50=\ngoogle.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=\ngoogle.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=\ngoogle.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=\ngoogle.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=\ngoogle.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=\ngoogle.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=\ngoogle.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=\ngoogle.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=\ngoogle.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=\ngoogle.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=\ngoogle.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=\ngoogle.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=\ngoogle.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=\ngoogle.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=\ngoogle.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=\ngoogle.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=\ngoogle.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=\ngoogle.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=\ngoogle.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=\ngoogle.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=\ngoogle.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=\ngoogle.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=\ngoogle.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=\ngoogle.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=\ngoogle.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=\ngoogle.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=\ngoogle.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=\ngoogle.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=\ngoogle.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=\ngoogle.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=\ngoogle.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=\ngoogle.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=\ngoogle.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=\ngoogle.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=\ngoogle.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=\ngoogle.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=\ngoogle.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=\ngoogle.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=\ngoogle.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=\ngoogle.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=\ngoogle.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=\ngoogle.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=\ngoogle.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=\ngoogle.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=\ngoogle.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=\ngoogle.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=\ngopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=\ngopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=\ngopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=\ngopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=\ngotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=\nhonnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=\nhonnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=\nhonnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=\nmodernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0=\nmodernc.org/cc/v4 v4.24.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=\nmodernc.org/ccgo/v4 v4.23.13 h1:PFiaemQwE/jdwi8XEHyEV+qYWoIuikLP3T4rvDeJb00=\nmodernc.org/ccgo/v4 v4.23.13/go.mod h1:vdN4h2WR5aEoNondUx26K7G8X+nuBscYnAEWSRmN2/0=\nmodernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=\nmodernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=\nmodernc.org/gc/v2 v2.6.1 h1:+Qf6xdG8l7B27TQ8D8lw/iFMUj1RXRBOuMUWziJOsk8=\nmodernc.org/gc/v2 v2.6.1/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=\nmodernc.org/libc v1.61.9 h1:PLSBXVkifXGELtJ5BOnBUyAHr7lsatNwFU/RRo4kfJM=\nmodernc.org/libc v1.61.9/go.mod h1:61xrnzk/aR8gr5bR7Uj/lLFLuXu2/zMpIjcry63Eumk=\nmodernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=\nmodernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=\nmodernc.org/memory v1.8.2 h1:cL9L4bcoAObu4NkxOlKWBWtNHIsnnACGF/TbqQ6sbcI=\nmodernc.org/memory v1.8.2/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU=\nmodernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=\nmodernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=\nmodernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=\nmodernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=\nmodernc.org/sqlite v1.34.5 h1:Bb6SR13/fjp15jt70CL4f18JIN7p7dnMExd+UFnF15g=\nmodernc.org/sqlite v1.34.5/go.mod h1:YLuNmX9NKs8wRNK2ko1LW1NGYcc9FkBO69JOt1AR9JE=\nmodernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=\nmodernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=\nmodernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=\nmodernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=\nrsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=\nrsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=\nrsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=\n"
  },
  {
    "path": "internal/README.md",
    "content": "maddy source tree\n------------------\n\nMain maddy code base lives here. No packages are intended to be used in\nthird-party software hence API is not stable.\n\nSubdirectories are organized as follows:\n```\n/\n  auxiliary libraries\nendpoint/\n  modules - protocol listeners (e.g. SMTP server, etc)\ntarget/\n  modules - final delivery targets (including outbound delivery, such as\n  target.smtp, remote)\nauth/\n  modules - authentication providers\ncheck/\n  modules - message checkers (module.Check)\nmodify/\n  modules - message modifiers (module.Modifier)\nstorage/\n  modules - local messages storage implementations (module.Storage)\n```\n"
  },
  {
    "path": "internal/auth/auth.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage auth\n\nimport \"strings\"\n\nfunc CheckDomainAuth(username string, perDomain bool, allowedDomains []string) (loginName string, allowed bool) {\n\tvar accountName, domain string\n\tif perDomain {\n\t\tparts := strings.Split(username, \"@\")\n\t\tif len(parts) != 2 {\n\t\t\treturn \"\", false\n\t\t}\n\t\tdomain = parts[1]\n\t\taccountName = username\n\t} else {\n\t\tparts := strings.Split(username, \"@\")\n\t\taccountName = parts[0]\n\t\tif len(parts) == 2 {\n\t\t\tdomain = parts[1]\n\t\t}\n\t}\n\n\tallowed = domain == \"\"\n\tif allowedDomains != nil && domain != \"\" {\n\t\tfor _, allowedDomain := range allowedDomains {\n\t\t\tif strings.EqualFold(domain, allowedDomain) {\n\t\t\t\tallowed = true\n\t\t\t}\n\t\t}\n\t\tif !allowed {\n\t\t\treturn \"\", false\n\t\t}\n\t}\n\n\treturn accountName, allowed\n}\n"
  },
  {
    "path": "internal/auth/auth_test.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage auth\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n)\n\nfunc TestCheckDomainAuth(t *testing.T) {\n\tcases := []struct {\n\t\trawUsername string\n\n\t\tperDomain      bool\n\t\tallowedDomains []string\n\n\t\tloginName string\n\t}{\n\t\t{\n\t\t\trawUsername: \"username\",\n\t\t\tloginName:   \"username\",\n\t\t},\n\t\t{\n\t\t\trawUsername:    \"username\",\n\t\t\tallowedDomains: []string{\"example.org\"},\n\t\t\tloginName:      \"username\",\n\t\t},\n\t\t{\n\t\t\trawUsername:    \"username@example.org\",\n\t\t\tallowedDomains: []string{\"example.org\"},\n\t\t\tloginName:      \"username\",\n\t\t},\n\t\t{\n\t\t\trawUsername:    \"username@example.com\",\n\t\t\tallowedDomains: []string{\"example.org\"},\n\t\t},\n\t\t{\n\t\t\trawUsername:    \"username\",\n\t\t\tallowedDomains: []string{\"example.org\"},\n\t\t\tperDomain:      true,\n\t\t},\n\t\t{\n\t\t\trawUsername:    \"username@example.com\",\n\t\t\tallowedDomains: []string{\"example.org\"},\n\t\t\tperDomain:      true,\n\t\t},\n\t\t{\n\t\t\trawUsername:    \"username@EXAMPLE.Org\",\n\t\t\tallowedDomains: []string{\"exaMPle.org\"},\n\t\t\tperDomain:      true,\n\t\t\tloginName:      \"username@EXAMPLE.Org\",\n\t\t},\n\t\t{\n\t\t\trawUsername:    \"username@example.org\",\n\t\t\tallowedDomains: []string{\"example.org\"},\n\t\t\tperDomain:      true,\n\t\t\tloginName:      \"username@example.org\",\n\t\t},\n\t}\n\n\tfor _, case_ := range cases {\n\t\tt.Run(fmt.Sprintf(\"%+v\", case_), func(t *testing.T) {\n\t\t\tloginName, allowed := CheckDomainAuth(case_.rawUsername, case_.perDomain, case_.allowedDomains)\n\t\t\tif case_.loginName != \"\" && !allowed {\n\t\t\t\tt.Fatalf(\"Unexpected authentication fail\")\n\t\t\t}\n\t\t\tif case_.loginName == \"\" && allowed {\n\t\t\t\tt.Fatalf(\"Expected authentication fail, got %s as login name\", loginName)\n\t\t\t}\n\n\t\t\tif loginName != case_.loginName {\n\t\t\t\tt.Errorf(\"Incorrect login name, got %s, wanted %s\", loginName, case_.loginName)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/auth/dovecot_sasl/dovecot_sasl.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage dovecotsasl\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\n\t\"github.com/emersion/go-sasl\"\n\tdovecotsasl \"github.com/foxcpp/go-dovecot-sasl\"\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/exterrors\"\n\t\"github.com/foxcpp/maddy/framework/log\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n\t\"github.com/foxcpp/maddy/internal/auth\"\n)\n\ntype Auth struct {\n\tinstName       string\n\tserverEndpoint string\n\tlog            *log.Logger\n\n\tnetwork string\n\taddr    string\n\n\tmechanisms map[string]dovecotsasl.Mechanism\n}\n\nconst modName = \"dovecot_sasl\"\n\nfunc New(c *container.C, _, instName string) (module.Module, error) {\n\ta := &Auth{\n\t\tinstName: instName,\n\t\tlog:      c.DefaultLogger.Sublogger(modName),\n\t}\n\n\treturn a, nil\n}\n\nfunc (a *Auth) Name() string {\n\treturn modName\n}\n\nfunc (a *Auth) InstanceName() string {\n\treturn a.instName\n}\n\nfunc (a *Auth) getConn() (*dovecotsasl.Client, error) {\n\t// TODO: Connection pooling\n\tconn, err := net.Dial(a.network, a.addr)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"%s: unable to contact server: %v\", modName, err)\n\t}\n\n\tcl, err := dovecotsasl.NewClient(conn)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"%s: unable to contact server: %v\", modName, err)\n\t}\n\n\treturn cl, nil\n}\n\nfunc (a *Auth) returnConn(cl *dovecotsasl.Client) {\n\tif err := cl.Close(); err != nil {\n\t\ta.log.Error(\"connection close failed\", err)\n\t}\n}\n\nfunc (a *Auth) Configure(inlineArgs []string, cfg *config.Map) error {\n\tswitch len(inlineArgs) {\n\tcase 0:\n\tcase 1:\n\t\ta.serverEndpoint = inlineArgs[0]\n\tdefault:\n\t\treturn fmt.Errorf(\"%s: one or none arguments needed\", modName)\n\t}\n\n\tcfg.String(\"endpoint\", false, false, a.serverEndpoint, &a.serverEndpoint)\n\tif _, err := cfg.Process(); err != nil {\n\t\treturn err\n\t}\n\tif a.serverEndpoint == \"\" {\n\t\treturn fmt.Errorf(\"%s: missing server endpoint\", modName)\n\t}\n\n\tendp, err := config.ParseEndpoint(a.serverEndpoint)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"%s: invalid server endpoint: %v\", modName, err)\n\t}\n\n\t// Dial once to check usability and also to get list of mechanisms.\n\tconn, err := net.Dial(endp.Scheme, endp.Address())\n\tif err != nil {\n\t\treturn fmt.Errorf(\"%s: unable to contact server: %v\", modName, err)\n\t}\n\n\tcl, err := dovecotsasl.NewClient(conn)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"%s: unable to contact server: %v\", modName, err)\n\t}\n\n\tdefer func() {\n\t\tif err := cl.Close(); err != nil {\n\t\t\ta.log.Error(\"connection close failed\", err)\n\t\t}\n\t}()\n\ta.mechanisms = make(map[string]dovecotsasl.Mechanism, len(cl.ConnInfo().Mechs))\n\tfor name, mech := range cl.ConnInfo().Mechs {\n\t\tif mech.Private {\n\t\t\tcontinue\n\t\t}\n\t\ta.mechanisms[name] = mech\n\t}\n\n\ta.network = endp.Scheme\n\ta.addr = endp.Address()\n\n\treturn nil\n}\n\nfunc (a *Auth) AuthPlain(username, password string) error {\n\tif _, ok := a.mechanisms[sasl.Plain]; ok {\n\t\tcl, err := a.getConn()\n\t\tif err != nil {\n\t\t\treturn exterrors.WithTemporary(err, true)\n\t\t}\n\t\tdefer a.returnConn(cl)\n\n\t\t// Pretend it is SMTPS even though we really don't know.\n\t\t// We also have no connection information to pass to the server...\n\t\treturn cl.Do(\"SMTP\", sasl.NewPlainClient(\"\", username, password),\n\t\t\tdovecotsasl.Secured, dovecotsasl.NoPenalty)\n\t}\n\tif _, ok := a.mechanisms[sasl.Login]; ok {\n\t\tcl, err := a.getConn()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdefer a.returnConn(cl)\n\n\t\treturn cl.Do(\"SMTP\", sasl.NewLoginClient(username, password),\n\t\t\tdovecotsasl.Secured, dovecotsasl.NoPenalty)\n\t}\n\n\treturn auth.ErrUnsupportedMech\n}\n\nfunc init() {\n\tmodules.Register(modName, New)\n}\n"
  },
  {
    "path": "internal/auth/external/externalauth.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage external\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/log\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n\t\"github.com/foxcpp/maddy/internal/auth\"\n)\n\ntype ExternalAuth struct {\n\tmodName    string\n\tinstName   string\n\thelperPath string\n\n\tperDomain bool\n\tdomains   []string\n\n\tlog *log.Logger\n}\n\nfunc New(c *container.C, modName, instName string) (module.Module, error) {\n\tea := &ExternalAuth{\n\t\tmodName:  modName,\n\t\tinstName: instName,\n\t\tlog:      c.DefaultLogger.Sublogger(modName),\n\t}\n\n\treturn ea, nil\n}\n\nfunc (ea *ExternalAuth) Name() string {\n\treturn ea.modName\n}\n\nfunc (ea *ExternalAuth) InstanceName() string {\n\treturn ea.instName\n}\n\nfunc (ea *ExternalAuth) Configure(inlineArgs []string, cfg *config.Map) error {\n\tif len(inlineArgs) != 0 {\n\t\treturn errors.New(\"external: inline arguments are not used\")\n\t}\n\n\tcfg.Bool(\"debug\", false, false, &ea.log.Debug)\n\tcfg.Bool(\"perdomain\", false, false, &ea.perDomain)\n\tcfg.StringList(\"domains\", false, false, nil, &ea.domains)\n\tcfg.String(\"helper\", false, false, \"\", &ea.helperPath)\n\tif _, err := cfg.Process(); err != nil {\n\t\treturn err\n\t}\n\tif ea.perDomain && ea.domains == nil {\n\t\treturn errors.New(\"auth_domains must be set if auth_perdomain is used\")\n\t}\n\n\tif ea.helperPath != \"\" {\n\t\tea.log.Debugln(\"using helper:\", ea.helperPath)\n\t} else {\n\t\tea.helperPath = filepath.Join(config.LibexecDirectory, \"maddy-auth-helper\")\n\t}\n\tif _, err := os.Stat(ea.helperPath); err != nil {\n\t\treturn fmt.Errorf(\"%s doesn't exist\", ea.helperPath)\n\t}\n\n\tea.log.Debugln(\"using helper:\", ea.helperPath)\n\n\treturn nil\n}\n\nfunc (ea *ExternalAuth) AuthPlain(username, password string) error {\n\taccountName, ok := auth.CheckDomainAuth(username, ea.perDomain, ea.domains)\n\tif !ok {\n\t\treturn module.ErrUnknownCredentials\n\t}\n\n\treturn AuthUsingHelper(ea.helperPath, accountName, password)\n}\n\nfunc init() {\n\tmodules.Register(\"auth.external\", New)\n}\n"
  },
  {
    "path": "internal/auth/external/helperauth.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage external\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"os/exec\"\n\n\t\"github.com/foxcpp/maddy/framework/module\"\n)\n\nfunc AuthUsingHelper(binaryPath, accountName, password string) error {\n\tcmd := exec.Command(binaryPath)\n\tstdin, err := cmd.StdinPipe()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"helperauth: stdin init: %w\", err)\n\t}\n\tif err := cmd.Start(); err != nil {\n\t\treturn fmt.Errorf(\"helperauth: process start: %w\", err)\n\t}\n\tif _, err := io.WriteString(stdin, accountName+\"\\n\"); err != nil {\n\t\treturn fmt.Errorf(\"helperauth: stdin write: %w\", err)\n\t}\n\tif _, err := io.WriteString(stdin, password+\"\\n\"); err != nil {\n\t\treturn fmt.Errorf(\"helperauth: stdin write: %w\", err)\n\t}\n\tif err := cmd.Wait(); err != nil {\n\t\tif exitErr, ok := err.(*exec.ExitError); ok {\n\t\t\t// Exit code 1 is for authentication failure.\n\t\t\tif exitErr.ExitCode() != 1 {\n\t\t\t\treturn fmt.Errorf(\"helperauth: %w: %v\", err, string(exitErr.Stderr))\n\t\t\t}\n\t\t\treturn module.ErrUnknownCredentials\n\t\t}\n\t\treturn fmt.Errorf(\"helperauth: process wait: %w\", err)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/auth/ldap/ldap.go",
    "content": "package ldap\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/url\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/foxcpp/maddy/framework/config\"\n\ttls2 \"github.com/foxcpp/maddy/framework/config/tls\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/log\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n\t\"github.com/go-ldap/ldap/v3\"\n)\n\nconst modName = \"auth.ldap\"\n\ntype Auth struct {\n\tinstName string\n\n\turls           []string\n\treadBind       func(*ldap.Conn) error\n\tstartls        bool\n\ttlsCfg         *tls.Config\n\tdialer         *net.Dialer\n\trequestTimeout time.Duration\n\n\tdnTemplate string\n\t// or\n\tbaseDN         string\n\tfilterTemplate string\n\n\tconn     *ldap.Conn\n\tconnLock sync.Mutex\n\n\tlog *log.Logger\n}\n\nfunc New(c *container.C, modName, instName string) (module.Module, error) {\n\treturn &Auth{\n\t\tinstName: instName,\n\t\tlog:      c.DefaultLogger.Sublogger(modName),\n\t}, nil\n}\n\nfunc (a *Auth) Configure(inlineArgs []string, cfg *config.Map) error {\n\ta.urls = inlineArgs\n\n\ta.dialer = &net.Dialer{}\n\n\tcfg.Bool(\"debug\", true, false, &a.log.Debug)\n\tcfg.Custom(\"tls_client\", true, false, func() (interface{}, error) {\n\t\treturn &tls.Config{}, nil\n\t}, tls2.TLSClientBlock, &a.tlsCfg)\n\tcfg.Callback(\"urls\", func(m *config.Map, node config.Node) error {\n\t\ta.urls = append(a.urls, node.Args...)\n\t\treturn nil\n\t})\n\tcfg.Custom(\"bind\", false, false, func() (interface{}, error) {\n\t\treturn func(*ldap.Conn) error {\n\t\t\treturn nil\n\t\t}, nil\n\t}, readBindDirective, &a.readBind)\n\tcfg.Bool(\"starttls\", false, false, &a.startls)\n\tcfg.Duration(\"connect_timeout\", false, false, time.Minute, &a.dialer.Timeout)\n\tcfg.Duration(\"request_timeout\", false, false, time.Minute, &a.requestTimeout)\n\tcfg.String(\"dn_template\", false, false, \"\", &a.dnTemplate)\n\tcfg.String(\"base_dn\", false, false, \"\", &a.baseDN)\n\tcfg.String(\"filter\", false, false, \"\", &a.filterTemplate)\n\tif _, err := cfg.Process(); err != nil {\n\t\treturn err\n\t}\n\n\tif a.dnTemplate == \"\" {\n\t\tif a.baseDN == \"\" {\n\t\t\treturn fmt.Errorf(\"auth.ldap: base_dn not set\")\n\t\t}\n\t\tif a.filterTemplate == \"\" {\n\t\t\treturn fmt.Errorf(\"auth.ldap: filter not set\")\n\t\t}\n\t} else {\n\t\tif a.baseDN != \"\" || a.filterTemplate != \"\" {\n\t\t\treturn fmt.Errorf(\"auth.ldap: search directives set when dn_template is used\")\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc readBindDirective(c *config.Map, n config.Node) (interface{}, error) {\n\tif len(n.Args) == 0 {\n\t\treturn nil, fmt.Errorf(\"auth.ldap: auth expects at least one argument\")\n\t}\n\tswitch n.Args[0] {\n\tcase \"off\":\n\t\treturn func(*ldap.Conn) error { return nil }, nil\n\tcase \"unauth\":\n\t\tif len(n.Args) == 2 {\n\t\t\treturn func(c *ldap.Conn) error {\n\t\t\t\treturn c.UnauthenticatedBind(n.Args[1])\n\t\t\t}, nil\n\t\t}\n\t\treturn func(c *ldap.Conn) error {\n\t\t\treturn c.UnauthenticatedBind(\"\")\n\t\t}, nil\n\tcase \"plain\":\n\t\tif len(n.Args) != 3 {\n\t\t\treturn nil, fmt.Errorf(\"auth.ldap: username and password expected for plaintext bind\")\n\t\t}\n\t\treturn func(c *ldap.Conn) error {\n\t\t\treturn c.Bind(n.Args[1], n.Args[2])\n\t\t}, nil\n\tcase \"external\":\n\t\treturn (*ldap.Conn).ExternalBind, nil\n\t}\n\treturn nil, fmt.Errorf(\"auth.ldap: unknown bind authentication: %v\", n.Args[0])\n}\n\nfunc (a *Auth) Name() string {\n\treturn modName\n}\n\nfunc (a *Auth) InstanceName() string {\n\treturn a.instName\n}\n\nfunc (a *Auth) newConn() (*ldap.Conn, error) {\n\tvar (\n\t\tconn   *ldap.Conn\n\t\ttlsCfg *tls.Config\n\t)\n\tfor _, u := range a.urls {\n\t\tparsedURL, err := url.Parse(u)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"auth.ldap: invalid server URL: %w\", err)\n\t\t}\n\t\thostname := parsedURL.Host\n\t\ta.tlsCfg.ServerName = strings.Split(hostname, \":\")[0]\n\t\ttlsCfg = a.tlsCfg.Clone()\n\n\t\tconn, err = ldap.DialURL(u, ldap.DialWithDialer(a.dialer), ldap.DialWithTLSConfig(tlsCfg))\n\t\tif err != nil {\n\t\t\ta.log.Error(\"cannot contact directory server\", err, \"url\", u)\n\t\t\tcontinue\n\t\t}\n\t\tbreak\n\t}\n\tif conn == nil {\n\t\treturn nil, fmt.Errorf(\"auth.ldap: all directory servers are unreachable\")\n\t}\n\n\tif a.requestTimeout != 0 {\n\t\tconn.SetTimeout(a.requestTimeout)\n\t}\n\n\tif a.startls {\n\t\tif err := conn.StartTLS(tlsCfg); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"auth.ldap: %w\", err)\n\t\t}\n\t}\n\n\tif err := a.readBind(conn); err != nil {\n\t\treturn nil, fmt.Errorf(\"auth.ldap: %w\", err)\n\t}\n\n\treturn conn, nil\n}\n\nfunc (a *Auth) getConn() (*ldap.Conn, error) {\n\ta.connLock.Lock()\n\tif a.conn == nil {\n\t\tconn, err := a.newConn()\n\t\tif err != nil {\n\t\t\ta.connLock.Unlock()\n\t\t\treturn nil, err\n\t\t}\n\t\ta.conn = conn\n\t}\n\tif a.conn.IsClosing() {\n\t\tif err := a.conn.Close(); err != nil {\n\t\t\ta.log.Error(\"Connection close failed\", err)\n\t\t}\n\t\tconn, err := a.newConn()\n\t\tif err != nil {\n\t\t\ta.connLock.Unlock()\n\t\t\treturn nil, err\n\t\t}\n\t\ta.conn = conn\n\t}\n\treturn a.conn, nil\n}\n\nfunc (a *Auth) returnConn(conn *ldap.Conn) {\n\tdefer a.connLock.Unlock()\n\tif err := a.readBind(conn); err != nil {\n\t\ta.log.Error(\"failed to rebind for reading\", err)\n\t\tif err := a.conn.Close(); err != nil {\n\t\t\ta.log.Error(\"Connection close failed\", err)\n\t\t}\n\t\ta.conn = nil\n\t}\n\tif a.conn != conn {\n\t\tif err := a.conn.Close(); err != nil {\n\t\t\ta.log.Error(\"Connection close failed\", err)\n\t\t}\n\t}\n\ta.conn = conn\n}\n\nfunc (a *Auth) Lookup(_ context.Context, username string) (string, bool, error) {\n\tconn, err := a.getConn()\n\tif err != nil {\n\t\treturn \"\", false, err\n\t}\n\tdefer a.returnConn(conn)\n\n\tvar userDN string\n\tif a.dnTemplate != \"\" {\n\t\treturn \"\", false, fmt.Errorf(\"auth.ldap: lookups require search config but dn_template is used\")\n\t} else {\n\t\treq := ldap.NewSearchRequest(\n\t\t\ta.baseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases,\n\t\t\t2, 0, false,\n\t\t\tstrings.ReplaceAll(a.filterTemplate, \"{username}\", ldap.EscapeFilter(username)),\n\t\t\t[]string{\"dn\"}, nil)\n\t\tres, err := conn.Search(req)\n\t\tif err != nil {\n\t\t\treturn \"\", false, fmt.Errorf(\"auth.ldap: search: %w\", err)\n\t\t}\n\t\tif len(res.Entries) > 1 {\n\t\t\treturn \"\", false, fmt.Errorf(\"auth.ldap: too manu entries returned (%d)\", len(res.Entries))\n\t\t}\n\t\tif len(res.Entries) == 0 {\n\t\t\treturn \"\", false, nil\n\t\t}\n\t\tuserDN = res.Entries[0].DN\n\t}\n\n\treturn userDN, true, nil\n}\n\nfunc (a *Auth) AuthPlain(username, password string) error {\n\tconn, err := a.getConn()\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer a.returnConn(conn)\n\n\tvar userDN string\n\tif a.dnTemplate != \"\" {\n\t\tuserDN = strings.ReplaceAll(a.dnTemplate, \"{username}\", ldap.EscapeDN(username))\n\t} else {\n\t\treq := ldap.NewSearchRequest(\n\t\t\ta.baseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases,\n\t\t\t2, 0, false,\n\t\t\tstrings.ReplaceAll(a.filterTemplate, \"{username}\", ldap.EscapeFilter(username)),\n\t\t\t[]string{\"dn\"}, nil)\n\t\tres, err := conn.Search(req)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"auth.ldap: search: %w\", err)\n\t\t}\n\t\tif len(res.Entries) > 1 {\n\t\t\treturn fmt.Errorf(\"auth.ldap: too manu entries returned (%d)\", len(res.Entries))\n\t\t}\n\t\tif len(res.Entries) == 0 {\n\t\t\treturn module.ErrUnknownCredentials\n\t\t}\n\t\tuserDN = res.Entries[0].DN\n\t}\n\n\tif err := conn.Bind(userDN, password); err != nil {\n\t\treturn module.ErrUnknownCredentials\n\t}\n\n\treturn nil\n}\n\nfunc (a *Auth) Start() error {\n\tvar err error\n\ta.conn, err = a.newConn()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"auth.ldap: %w\", err)\n\t}\n\treturn nil\n}\n\nfunc (a *Auth) Stop() error {\n\ta.connLock.Lock()\n\tdefer a.connLock.Unlock()\n\treturn a.conn.Close()\n}\n\nfunc init() {\n\tvar _ module.PlainAuth = &Auth{}\n\tvar _ module.Table = &Auth{}\n\tmodules.Register(modName, New)\n\tmodules.Register(\"table.ldap\", New)\n}\n"
  },
  {
    "path": "internal/auth/netauth/netauth.go",
    "content": "package netauth\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/log\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n\t\"github.com/hashicorp/go-hclog\"\n\t\"github.com/netauth/netauth/pkg/netauth\"\n)\n\nconst modName = \"auth.netauth\"\n\nfunc init() {\n\tvar _ module.PlainAuth = &Auth{}\n\tvar _ module.Table = &Auth{}\n\tmodules.Register(modName, New)\n\tmodules.Register(\"table.netauth\", New)\n}\n\n// Auth binds all methods related to the NetAuth client library.\ntype Auth struct {\n\tinstName  string\n\tmustGroup string\n\n\tnacl *netauth.Client\n\n\tlog *log.Logger\n}\n\n// New creates a new instance of the NetAuth module.\nfunc New(c *container.C, modName, instName string) (module.Module, error) {\n\treturn &Auth{\n\t\tinstName: instName,\n\t\tlog:      c.DefaultLogger.Sublogger(modName),\n\t}, nil\n}\n\nfunc (a *Auth) Configure(inlineArgs []string, cfg *config.Map) error {\n\tif len(inlineArgs) > 0 {\n\t\treturn fmt.Errorf(\"%s: inline arguments are not used\", modName)\n\t}\n\n\tl := hclog.New(&hclog.LoggerOptions{Output: a.log})\n\tn, err := netauth.NewWithLog(l)\n\tif err != nil {\n\t\treturn err\n\t}\n\ta.nacl = n\n\ta.nacl.SetServiceName(\"maddy\")\n\tcfg.String(\"require_group\", false, false, \"\", &a.mustGroup)\n\tcfg.Bool(\"debug\", true, false, &a.log.Debug)\n\tif _, err := cfg.Process(); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// Name returns \"auth.netauth\" as the fixed module name.\nfunc (a *Auth) Name() string {\n\treturn modName\n}\n\n// InstanceName returns the configured name for this instance of the\n// plugin.  Given the way that NetAuth works it doesn't really make\n// sense to have more than one instance, but this is part of the API.\nfunc (a *Auth) InstanceName() string {\n\treturn a.instName\n}\n\n// Lookup requests the entity from the remote NetAuth server,\n// potentially returning that the user does not exist at all.\nfunc (a *Auth) Lookup(ctx context.Context, username string) (string, bool, error) {\n\te, err := a.nacl.EntityInfo(ctx, username)\n\tif err != nil {\n\t\treturn \"\", false, fmt.Errorf(\"%s: search: %w\", modName, err)\n\t}\n\n\tif a.mustGroup != \"\" {\n\t\tif err := a.checkMustGroup(username); err != nil {\n\t\t\treturn \"\", false, err\n\t\t}\n\t}\n\treturn e.GetID(), true, nil\n}\n\n// AuthPlain attempts straightforward authentication of the entity on\n// the remote NetAuth server.\nfunc (a *Auth) AuthPlain(username, password string) error {\n\ta.log.Debugf(\"attempting to auth user: %s\", username)\n\tif err := a.nacl.AuthEntity(context.Background(), username, password); err != nil {\n\t\treturn module.ErrUnknownCredentials\n\t}\n\ta.log.Debugln(\"netauth returns successful auth\")\n\tif a.mustGroup != \"\" {\n\t\tif err := a.checkMustGroup(username); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (a *Auth) checkMustGroup(username string) error {\n\ta.log.Debugf(\"Performing require_group check: must=%s\", a.mustGroup)\n\tgroups, err := a.nacl.EntityGroups(context.Background(), username)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"%s: groups: %w\", modName, err)\n\t}\n\tfor _, g := range groups {\n\t\tif g.GetName() == a.mustGroup {\n\t\t\treturn nil\n\t\t}\n\t}\n\treturn fmt.Errorf(\"%s: missing required group (%s not in %s)\", modName, username, a.mustGroup)\n}\n"
  },
  {
    "path": "internal/auth/pam/module.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage pam\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/log\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n\t\"github.com/foxcpp/maddy/internal/auth/external\"\n)\n\ntype Auth struct {\n\tinstName   string\n\tuseHelper  bool\n\thelperPath string\n\n\tLog *log.Logger\n}\n\nfunc New(c *container.C, modName, instName string) (module.Module, error) {\n\treturn &Auth{\n\t\tinstName: instName,\n\t\tLog:      c.DefaultLogger.Sublogger(modName),\n\t}, nil\n}\n\nfunc (a *Auth) Name() string {\n\treturn \"pam\"\n}\n\nfunc (a *Auth) InstanceName() string {\n\treturn a.instName\n}\n\nfunc (a *Auth) Configure(inlineArgs []string, cfg *config.Map) error {\n\tif len(inlineArgs) != 0 {\n\t\treturn errors.New(\"pam: inline arguments are not used\")\n\t}\n\n\tcfg.Bool(\"debug\", true, false, &a.Log.Debug)\n\tcfg.Bool(\"use_helper\", false, false, &a.useHelper)\n\tif _, err := cfg.Process(); err != nil {\n\t\treturn err\n\t}\n\tif !canCallDirectly && !a.useHelper {\n\t\treturn errors.New(\"pam: this build lacks support for direct libpam invocation, use helper binary\")\n\t}\n\n\tif a.useHelper {\n\t\ta.helperPath = filepath.Join(config.LibexecDirectory, \"maddy-pam-helper\")\n\t\tif _, err := os.Stat(a.helperPath); err != nil {\n\t\t\treturn fmt.Errorf(\"pam: no helper binary (maddy-pam-helper) found in %s\", config.LibexecDirectory)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (a *Auth) AuthPlain(username, password string) error {\n\tif a.useHelper {\n\t\tif err := external.AuthUsingHelper(a.helperPath, username, password); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\terr := runPAMAuth(username, password)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc init() {\n\tmodules.Register(\"auth.pam\", New)\n}\n"
  },
  {
    "path": "internal/auth/pam/pam.c",
    "content": "//+build libpam\n\n/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2022 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\n#define _POSIX_C_SOURCE 200809L\n#include <stdio.h>\n#include <stdlib.h>\n#include <string.h>\n#include <security/pam_appl.h>\n#include \"pam.h\"\n\nstatic int conv_func(int num_msg, const struct pam_message **msg, struct pam_response **resp, void *appdata_ptr) {\n    struct pam_response *reply = malloc(sizeof(struct pam_response));\n    if (reply == NULL) {\n        return PAM_CONV_ERR;\n    }\n\n    char* password_cpy = malloc(strlen((char*)appdata_ptr)+1);\n    if (password_cpy == NULL) {\n        return PAM_CONV_ERR;\n    }\n    memcpy(password_cpy, (char*)appdata_ptr, strlen((char*)appdata_ptr)+1);\n\n    reply->resp = password_cpy;\n    reply->resp_retcode = 0;\n\n    // PAM frees pam_response for us.\n    *resp = reply;\n\n    return PAM_SUCCESS;\n}\n\nstruct error_obj run_pam_auth(const char *username, char *password) {\n    const struct pam_conv local_conv = { conv_func, password };\n    pam_handle_t *local_auth = NULL;\n    int status = pam_start(\"maddy\", username, &local_conv, &local_auth);\n    if (status != PAM_SUCCESS) {\n        struct error_obj ret_val;\n        ret_val.status = 2;\n        ret_val.func_name = \"pam_start\";\n        ret_val.error_msg = pam_strerror(local_auth, status);\n        return ret_val;\n    }\n\n    status = pam_authenticate(local_auth, PAM_SILENT|PAM_DISALLOW_NULL_AUTHTOK);\n    if (status != PAM_SUCCESS) {\n        struct error_obj ret_val;\n        if (status == PAM_AUTH_ERR || status == PAM_USER_UNKNOWN) {\n            ret_val.status = 1;\n        } else {\n            ret_val.status = 2;\n        }\n        ret_val.func_name = \"pam_authenticate\";\n        ret_val.error_msg = pam_strerror(local_auth, status);\n        return ret_val;\n    }\n\n    status = pam_acct_mgmt(local_auth, PAM_SILENT|PAM_DISALLOW_NULL_AUTHTOK);\n    if (status != PAM_SUCCESS) {\n        struct error_obj ret_val;\n        if (status == PAM_AUTH_ERR || status == PAM_USER_UNKNOWN || status == PAM_NEW_AUTHTOK_REQD) {\n            ret_val.status = 1;\n        } else {\n            ret_val.status = 2;\n        }\n        ret_val.func_name = \"pam_acct_mgmt\";\n        ret_val.error_msg = pam_strerror(local_auth, status);\n        return ret_val;\n    }\n\n    status = pam_end(local_auth, status);\n    if (status != PAM_SUCCESS) {\n        struct error_obj ret_val;\n        ret_val.status = 2;\n        ret_val.func_name = \"pam_end\";\n        ret_val.error_msg = pam_strerror(local_auth, status);\n        return ret_val;\n    }\n\n    struct error_obj ret_val;\n    ret_val.status = 0;\n    ret_val.func_name = NULL;\n    ret_val.error_msg = NULL;\n    return ret_val;\n}\n\n"
  },
  {
    "path": "internal/auth/pam/pam.go",
    "content": "//go:build cgo && libpam\n// +build cgo,libpam\n\n/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage pam\n\n/*\n#cgo LDFLAGS: -lpam\n#cgo CFLAGS: -DCGO -Wall -Wextra -Werror -Wno-unused-parameter -Wno-error=unused-parameter -Wpedantic -std=c99\n\n#include <stdlib.h>\n#include \"pam.h\"\n*/\nimport \"C\"\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"unsafe\"\n)\n\nconst canCallDirectly = true\n\nvar ErrInvalidCredentials = errors.New(\"pam: invalid credentials or unknown user\")\n\nfunc runPAMAuth(username, password string) error {\n\tusernameC := C.CString(username)\n\tpasswordC := C.CString(password)\n\tdefer C.free(unsafe.Pointer(usernameC))\n\tdefer C.free(unsafe.Pointer(passwordC))\n\terrObj := C.run_pam_auth(usernameC, passwordC)\n\tif errObj.status == 1 {\n\t\treturn ErrInvalidCredentials\n\t}\n\tif errObj.status == 2 {\n\t\treturn fmt.Errorf(\"%s: %s\", C.GoString(errObj.func_name), C.GoString(errObj.error_msg))\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/auth/pam/pam.h",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\n#pragma once\n\nstruct error_obj {\n    int status;\n    const char* func_name;\n    const char* error_msg;\n};\n\nstruct error_obj run_pam_auth(const char *username, char *password);\n"
  },
  {
    "path": "internal/auth/pam/pam_stub.go",
    "content": "//go:build !cgo || !libpam\n// +build !cgo !libpam\n\n/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage pam\n\nimport (\n\t\"errors\"\n)\n\nconst canCallDirectly = false\n\nvar ErrInvalidCredentials = errors.New(\"pam: invalid credentials or unknown user\")\n\nfunc runPAMAuth(username, password string) error {\n\treturn errors.New(\"pam: Can't call libpam directly\")\n}\n"
  },
  {
    "path": "internal/auth/pass_table/hash.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage pass_table\n\nimport (\n\t\"crypto/rand\"\n\t\"crypto/sha256\"\n\t\"crypto/subtle\"\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"io\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"golang.org/x/crypto/argon2\"\n\t\"golang.org/x/crypto/bcrypt\"\n)\n\nconst (\n\tHashSHA256 = \"sha256\"\n\tHashBcrypt = \"bcrypt\"\n\tHashArgon2 = \"argon2\"\n\n\tDefaultHash = HashBcrypt\n\n\tArgon2Salt = 16\n\tArgon2Size = 64\n)\n\ntype (\n\t// HashOpts is the structure that holds additional parameters for used hash\n\t// functions. They are used for new passwords.\n\t//\n\t// These parameters should be stored together with the hashed password\n\t// so it can be verified independently of the used HashOpts.\n\tHashOpts struct {\n\t\t// Bcrypt cost value to use. Should be at least 10.\n\t\tBcryptCost int\n\n\t\tArgon2Time    uint32\n\t\tArgon2Memory  uint32\n\t\tArgon2Threads uint8\n\t}\n\n\tFuncHashCompute func(opts HashOpts, pass string) (string, error)\n\tFuncHashVerify  func(pass, hashSalt string) error\n)\n\nvar (\n\tHashCompute = map[string]FuncHashCompute{\n\t\tHashBcrypt: computeBcrypt,\n\t\tHashArgon2: computeArgon2,\n\t}\n\tHashVerify = map[string]FuncHashVerify{\n\t\tHashBcrypt: verifyBcrypt,\n\t\tHashArgon2: verifyArgon2,\n\t}\n\n\tHashes = []string{HashSHA256, HashBcrypt, HashArgon2}\n)\n\nfunc computeArgon2(opts HashOpts, pass string) (string, error) {\n\tsalt := make([]byte, Argon2Salt)\n\tif _, err := io.ReadFull(rand.Reader, salt); err != nil {\n\t\treturn \"\", fmt.Errorf(\"pass_table: failed to generate salt: %w\", err)\n\t}\n\n\thash := argon2.IDKey([]byte(pass), salt, opts.Argon2Time, opts.Argon2Memory, opts.Argon2Threads, Argon2Size)\n\tvar out strings.Builder\n\tout.WriteString(strconv.FormatUint(uint64(opts.Argon2Time), 10))\n\tout.WriteRune(':')\n\tout.WriteString(strconv.FormatUint(uint64(opts.Argon2Memory), 10))\n\tout.WriteRune(':')\n\tout.WriteString(strconv.FormatUint(uint64(opts.Argon2Threads), 10))\n\tout.WriteRune(':')\n\tout.WriteString(base64.StdEncoding.EncodeToString(salt))\n\tout.WriteRune(':')\n\tout.WriteString(base64.StdEncoding.EncodeToString(hash))\n\treturn out.String(), nil\n}\n\nfunc verifyArgon2(pass, hashSalt string) error {\n\tparts := strings.SplitN(hashSalt, \":\", 5)\n\n\ttime, err := strconv.ParseUint(parts[0], 10, 32)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"pass_table: malformed hash string: %w\", err)\n\t}\n\tmemory, err := strconv.ParseUint(parts[1], 10, 32)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"pass_table: malformed hash string: %w\", err)\n\t}\n\tthreads, err := strconv.ParseUint(parts[2], 10, 8)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"pass_table: malformed hash string: %w\", err)\n\t}\n\tsalt, err := base64.StdEncoding.DecodeString(parts[3])\n\tif err != nil {\n\t\treturn fmt.Errorf(\"pass_table: malformed hash string: %w\", err)\n\t}\n\thash, err := base64.StdEncoding.DecodeString(parts[4])\n\tif err != nil {\n\t\treturn fmt.Errorf(\"pass_table: malformed hash string: %w\", err)\n\t}\n\n\tpassHash := argon2.IDKey([]byte(pass), salt, uint32(time), uint32(memory), uint8(threads), Argon2Size)\n\tif subtle.ConstantTimeCompare(passHash, hash) != 1 {\n\t\treturn fmt.Errorf(\"pass_table: hash mismatch\")\n\t}\n\treturn nil\n}\n\nfunc computeSHA256(_ HashOpts, pass string) (string, error) {\n\tsalt := make([]byte, 32)\n\tif _, err := io.ReadFull(rand.Reader, salt); err != nil {\n\t\treturn \"\", fmt.Errorf(\"pass_table: failed to generate salt: %w\", err)\n\t}\n\n\thashInput := salt\n\thashInput = append(hashInput, []byte(pass)...)\n\tsum := sha256.Sum256(hashInput)\n\treturn base64.StdEncoding.EncodeToString(salt) + \":\" + base64.StdEncoding.EncodeToString(sum[:]), nil\n}\n\nfunc verifySHA256(pass, hashSalt string) error {\n\tparts := strings.Split(hashSalt, \":\")\n\tif len(parts) != 2 {\n\t\treturn fmt.Errorf(\"pass_table: malformed hash string, no salt\")\n\t}\n\tsalt, err := base64.StdEncoding.DecodeString(parts[0])\n\tif err != nil {\n\t\treturn fmt.Errorf(\"pass_table: malformed hash string, cannot decode pass: %w\", err)\n\t}\n\thash, err := base64.StdEncoding.DecodeString(parts[1])\n\tif err != nil {\n\t\treturn fmt.Errorf(\"pass_table: malformed hash string, cannot decode pass: %w\", err)\n\t}\n\n\thashInput := salt\n\thashInput = append(hashInput, []byte(pass)...)\n\tsum := sha256.Sum256(hashInput)\n\n\tif subtle.ConstantTimeCompare(sum[:], hash) != 1 {\n\t\treturn fmt.Errorf(\"pass_table: hash mismatch\")\n\t}\n\treturn nil\n}\n\nfunc computeBcrypt(opts HashOpts, pass string) (string, error) {\n\thash, err := bcrypt.GenerateFromPassword([]byte(pass), opts.BcryptCost)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn string(hash), nil\n}\n\nfunc verifyBcrypt(pass, hashSalt string) error {\n\treturn bcrypt.CompareHashAndPassword([]byte(hashSalt), []byte(pass))\n}\n\nfunc addSHA256() {\n\tHashCompute[HashSHA256] = computeSHA256\n\tHashVerify[HashSHA256] = verifySHA256\n}\n"
  },
  {
    "path": "internal/auth/pass_table/table.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage pass_table\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/foxcpp/maddy/framework/config\"\n\tmodconfig \"github.com/foxcpp/maddy/framework/config/module\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n\t\"golang.org/x/crypto/bcrypt\"\n\t\"golang.org/x/text/secure/precis\"\n)\n\ntype Auth struct {\n\tmodName  string\n\tinstName string\n\n\ttable module.Table\n}\n\nfunc New(_ *container.C, modName, instName string) (module.Module, error) {\n\treturn &Auth{\n\t\tmodName:  modName,\n\t\tinstName: instName,\n\t}, nil\n}\n\nfunc (a *Auth) Configure(inlineArgs []string, cfg *config.Map) error {\n\tif len(inlineArgs) != 0 {\n\t\treturn modconfig.ModuleFromNode(\"table\", inlineArgs, cfg.Block, cfg.Globals, &a.table)\n\t}\n\n\tcfg.Custom(\"table\", false, true, nil, modconfig.TableDirective, &a.table)\n\t_, err := cfg.Process()\n\treturn err\n}\n\nfunc (a *Auth) Name() string {\n\treturn a.modName\n}\n\nfunc (a *Auth) InstanceName() string {\n\treturn a.instName\n}\n\nfunc (a *Auth) Lookup(ctx context.Context, username string) (string, bool, error) {\n\tkey, err := precis.UsernameCaseMapped.CompareKey(username)\n\tif err != nil {\n\t\treturn \"\", false, err\n\t}\n\n\treturn a.table.Lookup(ctx, key)\n}\n\nfunc (a *Auth) AuthPlain(username, password string) error {\n\tkey, err := precis.UsernameCaseMapped.CompareKey(username)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\thash, ok, err := a.table.Lookup(context.TODO(), key)\n\tif !ok {\n\t\treturn module.ErrUnknownCredentials\n\t}\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tparts := strings.SplitN(hash, \":\", 2)\n\tif len(parts) != 2 {\n\t\treturn fmt.Errorf(\"%s: auth plain %s: no hash tag\", a.modName, key)\n\t}\n\thashVerify := HashVerify[parts[0]]\n\tif hashVerify == nil {\n\t\treturn fmt.Errorf(\"%s: auth plain %s: unknown hash: %s\", a.modName, key, parts[0])\n\t}\n\treturn hashVerify(password, parts[1])\n}\n\nfunc (a *Auth) ListUsers() ([]string, error) {\n\ttbl, ok := a.table.(module.MutableTable)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"%s: table is not mutable, no management functionality available\", a.modName)\n\t}\n\n\tl, err := tbl.Keys()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"%s: list users: %w\", a.modName, err)\n\t}\n\treturn l, nil\n}\n\nfunc (a *Auth) CreateUser(username, password string) error {\n\treturn a.CreateUserHash(username, password, HashBcrypt, HashOpts{\n\t\tBcryptCost: bcrypt.DefaultCost,\n\t})\n}\n\nfunc (a *Auth) CreateUserHash(username, password string, hashAlgo string, opts HashOpts) error {\n\ttbl, ok := a.table.(module.MutableTable)\n\tif !ok {\n\t\treturn fmt.Errorf(\"%s: table is not mutable, no management functionality available\", a.modName)\n\t}\n\n\tif _, ok := HashCompute[hashAlgo]; !ok {\n\t\treturn fmt.Errorf(\"%s: unknown hash function: %v\", a.modName, hashAlgo)\n\t}\n\n\tkey, err := precis.UsernameCaseMapped.CompareKey(username)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"%s: create user %s (raw): %w\", a.modName, username, err)\n\t}\n\n\t_, ok, err = tbl.Lookup(context.TODO(), key)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"%s: create user %s: %w\", a.modName, key, err)\n\t}\n\tif ok {\n\t\treturn fmt.Errorf(\"%s: credentials for %s already exist\", a.modName, key)\n\t}\n\n\thash, err := HashCompute[hashAlgo](opts, password)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"%s: create user %s: hash generation: %w\", a.modName, key, err)\n\t}\n\n\tif err := tbl.SetKey(key, hashAlgo+\":\"+hash); err != nil {\n\t\treturn fmt.Errorf(\"%s: create user %s: %w\", a.modName, key, err)\n\t}\n\treturn nil\n}\n\nfunc (a *Auth) SetUserPassword(username, password string) error {\n\ttbl, ok := a.table.(module.MutableTable)\n\tif !ok {\n\t\treturn fmt.Errorf(\"%s: table is not mutable, no management functionality available\", a.modName)\n\t}\n\n\tkey, err := precis.UsernameCaseMapped.CompareKey(username)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"%s: set password %s (raw): %w\", a.modName, username, err)\n\t}\n\n\t// TODO: Allow to customize hash function.\n\thash, err := HashCompute[HashBcrypt](HashOpts{\n\t\tBcryptCost: bcrypt.DefaultCost,\n\t}, password)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"%s: set password %s: hash generation: %w\", a.modName, key, err)\n\t}\n\n\tif err := tbl.SetKey(key, \"bcrypt:\"+hash); err != nil {\n\t\treturn fmt.Errorf(\"%s: set password %s: %w\", a.modName, key, err)\n\t}\n\treturn nil\n}\n\nfunc (a *Auth) DeleteUser(username string) error {\n\ttbl, ok := a.table.(module.MutableTable)\n\tif !ok {\n\t\treturn fmt.Errorf(\"%s: table is not mutable, no management functionality available\", a.modName)\n\t}\n\n\tkey, err := precis.UsernameCaseMapped.CompareKey(username)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"%s: del user %s (raw): %w\", a.modName, username, err)\n\t}\n\n\tif err := tbl.RemoveKey(key); err != nil {\n\t\treturn fmt.Errorf(\"%s: del user %s: %w\", a.modName, key, err)\n\t}\n\treturn nil\n}\n\nfunc init() {\n\tmodules.Register(\"auth.pass_table\", New)\n}\n"
  },
  {
    "path": "internal/auth/pass_table/table_test.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage pass_table\n\nimport (\n\t\"testing\"\n\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/internal/testutils\"\n)\n\nfunc TestAuth_AuthPlain(t *testing.T) {\n\taddSHA256()\n\n\tmod, err := New(container.New(), \"pass_table\", \"\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\terr = mod.Configure([]string{\"dummy\"}, config.NewMap(nil, config.Node{\n\t\tChildren: []config.Node{},\n\t}))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\ta := mod.(*Auth)\n\ta.table = testutils.Table{\n\t\tM: map[string]string{\n\t\t\t\"foxcpp\":       \"sha256:U0FMVA==:8PDRAgaUqaLSk34WpYniXjaBgGM93Lc6iF4pw2slthw=\",\n\t\t\t\"not-foxcpp\":   \"bcrypt:$2y$10$4tEJtJ6dApmhETg8tJ4WHOeMtmYXQwmHDKIyfg09Bw1F/smhLjlaa\",\n\t\t\t\"not-foxcpp-2\": \"argon2:1:8:1:U0FBQUFBTFQ=:KHUshl3DcpHR3AoVd28ZeBGmZ1Fj1gwJgNn98Ia8DAvGHqI0BvFOMJPxtaAfO8F+qomm2O3h0P0yV50QGwXI/Q==\",\n\t\t},\n\t}\n\n\tcheck := func(user, pass string, ok bool) {\n\t\tt.Helper()\n\n\t\terr := a.AuthPlain(user, pass)\n\t\tif (err == nil) != ok {\n\t\t\tt.Errorf(\"ok=%v, err: %v\", ok, err)\n\t\t}\n\t}\n\n\tcheck(\"foxcpp\", \"password\", true)\n\tcheck(\"foxcpp\", \"different-password\", false)\n\tcheck(\"not-foxcpp\", \"password\", true)\n\tcheck(\"not-foxcpp\", \"different-password\", false)\n\tcheck(\"not-foxcpp-2\", \"password\", true)\n}\n"
  },
  {
    "path": "internal/auth/plain_separate/plain_separate.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage plain_separate\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\n\t\"github.com/foxcpp/maddy/framework/config\"\n\tmodconfig \"github.com/foxcpp/maddy/framework/config/module\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/log\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n)\n\ntype Auth struct {\n\tmodName  string\n\tinstName string\n\n\tuserTbls []module.Table\n\tpasswd   []module.PlainAuth\n\n\tonlyFirstID bool\n\n\tlog *log.Logger\n}\n\nfunc New(c *container.C, modName, instName string) (module.Module, error) {\n\ta := &Auth{\n\t\tmodName:     modName,\n\t\tinstName:    instName,\n\t\tonlyFirstID: false,\n\t\tlog:         c.DefaultLogger.Sublogger(modName),\n\t}\n\n\treturn a, nil\n}\n\nfunc (a *Auth) Name() string {\n\treturn a.modName\n}\n\nfunc (a *Auth) InstanceName() string {\n\treturn a.instName\n}\n\nfunc (a *Auth) Configure(inlineArgs []string, cfg *config.Map) error {\n\tif len(inlineArgs) != 0 {\n\t\treturn errors.New(\"plain_separate: inline arguments are not used\")\n\t}\n\n\tcfg.Bool(\"debug\", false, false, &a.log.Debug)\n\tcfg.Callback(\"user\", func(m *config.Map, node config.Node) error {\n\t\tvar tbl module.Table\n\t\terr := modconfig.ModuleFromNode(\"table\", node.Args, node, m.Globals, &tbl)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\ta.userTbls = append(a.userTbls, tbl)\n\t\treturn nil\n\t})\n\tcfg.Callback(\"pass\", func(m *config.Map, node config.Node) error {\n\t\tvar auth module.PlainAuth\n\t\terr := modconfig.ModuleFromNode(\"auth\", node.Args, node, m.Globals, &auth)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\ta.passwd = append(a.passwd, auth)\n\t\treturn nil\n\t})\n\n\tif _, err := cfg.Process(); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (a *Auth) Lookup(ctx context.Context, username string) (string, bool, error) {\n\tok := len(a.userTbls) == 0\n\tfor _, tbl := range a.userTbls {\n\t\t_, tblOk, err := tbl.Lookup(ctx, username)\n\t\tif err != nil {\n\t\t\treturn \"\", false, fmt.Errorf(\"plain_separate: underlying table error: %w\", err)\n\t\t}\n\t\tif tblOk {\n\t\t\tok = true\n\t\t\tbreak\n\t\t}\n\t}\n\tif !ok {\n\t\treturn \"\", false, nil\n\t}\n\treturn \"\", true, nil\n}\n\nfunc (a *Auth) AuthPlain(username, password string) error {\n\tok := len(a.userTbls) == 0\n\tfor _, tbl := range a.userTbls {\n\t\t_, tblOk, err := tbl.Lookup(context.TODO(), username)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif tblOk {\n\t\t\tok = true\n\t\t\tbreak\n\t\t}\n\t}\n\tif !ok {\n\t\treturn errors.New(\"user not found in tables\")\n\t}\n\n\tvar lastErr error\n\tfor _, p := range a.passwd {\n\t\tif err := p.AuthPlain(username, password); err != nil {\n\t\t\tlastErr = err\n\t\t\tcontinue\n\t\t}\n\n\t\treturn nil\n\t}\n\treturn lastErr\n}\n\nfunc init() {\n\tmodules.Register(\"auth.plain_separate\", New)\n}\n"
  },
  {
    "path": "internal/auth/plain_separate/plain_separate_test.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage plain_separate\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"testing\"\n\n\t\"github.com/emersion/go-sasl\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n)\n\ntype mockAuth struct {\n\tdb map[string]bool\n}\n\nfunc (mockAuth) SASLMechanisms() []string {\n\treturn []string{sasl.Plain, sasl.Login}\n}\n\nfunc (m mockAuth) AuthPlain(username, _ string) error {\n\tok := m.db[username]\n\tif !ok {\n\t\treturn errors.New(\"invalid creds\")\n\t}\n\treturn nil\n}\n\ntype mockTable struct {\n\tdb map[string]string\n}\n\nfunc (m mockTable) Lookup(_ context.Context, a string) (string, bool, error) {\n\tb, ok := m.db[a]\n\treturn b, ok, nil\n}\n\nfunc TestPlainSplit_NoUser(t *testing.T) {\n\ta := Auth{\n\t\tpasswd: []module.PlainAuth{\n\t\t\tmockAuth{\n\t\t\t\tdb: map[string]bool{\n\t\t\t\t\t\"user1\": true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\terr := a.AuthPlain(\"user1\", \"aaa\")\n\tif err != nil {\n\t\tt.Fatal(\"Unexpected error:\", err)\n\t}\n}\n\nfunc TestPlainSplit_NoUser_MultiPass(t *testing.T) {\n\ta := Auth{\n\t\tpasswd: []module.PlainAuth{\n\t\t\tmockAuth{\n\t\t\t\tdb: map[string]bool{\n\t\t\t\t\t\"user2\": true,\n\t\t\t\t},\n\t\t\t},\n\t\t\tmockAuth{\n\t\t\t\tdb: map[string]bool{\n\t\t\t\t\t\"user1\": true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\terr := a.AuthPlain(\"user1\", \"aaa\")\n\tif err != nil {\n\t\tt.Fatal(\"Unexpected error:\", err)\n\t}\n}\n\nfunc TestPlainSplit_UserPass(t *testing.T) {\n\ta := Auth{\n\t\tuserTbls: []module.Table{\n\t\t\tmockTable{\n\t\t\t\tdb: map[string]string{\n\t\t\t\t\t\"user1\": \"\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tpasswd: []module.PlainAuth{\n\t\t\tmockAuth{\n\t\t\t\tdb: map[string]bool{\n\t\t\t\t\t\"user2\": true,\n\t\t\t\t},\n\t\t\t},\n\t\t\tmockAuth{\n\t\t\t\tdb: map[string]bool{\n\t\t\t\t\t\"user1\": true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\terr := a.AuthPlain(\"user1\", \"aaa\")\n\tif err != nil {\n\t\tt.Fatal(\"Unexpected error:\", err)\n\t}\n}\n\nfunc TestPlainSplit_MultiUser_Pass(t *testing.T) {\n\ta := Auth{\n\t\tuserTbls: []module.Table{\n\t\t\tmockTable{\n\t\t\t\tdb: map[string]string{\n\t\t\t\t\t\"userWH\": \"\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tmockTable{\n\t\t\t\tdb: map[string]string{\n\t\t\t\t\t\"user1\": \"\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tpasswd: []module.PlainAuth{\n\t\t\tmockAuth{\n\t\t\t\tdb: map[string]bool{\n\t\t\t\t\t\"user2\": true,\n\t\t\t\t},\n\t\t\t},\n\t\t\tmockAuth{\n\t\t\t\tdb: map[string]bool{\n\t\t\t\t\t\"user1\": true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\terr := a.AuthPlain(\"user1\", \"aaa\")\n\tif err != nil {\n\t\tt.Fatal(\"Unexpected error:\", err)\n\t}\n}\n"
  },
  {
    "path": "internal/auth/sasl.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage auth\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\n\t\"github.com/emersion/go-sasl\"\n\t\"github.com/foxcpp/maddy/framework/config\"\n\tmodconfig \"github.com/foxcpp/maddy/framework/config/module\"\n\t\"github.com/foxcpp/maddy/framework/log\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/internal/auth/sasllogin\"\n\t\"github.com/foxcpp/maddy/internal/authz\"\n)\n\nvar (\n\tErrUnsupportedMech = errors.New(\"unsupported SASL mechanism\")\n\tErrInvalidAuthCred = errors.New(\"auth: invalid credentials\")\n)\n\n// SASLAuth is a wrapper that initializes sasl.Server using authenticators that\n// call maddy module objects.\n//\n// It also handles username translation using auth_map and auth_map_normalize\n// (AuthMap and AuthMapNormalize should be set).\n//\n// It supports reporting of multiple authorization identities so multiple\n// accounts can be associated with a single set of credentials.\ntype SASLAuth struct {\n\tLog         *log.Logger\n\tOnlyFirstID bool\n\tEnableLogin bool\n\n\tAuthMap       module.Table\n\tAuthNormalize authz.NormalizeFunc\n\n\tErrorMap func(err error) error\n\n\tPlain []module.PlainAuth\n}\n\nfunc (s *SASLAuth) SASLMechanisms() []string {\n\tvar mechs []string\n\n\tif len(s.Plain) != 0 {\n\t\tmechs = append(mechs, sasl.Plain)\n\t\tif s.EnableLogin {\n\t\t\tmechs = append(mechs, sasl.Login)\n\t\t}\n\t}\n\n\treturn mechs\n}\n\nfunc (s *SASLAuth) usernameForAuth(ctx context.Context, saslUsername string) (string, error) {\n\tif s.AuthNormalize != nil {\n\t\tvar err error\n\t\tsaslUsername, err = s.AuthNormalize(saslUsername)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\n\tif s.AuthMap == nil {\n\t\treturn saslUsername, nil\n\t}\n\n\tmapped, ok, err := s.AuthMap.Lookup(ctx, saslUsername)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif !ok {\n\t\treturn \"\", ErrInvalidAuthCred\n\t}\n\n\tif saslUsername != mapped {\n\t\ts.Log.DebugMsg(\"using mapped username for authentication\", \"username\", saslUsername, \"mapped_username\", mapped)\n\t}\n\n\treturn mapped, nil\n}\n\nfunc (s *SASLAuth) AuthPlain(username, password string) error {\n\tif len(s.Plain) == 0 {\n\t\treturn ErrUnsupportedMech\n\t}\n\n\tvar lastErr error\n\tfor _, p := range s.Plain {\n\t\tmappedUsername, err := s.usernameForAuth(context.TODO(), username)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\ts.Log.DebugMsg(\"attempting authentication\",\n\t\t\t\"mapped_username\", mappedUsername, \"original_username\", username,\n\t\t\t\"module\", p)\n\n\t\tlastErr = p.AuthPlain(mappedUsername, password)\n\t\tif lastErr == nil {\n\t\t\treturn nil\n\t\t}\n\t}\n\n\treturn fmt.Errorf(\"no auth. provider accepted creds, last err: %w\", lastErr)\n}\n\ntype ContextData struct {\n\t// Authentication username. May be different from identity.\n\tUsername string\n\n\t// Password used for password-based mechanisms.\n\tPassword string\n}\n\n// CreateSASL creates the sasl.Server instance for the corresponding mechanism.\nfunc (s *SASLAuth) CreateSASL(\n\tmech string, remoteAddr net.Addr,\n\tsuccessCb func(identity string, data ContextData) error,\n) sasl.Server {\n\tswitch mech {\n\tcase sasl.Plain:\n\t\treturn sasl.NewPlainServer(func(identity, username, password string) error {\n\t\t\tif identity == \"\" {\n\t\t\t\tidentity = username\n\t\t\t}\n\t\t\tif identity != username {\n\t\t\t\tif s.ErrorMap != nil {\n\t\t\t\t\treturn s.ErrorMap(ErrInvalidAuthCred)\n\t\t\t\t}\n\t\t\t\treturn ErrInvalidAuthCred\n\t\t\t}\n\n\t\t\terr := s.AuthPlain(username, password)\n\t\t\tif err != nil {\n\t\t\t\ts.Log.Error(\"authentication failed\", err, \"username\", username, \"src_ip\", remoteAddr)\n\t\t\t\tif s.ErrorMap != nil {\n\t\t\t\t\treturn s.ErrorMap(ErrInvalidAuthCred)\n\t\t\t\t}\n\t\t\t\treturn ErrInvalidAuthCred\n\t\t\t}\n\n\t\t\treturn successCb(identity, ContextData{\n\t\t\t\tUsername: username,\n\t\t\t\tPassword: password,\n\t\t\t})\n\t\t})\n\tcase sasl.Login:\n\t\tif !s.EnableLogin {\n\t\t\treturn FailingSASLServ{Err: ErrUnsupportedMech}\n\t\t}\n\n\t\treturn sasllogin.NewLoginServer(func(username, password string) error {\n\t\t\tusername, err := s.usernameForAuth(context.Background(), username)\n\t\t\tif err != nil {\n\t\t\t\tif s.ErrorMap != nil {\n\t\t\t\t\treturn s.ErrorMap(ErrInvalidAuthCred)\n\t\t\t\t}\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\terr = s.AuthPlain(username, password)\n\t\t\tif err != nil {\n\t\t\t\ts.Log.Error(\"authentication failed\", err, \"username\", username, \"src_ip\", remoteAddr)\n\t\t\t\tif s.ErrorMap != nil {\n\t\t\t\t\treturn s.ErrorMap(ErrInvalidAuthCred)\n\t\t\t\t}\n\t\t\t\treturn ErrInvalidAuthCred\n\t\t\t}\n\n\t\t\treturn successCb(username, ContextData{\n\t\t\t\tUsername: username,\n\t\t\t\tPassword: password,\n\t\t\t})\n\t\t})\n\t}\n\treturn FailingSASLServ{Err: ErrUnsupportedMech}\n}\n\n// AddProvider adds the SASL authentication provider to its mapping by parsing\n// the 'auth' configuration directive.\nfunc (s *SASLAuth) AddProvider(m *config.Map, node config.Node) error {\n\tvar any interface{}\n\tif err := modconfig.ModuleFromNode(\"auth\", node.Args, node, m.Globals, &any); err != nil {\n\t\treturn err\n\t}\n\n\thasAny := false\n\tif plainAuth, ok := any.(module.PlainAuth); ok {\n\t\ts.Plain = append(s.Plain, plainAuth)\n\t\thasAny = true\n\t}\n\n\tif !hasAny {\n\t\treturn config.NodeErr(node, \"auth: specified module does not provide any SASL mechanism\")\n\t}\n\treturn nil\n}\n\ntype FailingSASLServ struct{ Err error }\n\nfunc (s FailingSASLServ) Next([]byte) ([]byte, bool, error) {\n\treturn nil, true, s.Err\n}\n"
  },
  {
    "path": "internal/auth/sasl_test.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage auth\n\nimport (\n\t\"errors\"\n\t\"net\"\n\t\"testing\"\n\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/internal/testutils\"\n)\n\ntype mockAuth struct {\n\tdb map[string]bool\n}\n\nfunc (m mockAuth) AuthPlain(username, _ string) error {\n\tok := m.db[username]\n\tif !ok {\n\t\treturn errors.New(\"invalid creds\")\n\t}\n\treturn nil\n}\n\nfunc TestCreateSASL(t *testing.T) {\n\ta := SASLAuth{\n\t\tLog: testutils.Logger(t, \"saslauth\"),\n\t\tPlain: []module.PlainAuth{\n\t\t\t&mockAuth{\n\t\t\t\tdb: map[string]bool{\n\t\t\t\t\t\"user1\": true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tt.Run(\"XWHATEVER\", func(t *testing.T) {\n\t\tsrv := a.CreateSASL(\"XWHATEVER\", &net.TCPAddr{}, func(string, ContextData) error { return nil })\n\t\t_, _, err := srv.Next([]byte(\"\"))\n\t\tif err == nil {\n\t\t\tt.Error(\"No error for XWHATEVER use\")\n\t\t}\n\t})\n\n\tt.Run(\"PLAIN\", func(t *testing.T) {\n\t\tsrv := a.CreateSASL(\"PLAIN\", &net.TCPAddr{}, func(id string, data ContextData) error {\n\t\t\tif id != \"user1\" {\n\t\t\t\tt.Fatal(\"Wrong auth. identities passed to callback:\", id)\n\t\t\t}\n\t\t\treturn nil\n\t\t})\n\n\t\t_, _, err := srv.Next([]byte(\"\\x00user1\\x00aa\"))\n\t\tif err != nil {\n\t\t\tt.Error(\"Unexpected error:\", err)\n\t\t}\n\t})\n\n\tt.Run(\"PLAIN with authorization identity\", func(t *testing.T) {\n\t\tsrv := a.CreateSASL(\"PLAIN\", &net.TCPAddr{}, func(id string, data ContextData) error {\n\t\t\tif id != \"user1\" {\n\t\t\t\tt.Fatal(\"Wrong authorization identity passed:\", id)\n\t\t\t}\n\t\t\treturn nil\n\t\t})\n\n\t\t_, _, err := srv.Next([]byte(\"user1\\x00user1\\x00aa\"))\n\t\tif err != nil {\n\t\t\tt.Error(\"Unexpected error:\", err)\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "internal/auth/sasllogin/sasllogin.go",
    "content": "package sasllogin\n\nimport \"github.com/emersion/go-sasl\"\n\n// Copy-pasted from old emersion/go-sasl version\n\n// Authenticates users with an username and a password.\ntype LoginAuthenticator func(username, password string) error\ntype loginState int\n\nconst (\n\tloginNotStarted loginState = iota\n\tloginWaitingUsername\n\tloginWaitingPassword\n)\n\ntype loginServer struct {\n\tstate              loginState\n\tusername, password string\n\tauthenticate       LoginAuthenticator\n}\n\n// A server implementation of the LOGIN authentication mechanism, as described\n// in https://tools.ietf.org/html/draft-murchison-sasl-login-00.\n//\n// LOGIN is obsolete and should only be enabled for legacy clients that cannot\n// be updated to use PLAIN.\nfunc NewLoginServer(authenticator LoginAuthenticator) sasl.Server {\n\treturn &loginServer{authenticate: authenticator}\n}\n\nfunc (a *loginServer) Next(response []byte) (challenge []byte, done bool, err error) {\n\tswitch a.state {\n\tcase loginNotStarted:\n\t\t// Check for initial response field, as per RFC4422 section 3\n\t\tif response == nil {\n\t\t\tchallenge = []byte(\"Username:\")\n\t\t\tbreak\n\t\t}\n\t\ta.state++\n\t\tfallthrough\n\tcase loginWaitingUsername:\n\t\ta.username = string(response)\n\t\tchallenge = []byte(\"Password:\")\n\tcase loginWaitingPassword:\n\t\ta.password = string(response)\n\t\terr = a.authenticate(a.username, a.password)\n\t\tdone = true\n\tdefault:\n\t\terr = sasl.ErrUnexpectedClientResponse\n\t}\n\ta.state++\n\treturn\n}\n"
  },
  {
    "path": "internal/auth/shadow/module.go",
    "content": "//go:build !windows\n// +build !windows\n\n/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage shadow\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/log\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n\t\"github.com/foxcpp/maddy/internal/auth/external\"\n)\n\ntype Auth struct {\n\tinstName   string\n\tuseHelper  bool\n\thelperPath string\n\n\tlog *log.Logger\n}\n\nfunc New(c *container.C, modName, instName string) (module.Module, error) {\n\treturn &Auth{\n\t\tinstName: instName,\n\t\tlog:      c.DefaultLogger.Sublogger(modName),\n\t}, nil\n}\n\nfunc (a *Auth) Name() string {\n\treturn \"shadow\"\n}\n\nfunc (a *Auth) InstanceName() string {\n\treturn a.instName\n}\n\nfunc (a *Auth) Configure(inlineArgs []string, cfg *config.Map) error {\n\tif len(inlineArgs) != 0 {\n\t\treturn errors.New(\"shadow: inline arguments are not used\")\n\t}\n\n\tcfg.Bool(\"debug\", true, false, &a.log.Debug)\n\tcfg.Bool(\"use_helper\", false, false, &a.useHelper)\n\tif _, err := cfg.Process(); err != nil {\n\t\treturn err\n\t}\n\n\tif a.useHelper {\n\t\ta.helperPath = filepath.Join(config.LibexecDirectory, \"maddy-shadow-helper\")\n\t\tif _, err := os.Stat(a.helperPath); err != nil {\n\t\t\treturn fmt.Errorf(\"shadow: no helper binary (maddy-shadow-helper) found in %s\", config.LibexecDirectory)\n\t\t}\n\t} else {\n\t\tf, err := os.Open(\"/etc/shadow\")\n\t\tif err != nil {\n\t\t\tif os.IsPermission(err) {\n\t\t\t\treturn fmt.Errorf(\"shadow: can't read /etc/shadow due to permission error, use helper binary or run maddy as a privileged user\")\n\t\t\t}\n\t\t\treturn fmt.Errorf(\"shadow: can't read /etc/shadow: %v\", err)\n\t\t}\n\t\tif err := f.Close(); err != nil {\n\t\t\ta.log.Error(\"can't close /etc/shadow file\", err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (a *Auth) Lookup(username string) (string, bool, error) {\n\tif a.useHelper {\n\t\treturn \"\", false, fmt.Errorf(\"shadow: table lookup are not possible when using a helper\")\n\t}\n\n\tent, err := Lookup(username)\n\tif err != nil {\n\t\tif errors.Is(err, ErrNoSuchUser) {\n\t\t\treturn \"\", false, nil\n\t\t}\n\t\treturn \"\", false, err\n\t}\n\n\tif !ent.IsAccountValid() {\n\t\treturn \"\", false, nil\n\t}\n\n\treturn \"\", true, nil\n}\n\nfunc (a *Auth) AuthPlain(username, password string) error {\n\tif a.useHelper {\n\t\treturn external.AuthUsingHelper(a.helperPath, username, password)\n\t}\n\n\tent, err := Lookup(username)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !ent.IsAccountValid() {\n\t\treturn fmt.Errorf(\"shadow: account is expired\")\n\t}\n\n\tif !ent.IsPasswordValid() {\n\t\treturn fmt.Errorf(\"shadow: password is expired\")\n\t}\n\n\tif err := ent.VerifyPassword(password); err != nil {\n\t\tif errors.Is(err, ErrWrongPassword) {\n\t\t\treturn module.ErrUnknownCredentials\n\t\t}\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc init() {\n\tmodules.Register(\"auth.shadow\", New)\n}\n"
  },
  {
    "path": "internal/auth/shadow/read.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage shadow\n\nimport (\n\t\"bufio\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n)\n\nvar (\n\tErrNoSuchUser    = errors.New(\"shadow: user entry is not present in database\")\n\tErrWrongPassword = errors.New(\"shadow: wrong password\")\n)\n\n// Read reads system shadow passwords database and returns all entires in it.\nfunc Read() ([]Entry, error) {\n\tf, err := os.Open(\"/etc/shadow\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tscnr := bufio.NewScanner(f)\n\n\tvar res []Entry\n\tfor scnr.Scan() {\n\t\tent, err := parseEntry(scnr.Text())\n\t\tif err != nil {\n\t\t\treturn res, err\n\t\t}\n\n\t\tres = append(res, *ent)\n\t}\n\tif err := scnr.Err(); err != nil {\n\t\treturn res, err\n\t}\n\treturn res, nil\n}\n\nfunc parseEntry(line string) (*Entry, error) {\n\tparts := strings.Split(line, \":\")\n\tif len(parts) != 9 {\n\t\treturn nil, errors.New(\"read: malformed entry\")\n\t}\n\n\tres := &Entry{\n\t\tName: parts[0],\n\t\tPass: parts[1],\n\t}\n\n\tfor i, value := range [...]*int{\n\t\t&res.LastChange, &res.MinPassAge, &res.MaxPassAge,\n\t\t&res.WarnPeriod, &res.InactivityPeriod, &res.AcctExpiry, &res.Flags,\n\t} {\n\t\tif parts[2+i] == \"\" {\n\t\t\t*value = -1\n\t\t} else {\n\t\t\tvar err error\n\t\t\t*value, err = strconv.Atoi(parts[2+i])\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"read: invalid value for field %d\", 2+i)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn res, nil\n}\n\nfunc Lookup(name string) (*Entry, error) {\n\tentries, err := Read()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, entry := range entries {\n\t\tif entry.Name == name {\n\t\t\treturn &entry, nil\n\t\t}\n\t}\n\n\treturn nil, ErrNoSuchUser\n}\n"
  },
  {
    "path": "internal/auth/shadow/shadow.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\n// shadow package implements utilities for parsing and using shadow password\n// database on Unix systems.\npackage shadow\n\ntype Entry struct {\n\t// User login name.\n\tName string\n\n\t// Hashed user password.\n\tPass string\n\n\t// Days since Jan 1, 1970 password was last changed.\n\tLastChange int\n\n\t// The number of days the user will have to wait before she will be allowed to\n\t// change her password again.\n\t//\n\t// -1 if password aging is disabled.\n\tMinPassAge int\n\n\t// The number of days after which the user will have to change her password.\n\t//\n\t// -1 is password aging is disabled.\n\tMaxPassAge int\n\n\t// The number of days before a password is going to expire (see the maximum\n\t// password age above) during which the user should be warned.\n\t//\n\t// -1 is password aging is disabled.\n\tWarnPeriod int\n\n\t// The number of days after a password has expired (see the maximum\n\t// password age above) during which the password should still be accepted.\n\t//\n\t// -1 is password aging is disabled.\n\tInactivityPeriod int\n\n\t// The date of expiration of the account, expressed as the number of days\n\t// since Jan 1, 1970.\n\t//\n\t// -1 is account never expires.\n\tAcctExpiry int\n\n\t// Unused now.\n\tFlags int\n}\n"
  },
  {
    "path": "internal/auth/shadow/verify.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage shadow\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/GehirnInc/crypt\"\n\t_ \"github.com/GehirnInc/crypt/sha256_crypt\"\n\t_ \"github.com/GehirnInc/crypt/sha512_crypt\"\n)\n\nconst secsInDay = 86400\n\nfunc (e *Entry) IsAccountValid() bool {\n\tif e.AcctExpiry == -1 {\n\t\treturn true\n\t}\n\n\tnowDays := int(time.Now().Unix() / secsInDay)\n\treturn nowDays < e.AcctExpiry\n}\n\nfunc (e *Entry) IsPasswordValid() bool {\n\tif e.LastChange == -1 || e.MaxPassAge == -1 || e.InactivityPeriod == -1 {\n\t\treturn true\n\t}\n\n\tnowDays := int(time.Now().Unix() / secsInDay)\n\treturn nowDays < e.LastChange+e.MaxPassAge+e.InactivityPeriod\n}\n\nfunc (e *Entry) VerifyPassword(pass string) (err error) {\n\t// Do not permit null and locked passwords.\n\tif e.Pass == \"\" {\n\t\treturn errors.New(\"verify: null password\")\n\t}\n\tif e.Pass[0] == '!' {\n\t\treturn errors.New(\"verify: locked password\")\n\t}\n\n\t// crypt.NewFromHash may panic on unknown hash function.\n\tdefer func() {\n\t\tif rcvr := recover(); rcvr != nil {\n\t\t\terr = fmt.Errorf(\"%v\", rcvr)\n\t\t}\n\t}()\n\n\tif err := crypt.NewFromHash(e.Pass).Verify(e.Pass, []byte(pass)); err != nil {\n\t\tif errors.Is(err, crypt.ErrKeyMismatch) {\n\t\t\treturn ErrWrongPassword\n\t\t}\n\t\treturn err\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/authz/lookup.go",
    "content": "package authz\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/foxcpp/maddy/framework/address\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n)\n\nfunc AuthorizeEmailUse(ctx context.Context, username string, addrs []string, mapping module.Table) (bool, error) {\n\tvar validEmails []string\n\n\tif multi, ok := mapping.(module.MultiTable); ok {\n\t\tvar err error\n\t\tvalidEmails, err = multi.LookupMulti(ctx, username)\n\t\tif err != nil {\n\t\t\treturn false, fmt.Errorf(\"authz: %w\", err)\n\t\t}\n\t} else {\n\t\tvalidEmail, ok, err := mapping.Lookup(ctx, username)\n\t\tif err != nil {\n\t\t\treturn false, fmt.Errorf(\"authz: %w\", err)\n\t\t}\n\t\tif ok {\n\t\t\tvalidEmails = []string{validEmail}\n\t\t}\n\t}\n\n\tfor _, addr := range addrs {\n\t\t_, domain, err := address.Split(addr)\n\t\tif err != nil {\n\t\t\treturn false, fmt.Errorf(\"authz: %w\", err)\n\t\t}\n\n\t\tfor _, ent := range validEmails {\n\t\t\tif ent == domain || ent == \"*\" || ent == addr {\n\t\t\t\treturn true, nil\n\t\t\t}\n\t\t}\n\t}\n\n\treturn false, nil\n}\n"
  },
  {
    "path": "internal/authz/normalization.go",
    "content": "package authz\n\nimport (\n\t\"strings\"\n\n\t\"github.com/foxcpp/maddy/framework/address\"\n\t\"golang.org/x/text/secure/precis\"\n)\n\ntype NormalizeFunc func(string) (string, error)\n\nfunc NormalizeNoop(s string) (string, error) {\n\treturn s, nil\n}\n\n// NormalizeAuto applies address.PRECISFold to valid emails and\n// plain UsernameCaseMapped profile to other strings.\nfunc NormalizeAuto(s string) (string, error) {\n\tif address.Valid(s) {\n\t\treturn address.PRECISFold(s)\n\t}\n\treturn precis.UsernameCaseMapped.CompareKey(s)\n}\n\n// NormalizeFuncs defines configurable normalization functions to be used\n// in authentication and authorization routines.\nvar NormalizeFuncs = map[string]NormalizeFunc{\n\t\"auto\":                  NormalizeAuto,\n\t\"precis_casefold_email\": address.PRECISFold,\n\t\"precis_casefold\":       precis.UsernameCaseMapped.CompareKey,\n\t\"precis_email\":          address.PRECIS,\n\t\"precis\":                precis.UsernameCasePreserved.CompareKey,\n\t\"casefold\": func(s string) (string, error) {\n\t\treturn strings.ToLower(s), nil\n\t},\n\t\"noop\": NormalizeNoop,\n}\n"
  },
  {
    "path": "internal/check/authorize_sender/authorize_sender.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage authorize_sender\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/mail\"\n\n\t\"github.com/emersion/go-message/textproto\"\n\t\"github.com/foxcpp/maddy/framework/buffer\"\n\t\"github.com/foxcpp/maddy/framework/config\"\n\tmodconfig \"github.com/foxcpp/maddy/framework/config/module\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/exterrors\"\n\t\"github.com/foxcpp/maddy/framework/log\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n\t\"github.com/foxcpp/maddy/internal/authz\"\n\t\"github.com/foxcpp/maddy/internal/table\"\n\t\"github.com/foxcpp/maddy/internal/target\"\n)\n\nconst modName = \"check.authorize_sender\"\n\ntype Check struct {\n\tinstName string\n\tlog      *log.Logger\n\n\tcheckHeader  bool\n\temailPrepare module.Table\n\tuserToEmail  module.Table\n\n\tunauthAction  modconfig.FailAction\n\tnoMatchAction modconfig.FailAction\n\terrAction     modconfig.FailAction\n\n\tfromNorm authz.NormalizeFunc\n\tauthNorm authz.NormalizeFunc\n}\n\nfunc New(c *container.C, modName, instName string) (module.Module, error) {\n\treturn &Check{\n\t\tinstName: instName,\n\t\tlog:      c.DefaultLogger.Sublogger(modName),\n\t}, nil\n}\n\nfunc (c *Check) Name() string {\n\treturn modName\n}\n\nfunc (c *Check) InstanceName() string {\n\treturn c.instName\n}\n\nfunc (c *Check) Configure(inlineArgs []string, cfg *config.Map) error {\n\tif len(inlineArgs) != 0 {\n\t\treturn fmt.Errorf(\"%s: inline arguments are not used\", modName)\n\t}\n\n\tcfg.Bool(\"debug\", true, false, &c.log.Debug)\n\n\tcfg.Bool(\"check_header\", false, true, &c.checkHeader)\n\n\tcfg.Custom(\"prepare_email\", false, false, func() (interface{}, error) {\n\t\treturn &table.Identity{}, nil\n\t}, modconfig.TableDirective, &c.emailPrepare)\n\tcfg.Custom(\"user_to_email\", false, false, func() (interface{}, error) {\n\t\treturn &table.Identity{}, nil\n\t}, modconfig.TableDirective, &c.userToEmail)\n\n\tcfg.Custom(\"unauth_action\", false, false, func() (interface{}, error) {\n\t\treturn modconfig.FailAction{Reject: true}, nil\n\t}, modconfig.FailActionDirective, &c.unauthAction)\n\tcfg.Custom(\"no_match_action\", false, false, func() (interface{}, error) {\n\t\treturn modconfig.FailAction{Reject: true}, nil\n\t}, modconfig.FailActionDirective, &c.noMatchAction)\n\tcfg.Custom(\"err_action\", false, false, func() (interface{}, error) {\n\t\treturn modconfig.FailAction{Reject: true}, nil\n\t}, modconfig.FailActionDirective, &c.errAction)\n\n\tconfig.EnumMapped(cfg, \"auth_normalize\", true, false, authz.NormalizeFuncs, authz.NormalizeAuto,\n\t\t&c.authNorm)\n\tconfig.EnumMapped(cfg, \"from_normalize\", true, false, authz.NormalizeFuncs, authz.NormalizeAuto,\n\t\t&c.fromNorm)\n\n\tif _, err := cfg.Process(); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\ntype state struct {\n\tc       *Check\n\tmsgMeta *module.MsgMetadata\n\tlog     *log.Logger\n}\n\nfunc (c *Check) CheckStateForMsg(_ context.Context, msgMeta *module.MsgMetadata) (module.CheckState, error) {\n\treturn &state{\n\t\tc:       c,\n\t\tmsgMeta: msgMeta,\n\t\tlog:     target.DeliveryLogger(c.log, msgMeta),\n\t}, nil\n}\n\nfunc (s *state) authzSender(ctx context.Context, authName, email string) module.CheckResult {\n\tif authName == \"\" {\n\t\treturn s.c.unauthAction.Apply(module.CheckResult{\n\t\t\tReason: &exterrors.SMTPError{\n\t\t\t\tCode:         530,\n\t\t\t\tEnhancedCode: exterrors.EnhancedCode{5, 7, 0},\n\t\t\t\tMessage:      \"Authentication required\",\n\t\t\t\tCheckName:    modName,\n\t\t\t}})\n\t}\n\n\tfromEmailNorm, err := s.c.fromNorm(email)\n\tif err != nil {\n\t\treturn s.c.errAction.Apply(module.CheckResult{\n\t\t\tReason: &exterrors.SMTPError{\n\t\t\t\tCode:         553,\n\t\t\t\tEnhancedCode: exterrors.EnhancedCode{5, 1, 7},\n\t\t\t\tMessage:      \"Unable to normalize sender address\",\n\t\t\t\tCheckName:    modName,\n\t\t\t\tErr:          err,\n\t\t\t}})\n\t}\n\tauthNameNorm, err := s.c.authNorm(authName)\n\tif err != nil {\n\t\treturn s.c.errAction.Apply(module.CheckResult{\n\t\t\tReason: &exterrors.SMTPError{\n\t\t\t\tCode:         535,\n\t\t\t\tEnhancedCode: exterrors.EnhancedCode{5, 7, 8},\n\t\t\t\tMessage:      \"Unable to normalize authorization username\",\n\t\t\t\tCheckName:    modName,\n\t\t\t}})\n\t}\n\n\tvar preparedEmail []string\n\tvar ok bool\n\ts.log.DebugMsg(\"normalized names\", \"from\", fromEmailNorm, \"auth\", authNameNorm)\n\tif emailPrepareMulti, isMulti := s.c.emailPrepare.(module.MultiTable); isMulti {\n\t\tpreparedEmail, err = emailPrepareMulti.LookupMulti(ctx, fromEmailNorm)\n\t\tok = len(preparedEmail) > 0\n\t} else {\n\t\tvar preparedEmail_single string\n\t\tpreparedEmail_single, ok, err = s.c.emailPrepare.Lookup(ctx, fromEmailNorm)\n\t\tpreparedEmail = []string{preparedEmail_single}\n\t}\n\ts.log.DebugMsg(\"authorized emails\", \"preparedEmail\", preparedEmail, \"ok\", ok)\n\tif err != nil {\n\t\treturn s.c.errAction.Apply(module.CheckResult{\n\t\t\tReason: &exterrors.SMTPError{\n\t\t\t\tCode:         454,\n\t\t\t\tEnhancedCode: exterrors.EnhancedCode{4, 7, 0},\n\t\t\t\tMessage:      \"Internal error during policy check\",\n\t\t\t\tCheckName:    modName,\n\t\t\t\tErr:          err,\n\t\t\t}})\n\t}\n\tif !ok {\n\t\tpreparedEmail = []string{fromEmailNorm}\n\t}\n\n\tok, err = authz.AuthorizeEmailUse(ctx, authNameNorm, preparedEmail, s.c.userToEmail)\n\tif err != nil {\n\t\treturn s.c.errAction.Apply(module.CheckResult{\n\t\t\tReason: &exterrors.SMTPError{\n\t\t\t\tCode:         454,\n\t\t\t\tEnhancedCode: exterrors.EnhancedCode{4, 7, 0},\n\t\t\t\tMessage:      \"Internal error during policy check\",\n\t\t\t\tCheckName:    modName,\n\t\t\t\tErr:          err,\n\t\t\t}})\n\t}\n\tif !ok {\n\t\treturn s.c.noMatchAction.Apply(module.CheckResult{\n\t\t\tReason: &exterrors.SMTPError{\n\t\t\t\tCode:         553,\n\t\t\t\tEnhancedCode: exterrors.EnhancedCode{5, 7, 0},\n\t\t\t\tMessage:      \"Unauthorized use of sender address\",\n\t\t\t\tCheckName:    modName,\n\t\t\t}})\n\t}\n\n\treturn module.CheckResult{}\n}\n\nfunc (s *state) CheckConnection(_ context.Context) module.CheckResult {\n\treturn module.CheckResult{}\n}\n\nfunc (s *state) CheckSender(ctx context.Context, fromEmail string) module.CheckResult {\n\tif s.msgMeta.Conn == nil {\n\t\ts.log.Msg(\"skipping locally generated message\")\n\t\treturn module.CheckResult{}\n\t}\n\tauthName := s.msgMeta.Conn.AuthUser\n\n\treturn s.authzSender(ctx, authName, fromEmail)\n}\n\nfunc (s *state) CheckRcpt(_ context.Context, _ string) module.CheckResult {\n\treturn module.CheckResult{}\n}\n\nfunc (s *state) CheckBody(ctx context.Context, hdr textproto.Header, _ buffer.Buffer) module.CheckResult {\n\tif !s.c.checkHeader {\n\t\treturn module.CheckResult{}\n\t}\n\tif s.msgMeta.Conn == nil {\n\t\ts.log.Msg(\"skipping locally generated message\")\n\t\treturn module.CheckResult{}\n\t}\n\tauthName := s.msgMeta.Conn.AuthUser\n\n\tfromHdr := hdr.Get(\"From\")\n\tif fromHdr == \"\" {\n\t\treturn s.c.errAction.Apply(module.CheckResult{\n\t\t\tReason: &exterrors.SMTPError{\n\t\t\t\tCode:         550,\n\t\t\t\tEnhancedCode: exterrors.EnhancedCode{5, 7, 0},\n\t\t\t\tMessage:      \"Missing From header\",\n\t\t\t\tCheckName:    modName,\n\t\t\t}})\n\t}\n\tlist, err := mail.ParseAddressList(fromHdr)\n\tif err != nil || len(list) == 0 {\n\t\treturn s.c.errAction.Apply(module.CheckResult{\n\t\t\tReason: &exterrors.SMTPError{\n\t\t\t\tCode:         550,\n\t\t\t\tEnhancedCode: exterrors.EnhancedCode{5, 7, 0},\n\t\t\t\tMessage:      \"Malformed From header\",\n\t\t\t\tCheckName:    modName,\n\t\t\t\tErr:          err,\n\t\t\t}})\n\t}\n\tfromEmail := list[0].Address\n\tif len(list) > 1 {\n\t\treturn s.c.errAction.Apply(module.CheckResult{\n\t\t\tReason: &exterrors.SMTPError{\n\t\t\t\tCode:         550,\n\t\t\t\tEnhancedCode: exterrors.EnhancedCode{5, 7, 0},\n\t\t\t\tMessage:      \"Multiple From addresses are not allowed\",\n\t\t\t\tCheckName:    modName,\n\t\t\t\tErr:          err,\n\t\t\t}})\n\t}\n\n\tvar senderAddr string\n\tif senderHdr := hdr.Get(\"Sender\"); senderHdr != \"\" {\n\t\tsender, err := mail.ParseAddress(senderHdr)\n\t\tif err != nil {\n\t\t\treturn s.c.errAction.Apply(module.CheckResult{\n\t\t\t\tReason: &exterrors.SMTPError{\n\t\t\t\t\tCode:         550,\n\t\t\t\t\tEnhancedCode: exterrors.EnhancedCode{5, 7, 0},\n\t\t\t\t\tMessage:      \"Malformed Sender header\",\n\t\t\t\t\tCheckName:    modName,\n\t\t\t\t\tErr:          err,\n\t\t\t\t}})\n\t\t}\n\t\tsenderAddr = sender.Address\n\t}\n\n\tres := s.authzSender(ctx, authName, fromEmail)\n\tif res.Reason == nil {\n\t\treturn res\n\t}\n\n\tif senderAddr != \"\" && senderAddr != fromEmail {\n\t\tres = s.authzSender(ctx, authName, senderAddr)\n\t\tif res.Reason == nil {\n\t\t\treturn res\n\t\t}\n\t}\n\n\t// Neither matched.\n\treturn s.c.noMatchAction.Apply(module.CheckResult{\n\t\tReason: &exterrors.SMTPError{\n\t\t\tCode:         553,\n\t\t\tEnhancedCode: exterrors.EnhancedCode{5, 7, 0},\n\t\t\tMessage:      \"Unauthorized use of sender address\",\n\t\t\tCheckName:    modName,\n\t\t}})\n}\n\nfunc (s *state) Close() error {\n\treturn nil\n}\n\nfunc init() {\n\tmodules.Register(modName, New)\n}\n"
  },
  {
    "path": "internal/check/command/command.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage command\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"os\"\n\t\"os/exec\"\n\t\"regexp\"\n\t\"runtime/trace\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/emersion/go-message/textproto\"\n\t\"github.com/foxcpp/maddy/framework/buffer\"\n\t\"github.com/foxcpp/maddy/framework/config\"\n\tmodconfig \"github.com/foxcpp/maddy/framework/config/module\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/exterrors\"\n\t\"github.com/foxcpp/maddy/framework/log\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n\t\"github.com/foxcpp/maddy/internal/target\"\n)\n\nconst modName = \"check.command\"\n\ntype Stage string\n\nconst (\n\tStageConnection = \"conn\"\n\tStageSender     = \"sender\"\n\tStageRcpt       = \"rcpt\"\n\tStageBody       = \"body\"\n)\n\nvar placeholderRe = regexp.MustCompile(`{[a-zA-Z0-9_]+?}`)\n\ntype Check struct {\n\tinstName string\n\tlog      *log.Logger\n\n\tstage   Stage\n\tactions map[int]modconfig.FailAction\n\tcmd     string\n\tcmdArgs []string\n}\n\nfunc New(c *container.C, modName, instName string) (module.Module, error) {\n\tchk := &Check{\n\t\tinstName: instName,\n\t\tlog:      c.DefaultLogger.Sublogger(modName),\n\t\tactions: map[int]modconfig.FailAction{\n\t\t\t1: {\n\t\t\t\tReject: true,\n\t\t\t},\n\t\t\t2: {\n\t\t\t\tQuarantine: true,\n\t\t\t},\n\t\t},\n\t}\n\n\treturn chk, nil\n}\n\nfunc (c *Check) Name() string {\n\treturn modName\n}\n\nfunc (c *Check) InstanceName() string {\n\treturn c.instName\n}\n\nfunc (c *Check) Configure(inlineArgs []string, cfg *config.Map) error {\n\tif len(inlineArgs) == 0 {\n\t\treturn errors.New(\"command: at least one argument is required (command name)\")\n\t}\n\n\tc.cmd = inlineArgs[0]\n\tc.cmdArgs = inlineArgs[1:]\n\n\t// Check whether the inline argument command is usable.\n\tif _, err := exec.LookPath(c.cmd); err != nil {\n\t\treturn fmt.Errorf(\"command: %w\", err)\n\t}\n\n\tcfg.Enum(\"run_on\", false, false,\n\t\t[]string{StageConnection, StageSender, StageRcpt, StageBody}, StageBody,\n\t\t(*string)(&c.stage))\n\n\tcfg.AllowUnknown()\n\tunknown, err := cfg.Process()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor _, node := range unknown {\n\t\tswitch node.Name {\n\t\tcase \"code\":\n\t\t\tif len(node.Args) < 2 {\n\t\t\t\treturn config.NodeErr(node, \"at least two arguments are required: <code> <action>\")\n\t\t\t}\n\t\t\texitCode, err := strconv.Atoi(node.Args[0])\n\t\t\tif err != nil {\n\t\t\t\treturn config.NodeErr(node, \"%v\", err)\n\t\t\t}\n\t\t\taction, err := modconfig.ParseActionDirective(node.Args[1:])\n\t\t\tif err != nil {\n\t\t\t\treturn config.NodeErr(node, \"%v\", err)\n\t\t\t}\n\n\t\t\tc.actions[exitCode] = action\n\t\tdefault:\n\t\t\treturn config.NodeErr(node, \"unexpected directive: %v\", node.Name)\n\t\t}\n\t}\n\n\treturn nil\n}\n\ntype state struct {\n\tc       *Check\n\tmsgMeta *module.MsgMetadata\n\tlog     *log.Logger\n\n\tmailFrom string\n\trcpts    []string\n}\n\nfunc (c *Check) CheckStateForMsg(ctx context.Context, msgMeta *module.MsgMetadata) (module.CheckState, error) {\n\treturn &state{\n\t\tc:       c,\n\t\tmsgMeta: msgMeta,\n\t\tlog:     target.DeliveryLogger(c.log, msgMeta),\n\t}, nil\n}\n\nfunc (s *state) expandCommand(address string) (string, []string) {\n\texpArgs := make([]string, len(s.c.cmdArgs))\n\n\tfor i, arg := range s.c.cmdArgs {\n\t\texpArgs[i] = placeholderRe.ReplaceAllStringFunc(arg, func(placeholder string) string {\n\t\t\tswitch placeholder {\n\t\t\tcase \"{auth_user}\":\n\t\t\t\tif s.msgMeta.Conn == nil {\n\t\t\t\t\treturn \"\"\n\t\t\t\t}\n\t\t\t\treturn s.msgMeta.Conn.AuthUser\n\t\t\tcase \"{source_ip}\":\n\t\t\t\tif s.msgMeta.Conn == nil {\n\t\t\t\t\treturn \"\"\n\t\t\t\t}\n\t\t\t\ttcpAddr, _ := s.msgMeta.Conn.RemoteAddr.(*net.TCPAddr)\n\t\t\t\tif tcpAddr == nil {\n\t\t\t\t\treturn \"\"\n\t\t\t\t}\n\t\t\t\treturn tcpAddr.IP.String()\n\t\t\tcase \"{source_host}\":\n\t\t\t\tif s.msgMeta.Conn == nil {\n\t\t\t\t\treturn \"\"\n\t\t\t\t}\n\t\t\t\treturn s.msgMeta.Conn.Hostname\n\t\t\tcase \"{source_rdns}\":\n\t\t\t\tif s.msgMeta.Conn == nil {\n\t\t\t\t\treturn \"\"\n\t\t\t\t}\n\t\t\t\tvalI, err := s.msgMeta.Conn.RDNSName.Get()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn \"\"\n\t\t\t\t}\n\t\t\t\tif valI == nil {\n\t\t\t\t\treturn \"\"\n\t\t\t\t}\n\t\t\t\treturn valI.(string)\n\t\t\tcase \"{msg_id}\":\n\t\t\t\treturn s.msgMeta.ID\n\t\t\tcase \"{sender}\":\n\t\t\t\treturn s.mailFrom\n\t\t\tcase \"{rcpts}\":\n\t\t\t\treturn strings.Join(s.rcpts, \"\\n\")\n\t\t\tcase \"{address}\":\n\t\t\t\treturn address\n\t\t\t}\n\t\t\treturn placeholder\n\t\t})\n\t}\n\n\treturn s.c.cmd, expArgs\n}\n\nfunc (s *state) run(cmdName string, args []string, stdin io.Reader) module.CheckResult {\n\tcmd := exec.Command(cmdName, args...)\n\tcmd.Stdin = stdin\n\tstdout, err := cmd.StdoutPipe()\n\tif err != nil {\n\t\treturn module.CheckResult{\n\t\t\tReason: &exterrors.SMTPError{\n\t\t\t\tCode:      450,\n\t\t\t\tMessage:   \"Internal server error\",\n\t\t\t\tCheckName: \"command\",\n\t\t\t\tErr:       err,\n\t\t\t\tMisc: map[string]interface{}{\n\t\t\t\t\t\"cmd\": cmd.String(),\n\t\t\t\t},\n\t\t\t},\n\t\t\tReject: true,\n\t\t}\n\t}\n\n\tif err := cmd.Start(); err != nil {\n\t\treturn module.CheckResult{\n\t\t\tReason: &exterrors.SMTPError{\n\t\t\t\tCode:      450,\n\t\t\t\tMessage:   \"Internal server error\",\n\t\t\t\tCheckName: \"command\",\n\t\t\t\tErr:       err,\n\t\t\t\tMisc: map[string]interface{}{\n\t\t\t\t\t\"cmd\": cmd.String(),\n\t\t\t\t},\n\t\t\t},\n\t\t\tReject: true,\n\t\t}\n\t}\n\n\tbufOut := bufio.NewReader(stdout)\n\thdr, err := textproto.ReadHeader(bufOut)\n\tif err != nil && !errors.Is(err, io.EOF) {\n\t\tif err := cmd.Process.Signal(os.Interrupt); err != nil {\n\t\t\ts.log.Error(\"failed to kill process\", err)\n\t\t}\n\n\t\treturn module.CheckResult{\n\t\t\tReason: &exterrors.SMTPError{\n\t\t\t\tCode:      450,\n\t\t\t\tMessage:   \"Internal server error\",\n\t\t\t\tCheckName: \"command\",\n\t\t\t\tErr:       err,\n\t\t\t\tMisc: map[string]interface{}{\n\t\t\t\t\t\"cmd\": cmd.String(),\n\t\t\t\t},\n\t\t\t},\n\t\t\tReject: true,\n\t\t}\n\t}\n\n\tres := module.CheckResult{}\n\tres.Header = hdr\n\n\terr = cmd.Wait()\n\tif err != nil {\n\t\tif _, ok := err.(*exec.ExitError); !ok {\n\t\t\t// If that's not ExitError, the process may still be running. We do\n\t\t\t// not want this.\n\t\t\tif err := cmd.Process.Signal(os.Interrupt); err != nil {\n\t\t\t\ts.log.Error(\"failed to kill process\", err)\n\t\t\t}\n\t\t}\n\t\treturn s.errorRes(err, res, cmd.String())\n\t}\n\treturn res\n}\n\nfunc (s *state) errorRes(err error, res module.CheckResult, cmdLine string) module.CheckResult {\n\texitErr, ok := err.(*exec.ExitError)\n\tif !ok {\n\t\tres.Reason = &exterrors.SMTPError{\n\t\t\tCode:      450,\n\t\t\tMessage:   \"Internal server error\",\n\t\t\tCheckName: \"command\",\n\t\t\tErr:       err,\n\t\t\tMisc: map[string]interface{}{\n\t\t\t\t\"cmd\": cmdLine,\n\t\t\t},\n\t\t}\n\t\tres.Reject = true\n\t\treturn res\n\t}\n\n\taction, ok := s.c.actions[exitErr.ExitCode()]\n\tif !ok {\n\t\tres.Reason = &exterrors.SMTPError{\n\t\t\tCode:      450,\n\t\t\tMessage:   \"Internal server error\",\n\t\t\tCheckName: \"command\",\n\t\t\tErr:       err,\n\t\t\tReason:    \"unexpected exit code\",\n\t\t\tMisc: map[string]interface{}{\n\t\t\t\t\"cmd\":       cmdLine,\n\t\t\t\t\"exit_code\": exitErr.ExitCode(),\n\t\t\t},\n\t\t}\n\t\tres.Reject = true\n\t\treturn res\n\t}\n\n\tres.Reason = &exterrors.SMTPError{\n\t\tCode:         550,\n\t\tEnhancedCode: exterrors.EnhancedCode{5, 7, 1},\n\t\tMessage:      \"Message rejected for due to a local policy\",\n\t\tCheckName:    \"command\",\n\t\tMisc: map[string]interface{}{\n\t\t\t\"cmd\":       cmdLine,\n\t\t\t\"exit_code\": exitErr.ExitCode(),\n\t\t},\n\t}\n\n\treturn action.Apply(res)\n}\n\nfunc (s *state) CheckConnection(ctx context.Context) module.CheckResult {\n\tif s.c.stage != StageConnection {\n\t\treturn module.CheckResult{}\n\t}\n\n\tdefer trace.StartRegion(ctx, \"command/CheckConnection-\"+s.c.cmd).End()\n\n\tcmdName, cmdArgs := s.expandCommand(\"\")\n\treturn s.run(cmdName, cmdArgs, bytes.NewReader(nil))\n}\n\nfunc (s *state) CheckSender(ctx context.Context, addr string) module.CheckResult {\n\ts.mailFrom = addr\n\n\tif s.c.stage != StageSender {\n\t\treturn module.CheckResult{}\n\t}\n\n\tdefer trace.StartRegion(ctx, \"command/CheckSender\"+s.c.cmd).End()\n\n\tcmdName, cmdArgs := s.expandCommand(addr)\n\treturn s.run(cmdName, cmdArgs, bytes.NewReader(nil))\n}\n\nfunc (s *state) CheckRcpt(ctx context.Context, addr string) module.CheckResult {\n\ts.rcpts = append(s.rcpts, addr)\n\n\tif s.c.stage != StageRcpt {\n\t\treturn module.CheckResult{}\n\t}\n\tdefer trace.StartRegion(ctx, \"command/CheckRcpt\"+s.c.cmd).End()\n\n\tcmdName, cmdArgs := s.expandCommand(addr)\n\treturn s.run(cmdName, cmdArgs, bytes.NewReader(nil))\n}\n\nfunc (s *state) CheckBody(ctx context.Context, hdr textproto.Header, body buffer.Buffer) module.CheckResult {\n\tif s.c.stage != StageBody {\n\t\treturn module.CheckResult{}\n\t}\n\n\tdefer trace.StartRegion(ctx, \"command/CheckBody\"+s.c.cmd).End()\n\n\tcmdName, cmdArgs := s.expandCommand(\"\")\n\n\tvar buf bytes.Buffer\n\t_ = textproto.WriteHeader(&buf, hdr)\n\tbR, err := body.Open()\n\tif err != nil {\n\t\treturn module.CheckResult{\n\t\t\tReason: &exterrors.SMTPError{\n\t\t\t\tCode:      450,\n\t\t\t\tMessage:   \"Internal server error\",\n\t\t\t\tCheckName: \"command\",\n\t\t\t\tErr:       err,\n\t\t\t\tMisc: map[string]interface{}{\n\t\t\t\t\t\"cmd\": cmdName + \" \" + strings.Join(cmdArgs, \" \"),\n\t\t\t\t},\n\t\t\t},\n\t\t\tReject: true,\n\t\t}\n\t}\n\n\treturn s.run(cmdName, cmdArgs, io.MultiReader(bytes.NewReader(buf.Bytes()), bR))\n}\n\nfunc (s *state) Close() error {\n\treturn nil\n}\n\nfunc init() {\n\tmodules.Register(modName, New)\n}\n"
  },
  {
    "path": "internal/check/dkim/dkim.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage dkim\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"io\"\n\tnettextproto \"net/textproto\"\n\t\"runtime/trace\"\n\t\"strings\"\n\n\t\"github.com/emersion/go-message/textproto\"\n\t\"github.com/emersion/go-msgauth/authres\"\n\t\"github.com/emersion/go-msgauth/dkim\"\n\t\"github.com/foxcpp/maddy/framework/buffer\"\n\t\"github.com/foxcpp/maddy/framework/config\"\n\tmodconfig \"github.com/foxcpp/maddy/framework/config/module\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/dns\"\n\t\"github.com/foxcpp/maddy/framework/exterrors\"\n\t\"github.com/foxcpp/maddy/framework/log\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n\t\"github.com/foxcpp/maddy/internal/target\"\n)\n\ntype Check struct {\n\tinstName string\n\tlog      *log.Logger\n\n\trequiredFields  map[string]struct{}\n\tbrokenSigAction modconfig.FailAction\n\tnoSigAction     modconfig.FailAction\n\tfailOpen        bool\n\n\tresolver dns.Resolver\n}\n\nfunc New(c *container.C, modName, instName string) (module.Module, error) {\n\treturn &Check{\n\t\tinstName: instName,\n\t\tlog:      c.DefaultLogger.Sublogger(modName),\n\t\tresolver: dns.DefaultResolver(),\n\t}, nil\n}\n\nfunc (c *Check) Configure(inlineArgs []string, cfg *config.Map) error {\n\tif len(inlineArgs) != 0 {\n\t\treturn errors.New(\"check.dkim: inline arguments are not used\")\n\t}\n\n\tvar requiredFields []string\n\n\tcfg.Bool(\"debug\", true, false, &c.log.Debug)\n\tcfg.StringList(\"required_fields\", false, false, []string{\"From\", \"Subject\"}, &requiredFields)\n\tcfg.Bool(\"fail_open\", false, false, &c.failOpen)\n\tcfg.Custom(\"broken_sig_action\", false, false,\n\t\tfunc() (interface{}, error) {\n\t\t\treturn modconfig.FailAction{}, nil\n\t\t}, modconfig.FailActionDirective, &c.brokenSigAction)\n\tcfg.Custom(\"no_sig_action\", false, false,\n\t\tfunc() (interface{}, error) {\n\t\t\treturn modconfig.FailAction{}, nil\n\t\t}, modconfig.FailActionDirective, &c.noSigAction)\n\t_, err := cfg.Process()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tc.requiredFields = make(map[string]struct{})\n\tfor _, field := range requiredFields {\n\t\tc.requiredFields[nettextproto.CanonicalMIMEHeaderKey(field)] = struct{}{}\n\t}\n\n\treturn nil\n}\n\nfunc (c *Check) Name() string {\n\treturn \"check.dkim\"\n}\n\nfunc (c *Check) InstanceName() string {\n\treturn c.instName\n}\n\ntype dkimCheckState struct {\n\tc       *Check\n\tmsgMeta *module.MsgMetadata\n\tlog     *log.Logger\n}\n\nfunc (d *dkimCheckState) CheckConnection(ctx context.Context) module.CheckResult {\n\treturn module.CheckResult{}\n}\n\nfunc (d *dkimCheckState) CheckSender(ctx context.Context, mailFrom string) module.CheckResult {\n\treturn module.CheckResult{}\n}\n\nfunc (d *dkimCheckState) CheckRcpt(ctx context.Context, rcptTo string) module.CheckResult {\n\treturn module.CheckResult{}\n}\n\nfunc (d *dkimCheckState) CheckBody(ctx context.Context, header textproto.Header, body buffer.Buffer) module.CheckResult {\n\tdefer trace.StartRegion(ctx, \"check.dkim/CheckBody\").End()\n\n\tif !header.Has(\"DKIM-Signature\") {\n\t\tif d.c.noSigAction.Reject || d.c.noSigAction.Quarantine {\n\t\t\td.log.Printf(\"no signatures present\")\n\t\t} else {\n\t\t\td.log.Debugf(\"no signatures present\")\n\t\t}\n\t\treturn d.c.noSigAction.Apply(module.CheckResult{\n\t\t\tReason: &exterrors.SMTPError{\n\t\t\t\tCode:         550,\n\t\t\t\tEnhancedCode: exterrors.EnhancedCode{5, 7, 20},\n\t\t\t\tMessage:      \"No DKIM signatures\",\n\t\t\t\tCheckName:    \"check.dkim\",\n\t\t\t},\n\t\t\tAuthResult: []authres.Result{\n\t\t\t\t&authres.DKIMResult{\n\t\t\t\t\tValue: authres.ResultNone,\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\t}\n\n\tb := bytes.Buffer{}\n\t_ = textproto.WriteHeader(&b, header)\n\tbodyRdr, err := body.Open()\n\tif err != nil {\n\t\treturn module.CheckResult{\n\t\t\tReject: true,\n\t\t\tReason: exterrors.WithTemporary(\n\t\t\t\texterrors.WithFields(err, map[string]interface{}{\n\t\t\t\t\t\"check\":    \"check.dkim\",\n\t\t\t\t\t\"smtp_msg\": \"Internal I/O error\",\n\t\t\t\t}),\n\t\t\t\ttrue,\n\t\t\t),\n\t\t}\n\t}\n\n\tverifications, err := dkim.VerifyWithOptions(io.MultiReader(&b, bodyRdr), &dkim.VerifyOptions{\n\t\tLookupTXT: func(domain string) ([]string, error) {\n\t\t\treturn d.c.resolver.LookupTXT(ctx, domain)\n\t\t},\n\t})\n\tif err != nil {\n\t\treturn module.CheckResult{\n\t\t\tReject: true,\n\t\t\tReason: exterrors.WithTemporary(\n\t\t\t\texterrors.WithFields(err, map[string]interface{}{\n\t\t\t\t\t\"check\":    \"check.dkim\",\n\t\t\t\t\t\"smtp_msg\": \"Internal error during policy check\",\n\t\t\t\t}),\n\t\t\t\ttrue,\n\t\t\t),\n\t\t}\n\t}\n\n\tgoodSigs := false\n\n\tres := module.CheckResult{AuthResult: make([]authres.Result, 0, len(verifications))}\n\tfor _, verif := range verifications {\n\t\tval := authres.ResultValue(authres.ResultPass)\n\t\treason := \"\"\n\t\tif verif.Err != nil {\n\t\t\tval = authres.ResultFail\n\n\t\t\treason = strings.TrimPrefix(verif.Err.Error(), \"dkim: \")\n\t\t\tif !d.c.brokenSigAction.Reject || !d.c.brokenSigAction.Quarantine {\n\t\t\t\td.log.DebugMsg(\"bad signature\", \"domain\", verif.Domain, \"identifier\", verif.Identifier)\n\t\t\t}\n\t\t\tif dkim.IsPermFail(verif.Err) {\n\t\t\t\tval = authres.ResultPermError\n\t\t\t}\n\t\t\tif dkim.IsTempFail(verif.Err) {\n\t\t\t\tif !d.c.failOpen {\n\t\t\t\t\treturn module.CheckResult{\n\t\t\t\t\t\tReject: true,\n\t\t\t\t\t\tReason: &exterrors.SMTPError{\n\t\t\t\t\t\t\tCode:         421,\n\t\t\t\t\t\t\tEnhancedCode: exterrors.EnhancedCode{4, 7, 20},\n\t\t\t\t\t\t\tMessage:      \"Temporary error during DKIM verification\",\n\t\t\t\t\t\t\tCheckName:    \"check.dkim\",\n\t\t\t\t\t\t\tErr:          verif.Err,\n\t\t\t\t\t\t},\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tval = authres.ResultTempError\n\t\t\t}\n\n\t\t\tres.AuthResult = append(res.AuthResult, &authres.DKIMResult{\n\t\t\t\tValue:      val,\n\t\t\t\tReason:     reason,\n\t\t\t\tDomain:     verif.Domain,\n\t\t\t\tIdentifier: verif.Identifier,\n\t\t\t})\n\t\t\tcontinue\n\t\t}\n\n\t\tsignedFields := make(map[string]struct{}, len(verif.HeaderKeys))\n\t\tfor _, field := range verif.HeaderKeys {\n\t\t\tsignedFields[nettextproto.CanonicalMIMEHeaderKey(field)] = struct{}{}\n\t\t}\n\t\tfor field := range d.c.requiredFields {\n\t\t\tif _, ok := signedFields[field]; !ok {\n\t\t\t\tval = authres.ResultPermError\n\t\t\t\treason = \"some header fields are not signed\"\n\t\t\t}\n\t\t}\n\n\t\tif val == authres.ResultPass {\n\t\t\tgoodSigs = true\n\t\t\td.log.DebugMsg(\"good signature\", \"domain\", verif.Domain, \"identifier\", verif.Identifier)\n\t\t}\n\n\t\tres.AuthResult = append(res.AuthResult, &authres.DKIMResult{\n\t\t\tValue:      val,\n\t\t\tReason:     reason,\n\t\t\tDomain:     verif.Domain,\n\t\t\tIdentifier: verif.Identifier,\n\t\t})\n\t}\n\n\tif !goodSigs {\n\t\tres.Reason = &exterrors.SMTPError{\n\t\t\tCode:         550,\n\t\t\tEnhancedCode: exterrors.EnhancedCode{5, 7, 20},\n\t\t\tMessage:      \"No passing DKIM signatures\",\n\t\t\tCheckName:    \"check.dkim\",\n\t\t}\n\t\treturn d.c.brokenSigAction.Apply(res)\n\t}\n\treturn res\n}\n\nfunc (d *dkimCheckState) Name() string {\n\treturn \"check.dkim\"\n}\n\nfunc (d *dkimCheckState) Close() error {\n\treturn nil\n}\n\nfunc (c *Check) CheckStateForMsg(ctx context.Context, msgMeta *module.MsgMetadata) (module.CheckState, error) {\n\treturn &dkimCheckState{\n\t\tc:       c,\n\t\tmsgMeta: msgMeta,\n\t\tlog:     target.DeliveryLogger(c.log, msgMeta),\n\t}, nil\n}\n\nfunc init() {\n\tmodules.Register(\"check.dkim\", New)\n}\n"
  },
  {
    "path": "internal/check/dkim/dkim_test.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage dkim\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"net\"\n\t\"testing\"\n\n\t\"github.com/emersion/go-msgauth/authres\"\n\t\"github.com/foxcpp/go-mockdns\"\n\t\"github.com/foxcpp/maddy/framework/buffer\"\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/exterrors\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/internal/testutils\"\n)\n\nconst unsignedMailString = `From: Joe SixPack <joe@football.example.com>\nTo: Suzie Q <suzie@shopping.example.net>\nSubject: Is dinner ready?\nDate: Fri, 11 Jul 2003 21:00:37 -0700 (PDT)\nMessage-ID: <20030712040037.46341.5F8J@football.example.com>\n\nHi.\n\nWe lost the game. Are you hungry yet?\n\nJoe.\n`\n\nconst dnsPublicKey = \"v=DKIM1; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQ\" +\n\t\"KBgQDwIRP/UC3SBsEmGqZ9ZJW3/DkMoGeLnQg1fWn7/zYt\" +\n\t\"IxN2SnFCjxOCKG9v3b4jYfcTNh5ijSsq631uBItLa7od+v\" +\n\t\"/RtdC2UzJ1lWT947qR+Rcac2gbto/NMqJ0fzfVjH4OuKhi\" +\n\t\"tdY9tf6mcwGjaNBcWToIMmPSPDdQPNUYckcQ2QIDAQAB\"\n\nvar testZones = map[string]mockdns.Zone{\n\t\"brisbane._domainkey.example.com.\": {\n\t\tTXT: []string{dnsPublicKey},\n\t},\n}\n\nconst verifiedMailString = `DKIM-Signature: v=1; a=rsa-sha256; s=brisbane; d=example.com;\n      c=simple/simple; q=dns/txt; i=joe@football.example.com;\n      h=Received : From : To : Subject : Date : Message-ID;\n      bh=2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=;\n      b=AuUoFEfDxTDkHlLXSZEpZj79LICEps6eda7W3deTVFOk4yAUoqOB\n      4nujc7YopdG5dWLSdNg6xNAZpOPr+kHxt1IrE+NahM6L/LbvaHut\n      KVdkLLkpVaVVQPzeRDI009SO2Il5Lu7rDNH6mZckBdrIx0orEtZV\n      4bmp/YzhwvcubU4=;\nReceived: from client1.football.example.com  [192.0.2.1]\n      by submitserver.example.com with SUBMISSION;\n      Fri, 11 Jul 2003 21:01:54 -0700 (PDT)\nFrom: Joe SixPack <joe@football.example.com>\nTo: Suzie Q <suzie@shopping.example.net>\nSubject: Is dinner ready?\nDate: Fri, 11 Jul 2003 21:00:37 -0700 (PDT)\nMessage-ID: <20030712040037.46341.5F8J@football.example.com>\n\nHi.\n\nWe lost the game. Are you hungry yet?\n\nJoe.\n`\n\nfunc testCheck(t *testing.T, zones map[string]mockdns.Zone, cfg []config.Node) *Check {\n\tt.Helper()\n\tmod, err := New(container.New(), \"check.dkim\", \"\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tcheck := mod.(*Check)\n\tcheck.resolver = &mockdns.Resolver{Zones: zones}\n\tcheck.log = testutils.Logger(t, mod.Name())\n\n\tif err := check.Configure(nil, config.NewMap(nil, config.Node{Children: cfg})); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\treturn check\n}\n\nfunc TestDkimVerify_NoSig(t *testing.T) {\n\tcheck := testCheck(t, nil, nil) // No zones since this test requires no lookups.\n\n\t// Force certain reason so we can assert for it.\n\tcheck.noSigAction.Reject = true\n\tcheck.noSigAction.ReasonOverride = &exterrors.SMTPError{Code: 555}\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\n\t// The usual checking flow.\n\ts, err := check.CheckStateForMsg(ctx, &module.MsgMetadata{\n\t\tID: \"test_unsigned\",\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\ts.CheckConnection(ctx)\n\ts.CheckSender(ctx, \"joe@football.example.com\")\n\ts.CheckRcpt(ctx, \"suzie@shopping.example.net\")\n\n\thdr, buf := testutils.BodyFromStr(t, unsignedMailString)\n\tresult := s.CheckBody(ctx, hdr, buf)\n\n\tif result.Reason == nil {\n\t\tt.Fatal(\"No check fail reason set, auth. result:\", authres.Format(\"\", result.AuthResult))\n\t}\n\tif result.Reason.(*exterrors.SMTPError).Code != 555 {\n\t\tt.Fatal(\"Different fail reason:\", result.Reason)\n\t}\n}\n\nfunc TestDkimVerify_InvalidSig(t *testing.T) {\n\tcheck := testCheck(t, testZones, nil)\n\n\t// Force certain reason so we can assert for it.\n\tcheck.brokenSigAction.Reject = true\n\tcheck.brokenSigAction.ReasonOverride = &exterrors.SMTPError{Code: 555}\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\n\ts, err := check.CheckStateForMsg(ctx, &module.MsgMetadata{\n\t\tID: \"test_unsigned\",\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\ts.CheckConnection(ctx)\n\ts.CheckSender(ctx, \"joe@football.example.com\")\n\ts.CheckRcpt(ctx, \"suzie@shopping.example.net\")\n\n\thdr, buf := testutils.BodyFromStr(t, verifiedMailString)\n\t// Mess up the signature.\n\thdr.Set(\"From\", \"nope\")\n\n\tresult := s.CheckBody(ctx, hdr, buf)\n\n\tif result.Reason == nil {\n\t\tt.Fatal(\"No check fail reason set, auth. result:\", authres.Format(\"\", result.AuthResult))\n\t}\n\tif result.Reason.(*exterrors.SMTPError).Code != 555 {\n\t\tt.Fatal(\"Different fail reason:\", result.Reason)\n\t}\n}\n\nfunc TestDkimVerify_ValidSig(t *testing.T) {\n\tcheck := testCheck(t, testZones, nil)\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\n\ts, err := check.CheckStateForMsg(ctx, &module.MsgMetadata{\n\t\tID: \"test_unsigned\",\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\ts.CheckConnection(ctx)\n\ts.CheckSender(ctx, \"joe@football.example.com\")\n\ts.CheckRcpt(ctx, \"suzie@shopping.example.net\")\n\n\thdr, buf := testutils.BodyFromStr(t, verifiedMailString)\n\n\tresult := s.CheckBody(ctx, hdr, buf)\n\n\tif result.Reason != nil {\n\t\tt.Log(authres.Format(\"\", result.AuthResult))\n\t\tt.Fatal(\"Check fail reason set, auth. result:\", result.Reason, exterrors.Fields(result.Reason))\n\t}\n}\n\nfunc TestDkimVerify_RequiredFields(t *testing.T) {\n\tcheck := testCheck(t, testZones, []config.Node{\n\t\t{\n\t\t\t// Require field that is not covered by the signature.\n\t\t\tName: \"required_fields\",\n\t\t\tArgs: []string{\"From\", \"X-Important\"},\n\t\t},\n\t})\n\n\t// Force certain reason so we can assert for it.\n\tcheck.brokenSigAction.Reject = true\n\tcheck.brokenSigAction.ReasonOverride = &exterrors.SMTPError{Code: 555}\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\n\ts, err := check.CheckStateForMsg(ctx, &module.MsgMetadata{\n\t\tID: \"test_unsigned\",\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\ts.CheckConnection(ctx)\n\ts.CheckSender(ctx, \"joe@football.example.com\")\n\ts.CheckRcpt(ctx, \"suzie@shopping.example.net\")\n\n\thdr, buf := testutils.BodyFromStr(t, verifiedMailString)\n\n\tresult := s.CheckBody(ctx, hdr, buf)\n\n\tif result.Reason == nil {\n\t\tt.Fatal(\"No check fail reason set, auth. result:\", authres.Format(\"\", result.AuthResult))\n\t}\n\tif result.Reason.(*exterrors.SMTPError).Code != 555 {\n\t\tt.Fatal(\"Different fail reason:\", result.Reason)\n\t}\n}\n\nfunc TestDkimVerify_BufferOpenFail(t *testing.T) {\n\tcheck := testCheck(t, testZones, nil)\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\n\ts, err := check.CheckStateForMsg(ctx, &module.MsgMetadata{\n\t\tID: \"test_unsigned\",\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\ts.CheckConnection(ctx)\n\ts.CheckSender(ctx, \"joe@football.example.com\")\n\ts.CheckRcpt(ctx, \"suzie@shopping.example.net\")\n\n\tvar buf buffer.Buffer\n\thdr, buf := testutils.BodyFromStr(t, verifiedMailString)\n\tbuf = testutils.FailingBuffer{Blob: buf.(buffer.MemoryBuffer).Slice, OpenError: errors.New(\"No!\")}\n\n\tresult := s.CheckBody(ctx, hdr, buf)\n\tt.Log(\"auth. result:\", authres.Format(\"\", result.AuthResult))\n\n\tif result.Reason == nil {\n\t\tt.Fatal(\"No check fail reason set, auth. result:\", authres.Format(\"\", result.AuthResult))\n\t}\n}\n\nfunc TestDkimVerify_FailClosed(t *testing.T) {\n\tzones := map[string]mockdns.Zone{\n\t\t\"brisbane._domainkey.example.com.\": {\n\t\t\tErr: &net.DNSError{\n\t\t\t\tErr:         \"DNS server is not having a great time\",\n\t\t\t\tIsTemporary: true,\n\t\t\t\tIsTimeout:   true,\n\t\t\t},\n\t\t},\n\t}\n\tcheck := testCheck(t, zones, []config.Node{\n\t\t{\n\t\t\tName: \"fail_open\",\n\t\t\tArgs: []string{\"false\"},\n\t\t},\n\t})\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\n\ts, err := check.CheckStateForMsg(ctx, &module.MsgMetadata{\n\t\tID: \"test_unsigned\",\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\ts.CheckConnection(ctx)\n\ts.CheckSender(ctx, \"joe@football.example.com\")\n\ts.CheckRcpt(ctx, \"suzie@shopping.example.net\")\n\n\thdr, buf := testutils.BodyFromStr(t, verifiedMailString)\n\n\tresult := s.CheckBody(ctx, hdr, buf)\n\tt.Log(\"auth. result:\", authres.Format(\"\", result.AuthResult))\n\n\tif result.Reason == nil {\n\t\tt.Fatal(\"No check fail reason set, auth. result:\", authres.Format(\"\", result.AuthResult))\n\t}\n\tif !result.Reject {\n\t\tt.Fatal(\"No reject requested\")\n\t}\n\tif !exterrors.IsTemporary(result.Reason) {\n\t\tt.Fatal(\"Fail reason is not marked as temporary:\", result.Reason)\n\t}\n}\n\nfunc TestDkimVerify_FailOpen(t *testing.T) {\n\tzones := map[string]mockdns.Zone{\n\t\t\"brisbane._domainkey.example.com.\": {\n\t\t\tErr: &net.DNSError{\n\t\t\t\tErr:         \"DNS server is not having a great time\",\n\t\t\t\tIsTemporary: true,\n\t\t\t\tIsTimeout:   true,\n\t\t\t},\n\t\t},\n\t}\n\tcheck := testCheck(t, zones, []config.Node{\n\t\t{\n\t\t\tName: \"fail_open\",\n\t\t\tArgs: []string{\"true\"},\n\t\t},\n\t})\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\n\ts, err := check.CheckStateForMsg(ctx, &module.MsgMetadata{\n\t\tID: \"test_unsigned\",\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\ts.CheckConnection(ctx)\n\ts.CheckSender(ctx, \"joe@football.example.com\")\n\ts.CheckRcpt(ctx, \"suzie@shopping.example.net\")\n\n\thdr, buf := testutils.BodyFromStr(t, verifiedMailString)\n\n\tresult := s.CheckBody(ctx, hdr, buf)\n\n\tt.Log(\"auth. result:\", authres.Format(\"\", result.AuthResult))\n\tif result.Reason == nil {\n\t\tt.Fatal(\"No check fail reason set, auth. result:\", authres.Format(\"\", result.AuthResult))\n\t}\n\tif result.Reject {\n\t\tt.Fatal(\"Reject requested\")\n\t}\n\tif exterrors.IsTemporary(result.Reason) {\n\t\tt.Fatal(\"Fail reason is not marked as temporary:\", result.Reason)\n\t}\n\n\tif len(result.AuthResult) != 1 {\n\t\tt.Fatal(\"Wrong amount of auth. result fields:\", len(result.AuthResult))\n\t}\n\tresVal := result.AuthResult[0].(*authres.DKIMResult).Value\n\tif resVal != authres.ResultTempError {\n\t\tt.Fatal(\"Result is not temp. error:\", resVal)\n\t}\n}\n"
  },
  {
    "path": "internal/check/dns/dns.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage dns\n\nimport (\n\t\"strings\"\n\n\t\"github.com/foxcpp/maddy/framework/address\"\n\tmodconfig \"github.com/foxcpp/maddy/framework/config/module\"\n\t\"github.com/foxcpp/maddy/framework/dns\"\n\t\"github.com/foxcpp/maddy/framework/exterrors\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/internal/check\"\n)\n\nfunc requireMatchingRDNS(ctx check.StatelessCheckContext) module.CheckResult {\n\tif ctx.MsgMeta.Conn == nil {\n\t\tctx.Logger.Msg(\"locally-generated message, skipping\")\n\t\treturn module.CheckResult{}\n\t}\n\tif ctx.MsgMeta.Conn.RDNSName == nil {\n\t\tctx.Logger.Msg(\"rDNS lookup is disabled, skipping\")\n\t\treturn module.CheckResult{}\n\t}\n\n\trdnsNameI, err := ctx.MsgMeta.Conn.RDNSName.Get()\n\tif err != nil {\n\t\treason, misc := exterrors.UnwrapDNSErr(err)\n\t\treturn module.CheckResult{\n\t\t\tReason: &exterrors.SMTPError{\n\t\t\t\tCode:         exterrors.SMTPCode(err, 450, 550),\n\t\t\t\tEnhancedCode: exterrors.SMTPEnchCode(err, exterrors.EnhancedCode{0, 7, 25}),\n\t\t\t\tMessage:      \"DNS error during policy check\",\n\t\t\t\tCheckName:    \"require_matching_rdns\",\n\t\t\t\tErr:          err,\n\t\t\t\tReason:       reason,\n\t\t\t\tMisc:         misc,\n\t\t\t},\n\t\t}\n\t}\n\tif rdnsNameI == nil {\n\t\treturn module.CheckResult{\n\t\t\tReason: &exterrors.SMTPError{\n\t\t\t\tCode:         550,\n\t\t\t\tEnhancedCode: exterrors.EnhancedCode{5, 7, 25},\n\t\t\t\tMessage:      \"No PTR record found\",\n\t\t\t\tCheckName:    \"require_matching_rdns\",\n\t\t\t\tErr:          err,\n\t\t\t},\n\t\t}\n\t}\n\trdnsName := rdnsNameI.(string)\n\n\tsrcDomain := strings.TrimSuffix(ctx.MsgMeta.Conn.Hostname, \".\")\n\trdnsName = strings.TrimSuffix(rdnsName, \".\")\n\n\tif dns.Equal(rdnsName, srcDomain) {\n\t\tctx.Logger.Debugf(\"PTR record %s matches source domain, OK\", rdnsName)\n\t\treturn module.CheckResult{}\n\t}\n\n\treturn module.CheckResult{\n\t\tReason: &exterrors.SMTPError{\n\t\t\tCode:         550,\n\t\t\tEnhancedCode: exterrors.EnhancedCode{5, 7, 25},\n\t\t\tMessage:      \"rDNS name does not match source hostname\",\n\t\t\tCheckName:    \"require_matching_rdns\",\n\t\t},\n\t}\n}\n\nfunc requireMXRecord(ctx check.StatelessCheckContext, mailFrom string) module.CheckResult {\n\tif mailFrom == \"\" {\n\t\t// Permit null reverse-path for bounces.\n\t\treturn module.CheckResult{}\n\t}\n\n\t_, domain, err := address.Split(mailFrom)\n\tif err != nil {\n\t\treturn module.CheckResult{\n\t\t\tReason: &exterrors.SMTPError{\n\t\t\t\tCode:         501,\n\t\t\t\tEnhancedCode: exterrors.EnhancedCode{5, 1, 8},\n\t\t\t\tMessage:      \"Malformed sender address\",\n\t\t\t\tCheckName:    \"require_mx_record\",\n\t\t\t},\n\t\t}\n\t}\n\tif domain == \"\" {\n\t\treturn module.CheckResult{\n\t\t\tReason: &exterrors.SMTPError{\n\t\t\t\tCode:         501,\n\t\t\t\tEnhancedCode: exterrors.EnhancedCode{5, 1, 8},\n\t\t\t\tMessage:      \"No domain part in address\",\n\t\t\t\tCheckName:    \"require_mx_record\",\n\t\t\t},\n\t\t}\n\t}\n\n\tsrcMx, err := ctx.Resolver.LookupMX(ctx, domain)\n\tif err != nil {\n\t\treason, misc := exterrors.UnwrapDNSErr(err)\n\t\treturn module.CheckResult{\n\t\t\tReason: &exterrors.SMTPError{\n\t\t\t\tCode:         exterrors.SMTPCode(err, 450, 550),\n\t\t\t\tEnhancedCode: exterrors.SMTPEnchCode(err, exterrors.EnhancedCode{0, 7, 0}),\n\t\t\t\tMessage:      \"DNS error during policy check\",\n\t\t\t\tCheckName:    \"require_mx_record\",\n\t\t\t\tErr:          err,\n\t\t\t\tReason:       reason,\n\t\t\t\tMisc:         misc,\n\t\t\t},\n\t\t}\n\t}\n\n\tif len(srcMx) == 0 {\n\t\treturn module.CheckResult{\n\t\t\tReason: &exterrors.SMTPError{\n\t\t\t\tCode:         501,\n\t\t\t\tEnhancedCode: exterrors.EnhancedCode{5, 7, 27},\n\t\t\t\tMessage:      \"Domain in MAIL FROM does not have any MX records\",\n\t\t\t\tCheckName:    \"require_mx_record\",\n\t\t\t},\n\t\t}\n\t}\n\n\tfor _, mx := range srcMx {\n\t\tif mx.Host == \".\" {\n\t\t\treturn module.CheckResult{\n\t\t\t\tReason: &exterrors.SMTPError{\n\t\t\t\t\tCode:         501,\n\t\t\t\t\tEnhancedCode: exterrors.EnhancedCode{5, 7, 27},\n\t\t\t\t\tMessage:      \"Domain in MAIL FROM has null MX record\",\n\t\t\t\t\tCheckName:    \"require_mx_record\",\n\t\t\t\t},\n\t\t\t}\n\t\t}\n\t}\n\n\treturn module.CheckResult{}\n}\nfunc init() {\n\tcheck.RegisterStatelessCheck(\"require_matching_rdns\", modconfig.FailAction{Quarantine: true},\n\t\trequireMatchingRDNS, nil, nil, nil)\n\tcheck.RegisterStatelessCheck(\"require_mx_record\", modconfig.FailAction{Quarantine: true},\n\t\tnil, requireMXRecord, nil, nil)\n}\n"
  },
  {
    "path": "internal/check/dns/dns_test.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage dns\n\nimport (\n\t\"net\"\n\t\"testing\"\n\n\t\"github.com/foxcpp/go-mockdns\"\n\t\"github.com/foxcpp/maddy/framework/future\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/internal/check\"\n\t\"github.com/foxcpp/maddy/internal/testutils\"\n)\n\nfunc TestRequireMatchingRDNS(t *testing.T) {\n\ttest := func(rdns, srcHost string, fail bool) {\n\t\trdnsFut := future.New()\n\t\tvar ptr []string\n\t\tif rdns != \"\" {\n\t\t\trdnsFut.Set(rdns, nil)\n\t\t\tptr = []string{rdns}\n\t\t} else {\n\t\t\trdnsFut.Set(nil, nil)\n\t\t}\n\n\t\tres := requireMatchingRDNS(check.StatelessCheckContext{\n\t\t\tResolver: &mockdns.Resolver{\n\t\t\t\tZones: map[string]mockdns.Zone{\n\t\t\t\t\t\"4.3.2.1.in-addr.arpa.\": {\n\t\t\t\t\t\tPTR: ptr,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tMsgMeta: &module.MsgMetadata{\n\t\t\t\tConn: &module.ConnState{\n\t\t\t\t\tRemoteAddr: &net.TCPAddr{IP: net.IPv4(1, 2, 3, 4), Port: 55555},\n\t\t\t\t\tHostname:   srcHost,\n\t\t\t\t\tRDNSName:   rdnsFut,\n\t\t\t\t},\n\t\t\t},\n\t\t\tLogger: testutils.Logger(t, \"require_matching_rdns\"),\n\t\t})\n\n\t\tactualFail := res.Reason != nil\n\t\tif fail && !actualFail {\n\t\t\tt.Errorf(\"%v, %s: expected failure but check succeeded\", rdns, srcHost)\n\t\t}\n\t\tif !fail && actualFail {\n\t\t\tt.Errorf(\"%v, %s: unexpected failure\", rdns, srcHost)\n\t\t}\n\t}\n\n\ttest(\"\", \"example.org\", true)\n\ttest(\"example.org\", \"[1.2.3.4]\", true)\n\ttest(\"example.org\", \"[IPv6:beef::1]\", true)\n\ttest(\"example.org\", \"example.org\", false)\n\ttest(\"example.org.\", \"example.org\", false)\n\ttest(\"example.org\", \"example.org.\", false)\n\ttest(\"example.org.\", \"example.org.\", false)\n\ttest(\"example.com.\", \"example.org.\", true)\n}\n\nfunc TestRequireMXRecord(t *testing.T) {\n\ttest := func(mailFrom, mxDomain string, mx []net.MX, fail bool) {\n\t\tres := requireMXRecord(check.StatelessCheckContext{\n\t\t\tResolver: &mockdns.Resolver{\n\t\t\t\tZones: map[string]mockdns.Zone{\n\t\t\t\t\tmxDomain + \".\": {\n\t\t\t\t\t\tMX: mx,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tMsgMeta: &module.MsgMetadata{\n\t\t\t\tConn: &module.ConnState{\n\t\t\t\t\tRemoteAddr: &net.TCPAddr{IP: net.IPv4(1, 2, 3, 4), Port: 55555},\n\t\t\t\t},\n\t\t\t},\n\t\t\tLogger: testutils.Logger(t, \"require_mx_record\"),\n\t\t}, mailFrom)\n\n\t\tactualFail := res.Reason != nil\n\t\tif fail && !actualFail {\n\t\t\tt.Errorf(\"%v, %v: expected failure but check succeeded\", mailFrom, mx)\n\t\t}\n\t\tif !fail && actualFail {\n\t\t\tt.Errorf(\"%v, %v: unexpected failure\", mailFrom, mx)\n\t\t}\n\t}\n\n\ttest(\"foo@example.org\", \"example.org\", nil, true)\n\ttest(\"foo@example.com\", \"\", nil, true) // NXDOMAIN\n\ttest(\"foo@[1.2.3.4]\", \"\", nil, true)\n\ttest(\"[IPv6:beef::1]\", \"\", nil, true)\n\ttest(\"[IPv6:beef::1]\", \"\", nil, true)\n\ttest(\"foo@example.org\", \"example.org\", []net.MX{{Host: \"a.com\"}}, false)\n\ttest(\"foo@\", \"\", nil, true)\n\ttest(\"\", \"\", nil, false) // Permit <> for bounces.\n\ttest(\"foo@example.org\", \"example.org\", []net.MX{{Host: \".\"}}, true)\n}\n"
  },
  {
    "path": "internal/check/dnsbl/common.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage dnsbl\n\nimport (\n\t\"context\"\n\t\"net\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/foxcpp/maddy/framework/dns\"\n\t\"github.com/foxcpp/maddy/framework/exterrors\"\n)\n\ntype ListedErr struct {\n\tIdentity string\n\tList     string\n\tReason   string\n\tScore    int\n\tMessage  string\n}\n\nfunc (le ListedErr) Fields() map[string]interface{} {\n\tmsg := \"Client identity listed in the used DNSBL\"\n\tif le.Message != \"\" {\n\t\tmsg = le.Message\n\t}\n\treturn map[string]interface{}{\n\t\t\"check\":           \"dnsbl\",\n\t\t\"list\":            le.List,\n\t\t\"listed_identity\": le.Identity,\n\t\t\"reason\":          le.Reason,\n\t\t\"smtp_code\":       554,\n\t\t\"smtp_enchcode\":   exterrors.EnhancedCode{5, 7, 0},\n\t\t\"smtp_msg\":        msg,\n\t}\n}\n\nfunc (le ListedErr) Error() string {\n\treturn le.Identity + \" is listed in the used DNSBL\"\n}\n\nfunc checkDomain(ctx context.Context, resolver dns.Resolver, cfg List, domain string) error {\n\tquery := domain + \".\" + cfg.Zone\n\n\taddrs, err := resolver.LookupHost(ctx, query)\n\tif err != nil {\n\t\tif dnsErr, ok := err.(*net.DNSError); ok && dnsErr.IsNotFound {\n\t\t\treturn nil\n\t\t}\n\n\t\treturn err\n\t}\n\n\tif len(addrs) == 0 {\n\t\treturn nil\n\t}\n\n\tvar score int\n\tvar customMessage string\n\tvar filteredAddrs []string\n\n\t// If ResponseRules is configured, use new behavior\n\tif len(cfg.ResponseRules) > 0 {\n\t\t// Convert string addresses to IPAddr for matching\n\t\tipAddrs := make([]net.IPAddr, 0, len(addrs))\n\t\tfor _, addr := range addrs {\n\t\t\tif ip := net.ParseIP(addr); ip != nil {\n\t\t\t\tipAddrs = append(ipAddrs, net.IPAddr{IP: ip})\n\t\t\t}\n\t\t}\n\n\t\tmatchedScore, matchedMessages, matchedReasons, matched := matchResponseRules(ipAddrs, cfg.ResponseRules)\n\t\tif !matched {\n\t\t\treturn nil\n\t\t}\n\t\tscore = matchedScore\n\n\t\t// Use first matched message if available\n\t\tif len(matchedMessages) > 0 {\n\t\t\tcustomMessage = matchedMessages[0]\n\t\t}\n\n\t\tfilteredAddrs = matchedReasons\n\t} else {\n\t\t// Legacy behavior: accept all addresses\n\t\tfilteredAddrs = addrs\n\t}\n\n\t// Attempt to extract explanation string from TXT records (shared by both paths)\n\ttxts, err := resolver.LookupTXT(ctx, query)\n\tvar reason string\n\tif err == nil && len(txts) > 0 {\n\t\treason = strings.Join(txts, \"; \")\n\t} else {\n\t\t// Not significant, include addresses as reason. Usually they are\n\t\t// mapped to some predefined 'reasons' by BL.\n\t\treason = strings.Join(filteredAddrs, \"; \")\n\t}\n\n\treturn ListedErr{\n\t\tIdentity: domain,\n\t\tList:     cfg.Zone,\n\t\tReason:   reason,\n\t\tScore:    score,\n\t\tMessage:  customMessage,\n\t}\n}\n\nfunc matchResponseRules(addrs []net.IPAddr, rules []ResponseRule) (score int, messages []string, reasons []string, matched bool) {\n\t// Track which rules have been matched to avoid counting the same rule multiple times\n\tmatchedRules := make(map[int]bool)\n\n\tfor _, addr := range addrs {\n\t\tfor ruleIdx, rule := range rules {\n\t\t\t// Skip if this rule has already been matched\n\t\t\tif matchedRules[ruleIdx] {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfor _, respNet := range rule.Networks {\n\t\t\t\tif respNet.Contains(addr.IP) {\n\t\t\t\t\tscore += rule.Score\n\t\t\t\t\tif rule.Message != \"\" {\n\t\t\t\t\t\tmessages = append(messages, rule.Message)\n\t\t\t\t\t}\n\t\t\t\t\treasons = append(reasons, addr.IP.String())\n\t\t\t\t\tmatchedRules[ruleIdx] = true\n\t\t\t\t\tmatched = true\n\t\t\t\t\tbreak // Move to next rule\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc checkIP(ctx context.Context, resolver dns.Resolver, cfg List, ip net.IP) error {\n\tipv6 := true\n\tif ipv4 := ip.To4(); ipv4 != nil {\n\t\tip = ipv4\n\t\tipv6 = false\n\t}\n\n\tif ipv6 && !cfg.ClientIPv6 {\n\t\treturn nil\n\t}\n\tif !ipv6 && !cfg.ClientIPv4 {\n\t\treturn nil\n\t}\n\n\tquery := queryString(ip) + \".\" + cfg.Zone\n\n\taddrs, err := resolver.LookupIPAddr(ctx, query)\n\tif err != nil {\n\t\tif dnsErr, ok := err.(*net.DNSError); ok && dnsErr.IsNotFound {\n\t\t\treturn nil\n\t\t}\n\n\t\treturn err\n\t}\n\n\tvar filteredAddrs []net.IPAddr\n\tvar score int\n\tvar customMessage string\n\n\t// If ResponseRules is configured, use new behavior\n\tif len(cfg.ResponseRules) > 0 {\n\t\tmatchedScore, matchedMessages, matchedReasons, matched := matchResponseRules(addrs, cfg.ResponseRules)\n\t\tif !matched {\n\t\t\treturn nil\n\t\t}\n\t\tscore = matchedScore\n\n\t\t// Use first matched message if available\n\t\tif len(matchedMessages) > 0 {\n\t\t\tcustomMessage = matchedMessages[0]\n\t\t}\n\n\t\t// Build filteredAddrs from matched reasons for TXT lookup fallback\n\t\tfor _, reason := range matchedReasons {\n\t\t\tfilteredAddrs = append(filteredAddrs, net.IPAddr{IP: net.ParseIP(reason)})\n\t\t}\n\t} else {\n\t\t// Legacy behavior: use flat Responses filter\n\t\tfilteredAddrs = make([]net.IPAddr, 0, len(addrs))\n\taddrsLoop:\n\t\tfor _, addr := range addrs {\n\t\t\t// No responses whitelist configured - permit all.\n\t\t\tif len(cfg.Responses) == 0 {\n\t\t\t\tfilteredAddrs = append(filteredAddrs, addr)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfor _, respNet := range cfg.Responses {\n\t\t\t\tif respNet.Contains(addr.IP) {\n\t\t\t\t\tfilteredAddrs = append(filteredAddrs, addr)\n\t\t\t\t\tcontinue addrsLoop\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif len(filteredAddrs) == 0 {\n\t\t\treturn nil\n\t\t}\n\t}\n\n\t// Attempt to extract explanation string from TXT records (shared by both paths)\n\ttxts, err := resolver.LookupTXT(ctx, query)\n\tvar reason string\n\tif err == nil && len(txts) > 0 {\n\t\treason = strings.Join(txts, \"; \")\n\t} else {\n\t\t// Not significant, include addresses as reason. Usually they are\n\t\t// mapped to some predefined 'reasons' by BL.\n\t\treasonParts := make([]string, 0, len(filteredAddrs))\n\t\tfor _, addr := range filteredAddrs {\n\t\t\treasonParts = append(reasonParts, addr.IP.String())\n\t\t}\n\t\treason = strings.Join(reasonParts, \"; \")\n\t}\n\n\treturn ListedErr{\n\t\tIdentity: ip.String(),\n\t\tList:     cfg.Zone,\n\t\tReason:   reason,\n\t\tScore:    score,\n\t\tMessage:  customMessage,\n\t}\n}\n\nfunc queryString(ip net.IP) string {\n\tipv6 := true\n\tif ipv4 := ip.To4(); ipv4 != nil {\n\t\tip = ipv4\n\t\tipv6 = false\n\t}\n\n\tres := strings.Builder{}\n\tif ipv6 {\n\t\tres.Grow(63) // 0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0\n\t} else {\n\t\tres.Grow(15) // 000.000.000.000\n\t}\n\n\tfor i := len(ip) - 1; i >= 0; i-- {\n\t\toctet := ip[i]\n\n\t\tif ipv6 {\n\t\t\t// X.X\n\t\t\tres.WriteString(strconv.FormatInt(int64(octet&0xf), 16))\n\t\t\tres.WriteRune('.')\n\t\t\tres.WriteString(strconv.FormatInt(int64((octet&0xf0)>>4), 16))\n\t\t} else {\n\t\t\t// X\n\t\t\tres.WriteString(strconv.Itoa(int(octet)))\n\t\t}\n\n\t\tif i != 0 {\n\t\t\tres.WriteRune('.')\n\t\t}\n\t}\n\treturn res.String()\n}\n"
  },
  {
    "path": "internal/check/dnsbl/common_test.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage dnsbl\n\nimport (\n\t\"context\"\n\t\"net\"\n\t\"reflect\"\n\t\"testing\"\n\n\t\"github.com/foxcpp/go-mockdns\"\n)\n\nfunc TestQueryString(t *testing.T) {\n\ttest := func(ip, queryStr string) {\n\t\tt.Helper()\n\n\t\tparsed := net.ParseIP(ip)\n\t\tif parsed == nil {\n\t\t\tpanic(\"Malformed IP in test\")\n\t\t}\n\n\t\tactual := queryString(parsed)\n\t\tif actual != queryStr {\n\t\t\tt.Errorf(\"want queryString(%s) to be %s, got %s\", ip, queryStr, actual)\n\t\t}\n\t}\n\n\ttest(\"2001:db8:1:2:3:4:567:89ab\", \"b.a.9.8.7.6.5.0.4.0.0.0.3.0.0.0.2.0.0.0.1.0.0.0.8.b.d.0.1.0.0.2\")\n\ttest(\"2001::1:2:3:4:567:89ab\", \"b.a.9.8.7.6.5.0.4.0.0.0.3.0.0.0.2.0.0.0.1.0.0.0.0.0.0.0.1.0.0.2\")\n\ttest(\"192.0.2.99\", \"99.2.0.192\")\n}\n\nfunc TestCheckDomain(t *testing.T) {\n\ttest := func(zones map[string]mockdns.Zone, cfg List, domain string, expectedErr error) {\n\t\tt.Helper()\n\t\tresolver := mockdns.Resolver{Zones: zones}\n\t\terr := checkDomain(context.Background(), &resolver, cfg, domain)\n\t\tif !reflect.DeepEqual(err, expectedErr) {\n\t\t\tt.Errorf(\"expected err to be '%#v', got '%#v'\", expectedErr, err)\n\t\t}\n\t}\n\n\ttest(nil, List{Zone: \"example.org\"}, \"example.com\", nil)\n\ttest(map[string]mockdns.Zone{\n\t\t\"example.com.example.org.\": {\n\t\t\tErr: &net.DNSError{\n\t\t\t\tErr:         \"i/o timeout\",\n\t\t\t\tIsTimeout:   true,\n\t\t\t\tIsTemporary: true,\n\t\t\t},\n\t\t},\n\t}, List{Zone: \"example.org\"}, \"example.com\", &net.DNSError{\n\t\tErr:         \"i/o timeout\",\n\t\tIsTimeout:   true,\n\t\tIsTemporary: true,\n\t})\n\ttest(map[string]mockdns.Zone{\n\t\t\"example.com.example.org.\": {\n\t\t\tA: []string{\"127.0.0.1\"},\n\t\t},\n\t}, List{Zone: \"example.org\"}, \"example.com\", ListedErr{\n\t\tIdentity: \"example.com\",\n\t\tList:     \"example.org\",\n\t\tReason:   \"127.0.0.1\",\n\t})\n\ttest(map[string]mockdns.Zone{\n\t\t\"example.org.example.com.\": {\n\t\t\tA: []string{\"127.0.0.1\"},\n\t\t},\n\t}, List{Zone: \"example.org\"}, \"example.com\", nil)\n\ttest(map[string]mockdns.Zone{\n\t\t\"example.com.example.org.\": {\n\t\t\tA:   []string{\"127.0.0.1\"},\n\t\t\tTXT: []string{\"Reason\"},\n\t\t},\n\t}, List{Zone: \"example.org\"}, \"example.com\", ListedErr{\n\t\tIdentity: \"example.com\",\n\t\tList:     \"example.org\",\n\t\tReason:   \"Reason\",\n\t})\n\ttest(map[string]mockdns.Zone{\n\t\t\"example.com.example.org.\": {\n\t\t\tA:   []string{\"127.0.0.1\"},\n\t\t\tTXT: []string{\"Reason 1\", \"Reason 2\"},\n\t\t},\n\t}, List{Zone: \"example.org\"}, \"example.com\", ListedErr{\n\t\tIdentity: \"example.com\",\n\t\tList:     \"example.org\",\n\t\tReason:   \"Reason 1; Reason 2\",\n\t})\n\ttest(map[string]mockdns.Zone{\n\t\t\"example.com.example.org.\": {\n\t\t\tA: []string{\"127.0.0.1\", \"127.0.0.2\"},\n\t\t},\n\t}, List{Zone: \"example.org\"}, \"example.com\", ListedErr{\n\t\tIdentity: \"example.com\",\n\t\tList:     \"example.org\",\n\t\tReason:   \"127.0.0.1; 127.0.0.2\",\n\t})\n}\n\nfunc TestCheckIP(t *testing.T) {\n\ttest := func(zones map[string]mockdns.Zone, cfg List, ip net.IP, expectedErr error) {\n\t\tt.Helper()\n\t\tresolver := mockdns.Resolver{Zones: zones}\n\t\terr := checkIP(context.Background(), &resolver, cfg, ip)\n\t\tif !reflect.DeepEqual(err, expectedErr) {\n\t\t\tt.Errorf(\"expected err to be '%#v', got '%#v'\", expectedErr, err)\n\t\t}\n\t}\n\n\ttest(nil, List{Zone: \"example.org\"}, net.IPv4(1, 2, 3, 4), nil)\n\ttest(nil, List{Zone: \"example.org\", ClientIPv4: true}, net.IPv4(1, 2, 3, 4), nil)\n\ttest(map[string]mockdns.Zone{\n\t\t\"4.3.2.1.example.org.\": {\n\t\t\tA: []string{\"127.0.0.1\"},\n\t\t},\n\t}, List{Zone: \"example.org\", ClientIPv4: true}, net.IPv4(1, 2, 3, 4), ListedErr{\n\t\tIdentity: \"1.2.3.4\",\n\t\tList:     \"example.org\",\n\t\tReason:   \"127.0.0.1\",\n\t})\n\ttest(map[string]mockdns.Zone{\n\t\t\"4.3.2.1.example.org.\": {\n\t\t\tA: []string{\"128.0.0.1\"},\n\t\t},\n\t}, List{\n\t\tZone:       \"example.org\",\n\t\tClientIPv4: true,\n\t\tResponses: []net.IPNet{\n\t\t\t{\n\t\t\t\tIP:   net.IPv4(127, 0, 0, 1),\n\t\t\t\tMask: net.IPv4Mask(255, 255, 255, 0),\n\t\t\t},\n\t\t},\n\t}, net.IPv4(1, 2, 3, 4), nil)\n\ttest(map[string]mockdns.Zone{\n\t\t\"4.3.2.1.example.org.\": {\n\t\t\tA: []string{\"128.0.0.1\"},\n\t\t},\n\t}, List{\n\t\tZone:       \"example.org\",\n\t\tClientIPv4: true,\n\t\tResponses: []net.IPNet{\n\t\t\t{\n\t\t\t\tIP:   net.IPv4(127, 0, 0, 0),\n\t\t\t\tMask: net.IPv4Mask(255, 255, 255, 0),\n\t\t\t},\n\t\t\t{\n\t\t\t\tIP:   net.IPv4(128, 0, 0, 0),\n\t\t\t\tMask: net.IPv4Mask(255, 255, 255, 0),\n\t\t\t},\n\t\t},\n\t}, net.IPv4(1, 2, 3, 4), ListedErr{\n\t\tIdentity: \"1.2.3.4\",\n\t\tList:     \"example.org\",\n\t\tReason:   \"128.0.0.1\",\n\t})\n\ttest(map[string]mockdns.Zone{\n\t\t\"4.3.2.1.example.org.\": {\n\t\t\tA: []string{\"127.0.0.1\"},\n\t\t},\n\t}, List{Zone: \"example.org\"}, net.IPv4(1, 2, 3, 4), nil)\n\ttest(map[string]mockdns.Zone{\n\t\t\"4.3.2.1.example.org.\": {\n\t\t\tErr: &net.DNSError{\n\t\t\t\tErr:         \"i/o timeout\",\n\t\t\t\tIsTimeout:   true,\n\t\t\t\tIsTemporary: true,\n\t\t\t},\n\t\t},\n\t}, List{Zone: \"example.org\", ClientIPv4: true}, net.IPv4(1, 2, 3, 4), &net.DNSError{\n\t\tErr:         \"i/o timeout\",\n\t\tIsTimeout:   true,\n\t\tIsTemporary: true,\n\t})\n\n\ttest(map[string]mockdns.Zone{\n\t\t\"4.3.2.1.example.org.\": {\n\t\t\tA:   []string{\"127.0.0.1\"},\n\t\t\tTXT: []string{\"Reason\"},\n\t\t},\n\t}, List{Zone: \"example.org\", ClientIPv4: true}, net.IPv4(1, 2, 3, 4), ListedErr{\n\t\tIdentity: \"1.2.3.4\",\n\t\tList:     \"example.org\",\n\t\tReason:   \"Reason\",\n\t})\n\ttest(map[string]mockdns.Zone{\n\t\t\"4.3.2.1.example.org.\": {\n\t\t\tA: []string{\"127.0.0.1\", \"127.0.0.2\"},\n\t\t},\n\t}, List{Zone: \"example.org\", ClientIPv4: true}, net.IPv4(1, 2, 3, 4), ListedErr{\n\t\tIdentity: \"1.2.3.4\",\n\t\tList:     \"example.org\",\n\t\tReason:   \"127.0.0.1; 127.0.0.2\",\n\t})\n\ttest(map[string]mockdns.Zone{\n\t\t\"4.3.2.1.example.org.\": {\n\t\t\tA:   []string{\"127.0.0.1\", \"127.0.0.2\"},\n\t\t\tTXT: []string{\"Reason\", \"Reason 2\"},\n\t\t},\n\t}, List{Zone: \"example.org\", ClientIPv4: true}, net.IPv4(1, 2, 3, 4), ListedErr{\n\t\tIdentity: \"1.2.3.4\",\n\t\tList:     \"example.org\",\n\t\tReason:   \"Reason; Reason 2\",\n\t})\n\ttest(map[string]mockdns.Zone{\n\t\t\"b.a.9.8.7.6.5.0.4.0.0.0.3.0.0.0.2.0.0.0.1.0.0.0.8.b.d.0.1.0.0.2.example.org.\": {\n\t\t\tA: []string{\"127.0.0.1\"},\n\t\t},\n\t}, List{Zone: \"example.org\", ClientIPv4: true}, net.ParseIP(\"2001:db8:1:2:3:4:567:89ab\"), nil)\n\ttest(map[string]mockdns.Zone{\n\t\t\"b.a.9.8.7.6.5.0.4.0.0.0.3.0.0.0.2.0.0.0.1.0.0.0.8.b.d.0.1.0.0.2.example.org.\": {\n\t\t\tA: []string{\"127.0.0.1\"},\n\t\t},\n\t}, List{Zone: \"example.org\", ClientIPv6: true}, net.ParseIP(\"2001:db8:1:2:3:4:567:89ab\"), ListedErr{\n\t\tIdentity: \"2001:db8:1:2:3:4:567:89ab\",\n\t\tList:     \"example.org\",\n\t\tReason:   \"127.0.0.1\",\n\t})\n}\n\nfunc TestCheckDomainWithResponseRules(t *testing.T) {\n\ttest := func(zones map[string]mockdns.Zone, cfg List, domain string, expectedErr error) {\n\t\tt.Helper()\n\t\tresolver := mockdns.Resolver{Zones: zones}\n\t\terr := checkDomain(context.Background(), &resolver, cfg, domain)\n\t\tif expectedErr == nil {\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"expected no error, got '%#v'\", err)\n\t\t\t}\n\t\t} else {\n\t\t\tif err == nil {\n\t\t\t\tt.Errorf(\"expected err to be '%#v', got nil\", expectedErr)\n\t\t\t} else {\n\t\t\t\texpectedLE, okExpected := expectedErr.(ListedErr)\n\t\t\t\tactualLE, okActual := err.(ListedErr)\n\t\t\t\tif !okExpected || !okActual {\n\t\t\t\t\tt.Errorf(\"expected err to be '%#v', got '%#v'\", expectedErr, err)\n\t\t\t\t} else {\n\t\t\t\t\tif expectedLE.Identity != actualLE.Identity ||\n\t\t\t\t\t\texpectedLE.List != actualLE.List ||\n\t\t\t\t\t\texpectedLE.Score != actualLE.Score ||\n\t\t\t\t\t\texpectedLE.Message != actualLE.Message {\n\t\t\t\t\t\tt.Errorf(\"expected err to be '%#v', got '%#v'\", expectedErr, err)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Test domain with single response code and custom message\n\ttest(map[string]mockdns.Zone{\n\t\t\"spam.example.com.dnsbl.example.org.\": {\n\t\t\tA: []string{\"127.0.0.2\"},\n\t\t},\n\t}, List{\n\t\tZone: \"dnsbl.example.org\",\n\t\tResponseRules: []ResponseRule{\n\t\t\t{\n\t\t\t\tNetworks: []net.IPNet{\n\t\t\t\t\t{IP: net.IPv4(127, 0, 0, 2), Mask: net.IPv4Mask(255, 255, 255, 255)},\n\t\t\t\t},\n\t\t\t\tScore:   10,\n\t\t\t\tMessage: \"Domain listed as spam source\",\n\t\t\t},\n\t\t},\n\t}, \"spam.example.com\", ListedErr{\n\t\tIdentity: \"spam.example.com\",\n\t\tList:     \"dnsbl.example.org\",\n\t\tScore:    10,\n\t\tMessage:  \"Domain listed as spam source\",\n\t})\n\n\t// Test domain with multiple response codes - scores should sum\n\ttest(map[string]mockdns.Zone{\n\t\t\"multi.example.com.dnsbl.example.org.\": {\n\t\t\tA: []string{\"127.0.0.2\", \"127.0.0.11\"},\n\t\t},\n\t}, List{\n\t\tZone: \"dnsbl.example.org\",\n\t\tResponseRules: []ResponseRule{\n\t\t\t{\n\t\t\t\tNetworks: []net.IPNet{\n\t\t\t\t\t{IP: net.IPv4(127, 0, 0, 2), Mask: net.IPv4Mask(255, 255, 255, 255)},\n\t\t\t\t},\n\t\t\t\tScore:   10,\n\t\t\t\tMessage: \"High severity\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tNetworks: []net.IPNet{\n\t\t\t\t\t{IP: net.IPv4(127, 0, 0, 11), Mask: net.IPv4Mask(255, 255, 255, 255)},\n\t\t\t\t},\n\t\t\t\tScore:   5,\n\t\t\t\tMessage: \"Low severity\",\n\t\t\t},\n\t\t},\n\t}, \"multi.example.com\", ListedErr{\n\t\tIdentity: \"multi.example.com\",\n\t\tList:     \"dnsbl.example.org\",\n\t\tScore:    15, // 10 + 5\n\t\tMessage:  \"High severity\",\n\t})\n\n\t// Test domain with no matching response codes\n\ttest(map[string]mockdns.Zone{\n\t\t\"unknown.example.com.dnsbl.example.org.\": {\n\t\t\tA: []string{\"127.0.0.99\"},\n\t\t},\n\t}, List{\n\t\tZone: \"dnsbl.example.org\",\n\t\tResponseRules: []ResponseRule{\n\t\t\t{\n\t\t\t\tNetworks: []net.IPNet{\n\t\t\t\t\t{IP: net.IPv4(127, 0, 0, 2), Mask: net.IPv4Mask(255, 255, 255, 255)},\n\t\t\t\t},\n\t\t\t\tScore:   10,\n\t\t\t\tMessage: \"Listed\",\n\t\t\t},\n\t\t},\n\t}, \"unknown.example.com\", nil)\n}\n"
  },
  {
    "path": "internal/check/dnsbl/dnsbl.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage dnsbl\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"net\"\n\t\"runtime/trace\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/emersion/go-message/textproto\"\n\t\"github.com/foxcpp/maddy/framework/address\"\n\t\"github.com/foxcpp/maddy/framework/buffer\"\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/dns\"\n\t\"github.com/foxcpp/maddy/framework/exterrors\"\n\t\"github.com/foxcpp/maddy/framework/log\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n\t\"github.com/foxcpp/maddy/internal/target\"\n\t\"golang.org/x/sync/errgroup\"\n)\n\ntype ResponseRule struct {\n\tNetworks []net.IPNet\n\tScore    int\n\tMessage  string\n}\n\ntype List struct {\n\tZone string\n\n\tClientIPv4 bool\n\tClientIPv6 bool\n\n\tEHLO     bool\n\tMAILFROM bool\n\n\tScoreAdj  int\n\tResponses []net.IPNet\n\n\tResponseRules []ResponseRule\n}\n\nvar defaultBL = List{\n\tClientIPv4: true,\n}\n\ntype DNSBL struct {\n\tinstName   string\n\tcheckEarly bool\n\tbls        []List\n\n\tquarantineThres int\n\trejectThres     int\n\n\tresolver dns.Resolver\n\tlog      *log.Logger\n}\n\nfunc New(c *container.C, modName, instName string) (module.Module, error) {\n\treturn &DNSBL{\n\t\tinstName: instName,\n\n\t\tresolver: dns.DefaultResolver(),\n\t\tlog:      c.DefaultLogger.Sublogger(modName),\n\t}, nil\n}\n\nfunc (bl *DNSBL) Name() string {\n\treturn \"dnsbl\"\n}\n\nfunc (bl *DNSBL) InstanceName() string {\n\treturn bl.instName\n}\n\nfunc (bl *DNSBL) Configure(inlineArgs []string, cfg *config.Map) error {\n\tcfg.Bool(\"debug\", false, false, &bl.log.Debug)\n\tcfg.Bool(\"check_early\", false, false, &bl.checkEarly)\n\tcfg.Int(\"quarantine_threshold\", false, false, 1, &bl.quarantineThres)\n\tcfg.Int(\"reject_threshold\", false, false, 9999, &bl.rejectThres)\n\tcfg.AllowUnknown()\n\tunknown, err := cfg.Process()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor _, inlineBl := range inlineArgs {\n\t\tcfg := defaultBL\n\t\tcfg.Zone = inlineBl\n\t\tgo bl.testList(cfg)\n\t\tbl.bls = append(bl.bls, cfg)\n\t}\n\n\tfor _, node := range unknown {\n\t\tif err := bl.readListCfg(node); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (bl *DNSBL) readListCfg(node config.Node) error {\n\tvar (\n\t\tlistCfg      List\n\t\tresponseNets []string\n\t)\n\n\tcfg := config.NewMap(nil, node)\n\tcfg.Bool(\"client_ipv4\", false, defaultBL.ClientIPv4, &listCfg.ClientIPv4)\n\tcfg.Bool(\"client_ipv6\", false, defaultBL.ClientIPv4, &listCfg.ClientIPv6)\n\tcfg.Bool(\"ehlo\", false, defaultBL.EHLO, &listCfg.EHLO)\n\tcfg.Bool(\"mailfrom\", false, defaultBL.EHLO, &listCfg.MAILFROM)\n\tcfg.Int(\"score\", false, false, 1, &listCfg.ScoreAdj)\n\tcfg.StringList(\"responses\", false, false, []string{\"127.0.0.1/24\"}, &responseNets)\n\tcfg.Callback(\"response\", func(_ *config.Map, node config.Node) error {\n\t\trule, err := parseResponseRule(node)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tlistCfg.ResponseRules = append(listCfg.ResponseRules, rule)\n\t\treturn nil\n\t})\n\tif _, err := cfg.Process(); err != nil {\n\t\treturn err\n\t}\n\n\tfor _, resp := range responseNets {\n\t\t// If there is no / - it is a plain IP address, append\n\t\t// '/32'.\n\t\tif !strings.Contains(resp, \"/\") {\n\t\t\tresp += \"/32\"\n\t\t}\n\n\t\t_, ipNet, err := net.ParseCIDR(resp)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tlistCfg.Responses = append(listCfg.Responses, *ipNet)\n\t}\n\n\t// Warn if both response and responses are configured\n\tif len(listCfg.ResponseRules) > 0 && len(responseNets) > 0 {\n\t\tbl.log.Msg(\"both 'response' blocks and 'responses' directive are specified, 'response' blocks take precedence\", \"list\", node.Name)\n\t}\n\n\tfor _, zone := range append([]string{node.Name}, node.Args...) {\n\t\tzoneCfg := listCfg\n\t\tzoneCfg.Zone = zone\n\n\t\tif listCfg.ScoreAdj < 0 {\n\t\t\tif zoneCfg.EHLO {\n\t\t\t\treturn errors.New(\"dnsbl: 'ehlo' should not be used with negative score\")\n\t\t\t}\n\t\t\tif zoneCfg.MAILFROM {\n\t\t\t\treturn errors.New(\"dnsbl: 'mailfrom' should not be used with negative score\")\n\t\t\t}\n\t\t}\n\t\tbl.bls = append(bl.bls, zoneCfg)\n\n\t\t// From RFC 5782 Section 7:\n\t\t// >To avoid this situation, systems that use\n\t\t// >DNSxLs SHOULD check for the test entries described in Section 5 to\n\t\t// >ensure that a domain actually has the structure of a DNSxL, and\n\t\t// >SHOULD NOT use any DNSxL domain that does not have correct test\n\t\t// >entries.\n\t\t// Sadly, however, many DNSBLs lack test records so at most we can\n\t\t// log a warning. Also, DNS is kinda slow so we do checks\n\t\t// asynchronously to prevent slowing down server start-up.\n\t\tgo bl.testList(zoneCfg)\n\t}\n\n\treturn nil\n}\n\nfunc parseResponseRule(node config.Node) (ResponseRule, error) {\n\tvar rule ResponseRule\n\n\tif len(node.Args) == 0 {\n\t\treturn rule, config.NodeErr(node, \"response block requires at least one IP address or CIDR as argument\")\n\t}\n\n\t// Parse IP addresses/CIDRs from arguments\n\tfor _, arg := range node.Args {\n\t\t// If there is no / - it is a plain IP address, append '/32' or '/128'\n\t\tresp := arg\n\t\tif !strings.Contains(resp, \"/\") {\n\t\t\t// Check if it's IPv6 to determine the mask\n\t\t\tif strings.Contains(resp, \":\") {\n\t\t\t\tresp += \"/128\"\n\t\t\t} else {\n\t\t\t\tresp += \"/32\"\n\t\t\t}\n\t\t}\n\n\t\t_, ipNet, err := net.ParseCIDR(resp)\n\t\tif err != nil {\n\t\t\treturn rule, config.NodeErr(node, \"invalid IP address or CIDR: %s: %v\", arg, err)\n\t\t}\n\t\trule.Networks = append(rule.Networks, *ipNet)\n\t}\n\n\t// Parse directives within the response block\n\tcfg := config.NewMap(nil, node)\n\tcfg.Int(\"score\", false, true, 0, &rule.Score)\n\tcfg.String(\"message\", false, false, \"\", &rule.Message)\n\tif _, err := cfg.Process(); err != nil {\n\t\treturn rule, err\n\t}\n\n\treturn rule, nil\n}\n\nfunc (bl *DNSBL) testList(listCfg List) {\n\t// Check RFC 5782 Section 5 requirements.\n\n\tbl.log.DebugMsg(\"testing list for RFC 5782 requirements...\", \"list\", listCfg.Zone)\n\n\t// 1. IPv4-based DNSxLs MUST contain an entry for 127.0.0.2 for testing purposes.\n\tif listCfg.ClientIPv4 {\n\t\terr := checkIP(context.Background(), bl.resolver, listCfg, net.IPv4(127, 0, 0, 2))\n\t\tif err == nil {\n\t\t\tbl.log.Msg(\"List does not contain a test record for 127.0.0.2\", \"list\", listCfg.Zone)\n\t\t} else if _, ok := err.(ListedErr); !ok {\n\t\t\tbl.log.Error(\"lookup error, bailing out\", err, \"list\", listCfg.Zone)\n\t\t\treturn\n\t\t}\n\n\t\t// 2. IPv4-based DNSxLs MUST NOT contain an entry for 127.0.0.1.\n\t\terr = checkIP(context.Background(), bl.resolver, listCfg, net.IPv4(127, 0, 0, 1))\n\t\tif err != nil {\n\t\t\t_, ok := err.(ListedErr)\n\t\t\tif !ok {\n\t\t\t\tbl.log.Error(\"lookup error, bailing out\", err, \"list\", listCfg.Zone)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tbl.log.Msg(\"List contains a record for 127.0.0.1\", \"list\", listCfg.Zone)\n\t\t}\n\t}\n\n\tif listCfg.ClientIPv6 {\n\t\t// 1. IPv6-based DNSxLs MUST contain an entry for ::FFFF:7F00:2\n\t\tmustIP := net.ParseIP(\"::FFFF:7F00:2\")\n\n\t\terr := checkIP(context.Background(), bl.resolver, listCfg, mustIP)\n\t\tif err == nil {\n\t\t\tbl.log.Msg(\"List does not contain a test record for ::FFFF:7F00:2\", \"list\", listCfg.Zone)\n\t\t} else if _, ok := err.(ListedErr); !ok {\n\t\t\tbl.log.Error(\"lookup error, bailing out\", err, \"list\", listCfg.Zone)\n\t\t\treturn\n\t\t}\n\n\t\t// 2. IPv4-based DNSxLs MUST NOT contain an entry for ::FFFF:7F00:1\n\t\tmustNotIP := net.ParseIP(\"::FFFF:7F00:1\")\n\t\terr = checkIP(context.Background(), bl.resolver, listCfg, mustNotIP)\n\t\tif err != nil {\n\t\t\t_, ok := err.(ListedErr)\n\t\t\tif !ok {\n\t\t\t\tbl.log.Error(\"lookup error, bailing out\", err, \"list\", listCfg.Zone)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tbl.log.Msg(\"List contains a record for ::FFFF:7F00:1\", \"list\", listCfg.Zone)\n\t\t}\n\t}\n\n\tif listCfg.EHLO || listCfg.MAILFROM {\n\t\t// Domain-name-based DNSxLs MUST contain an entry for the reserved\n\t\t// domain name \"TEST\".\n\t\terr := checkDomain(context.Background(), bl.resolver, listCfg, \"test\")\n\t\tif err == nil {\n\t\t\tbl.log.Msg(\"List does not contain a test record for 'test' TLD\", \"list\", listCfg.Zone)\n\t\t} else if _, ok := err.(ListedErr); !ok {\n\t\t\tbl.log.Error(\"lookup error, bailing out\", err, \"list\", listCfg.Zone)\n\t\t\treturn\n\t\t}\n\n\t\t// ... and MUST NOT contain an entry for the reserved domain name\n\t\t// \"INVALID\".\n\t\terr = checkDomain(context.Background(), bl.resolver, listCfg, \"invalid\")\n\t\tif err != nil {\n\t\t\t_, ok := err.(ListedErr)\n\t\t\tif !ok {\n\t\t\t\tbl.log.Error(\"lookup error, bailing out\", err, \"list\", listCfg.Zone)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tbl.log.Msg(\"List contains a record for 'invalid' TLD\", \"list\", listCfg.Zone)\n\t\t}\n\t}\n}\n\nfunc (bl *DNSBL) checkList(ctx context.Context, list List, ip net.IP, ehlo, mailFrom string) error {\n\tif list.ClientIPv4 || list.ClientIPv6 {\n\t\tif err := checkIP(ctx, bl.resolver, list, ip); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif list.EHLO && ehlo != \"\" {\n\t\t// Skip IPs in EHLO.\n\t\tif strings.HasPrefix(ehlo, \"[\") && strings.HasSuffix(ehlo, \"]\") {\n\t\t\treturn nil\n\t\t}\n\n\t\tif err := checkDomain(ctx, bl.resolver, list, ehlo); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif list.MAILFROM && mailFrom != \"\" {\n\t\t_, domain, err := address.Split(mailFrom)\n\t\tif err != nil || domain == \"\" {\n\t\t\t// Probably <postmaster> or <>, not much we can check.\n\t\t\treturn nil\n\t\t}\n\n\t\t// If EHLO == domain (usually the case for small/private email servers)\n\t\t// then don't do a second lookup for the same domain.\n\t\tif list.EHLO && dns.Equal(domain, ehlo) {\n\t\t\treturn nil\n\t\t}\n\n\t\tif err := checkDomain(ctx, bl.resolver, list, domain); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (bl *DNSBL) checkLists(ctx context.Context, ip net.IP, ehlo, mailFrom string) module.CheckResult {\n\tvar (\n\t\teg = errgroup.Group{}\n\n\t\t// Protects variables below.\n\t\tlck      sync.Mutex\n\t\tscore    int\n\t\tlistedOn []string\n\t\treasons  []string\n\t\tmessages []string\n\t)\n\n\tfor _, list := range bl.bls {\n\t\teg.Go(func() error {\n\t\t\terr := bl.checkList(ctx, list, ip, ehlo, mailFrom)\n\t\t\tif err != nil {\n\t\t\t\tlistErr, listed := err.(ListedErr)\n\t\t\t\tif !listed {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tlck.Lock()\n\t\t\t\tdefer lck.Unlock()\n\t\t\t\tlistedOn = append(listedOn, listErr.List)\n\t\t\t\treasons = append(reasons, listErr.Reason)\n\n\t\t\t\t// Use score from ListedErr if set (new behavior), otherwise use legacy ScoreAdj\n\t\t\t\tif listErr.Score != 0 {\n\t\t\t\t\tscore += listErr.Score\n\t\t\t\t} else {\n\t\t\t\t\tscore += list.ScoreAdj\n\t\t\t\t}\n\n\t\t\t\t// Collect custom messages if available\n\t\t\t\tif listErr.Message != \"\" {\n\t\t\t\t\tmessages = append(messages, listErr.Message)\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn nil\n\t\t})\n\t}\n\n\terr := eg.Wait()\n\tif err != nil {\n\t\t// Lookup error for BL, hard-fail.\n\t\treturn module.CheckResult{\n\t\t\tReject: true,\n\t\t\tReason: &exterrors.SMTPError{\n\t\t\t\tCode:         exterrors.SMTPCode(err, 451, 554),\n\t\t\t\tEnhancedCode: exterrors.SMTPEnchCode(err, exterrors.EnhancedCode{0, 7, 0}),\n\t\t\t\tMessage:      \"DNS error during policy check\",\n\t\t\t\tErr:          err,\n\t\t\t\tCheckName:    \"dnsbl\",\n\t\t\t},\n\t\t}\n\t}\n\n\t// Use custom message if available, otherwise use default\n\tmessage := \"Client identity is listed in the used DNSBL\"\n\tif len(messages) > 0 {\n\t\tmessage = strings.Join(messages, \"; \")\n\t}\n\n\tif score >= bl.rejectThres {\n\t\treturn module.CheckResult{\n\t\t\tReject: true,\n\t\t\tReason: &exterrors.SMTPError{\n\t\t\t\tCode:         554,\n\t\t\t\tEnhancedCode: exterrors.EnhancedCode{5, 7, 0},\n\t\t\t\tMessage:      message,\n\t\t\t\tErr:          err,\n\t\t\t\tCheckName:    \"dnsbl\",\n\t\t\t},\n\t\t}\n\t}\n\tif score >= bl.quarantineThres {\n\t\treturn module.CheckResult{\n\t\t\tQuarantine: true,\n\t\t\tReason: &exterrors.SMTPError{\n\t\t\t\tCode:         554,\n\t\t\t\tEnhancedCode: exterrors.EnhancedCode{5, 7, 0},\n\t\t\t\tMessage:      message,\n\t\t\t\tErr:          err,\n\t\t\t\tCheckName:    \"dnsbl\",\n\t\t\t},\n\t\t}\n\t}\n\n\treturn module.CheckResult{}\n}\n\n// CheckConnection implements module.EarlyCheck.\nfunc (bl *DNSBL) CheckConnection(ctx context.Context, state *module.ConnState) error {\n\tdefer trace.StartRegion(ctx, \"dnsbl/CheckConnection (Early)\").End()\n\n\tip, ok := state.RemoteAddr.(*net.TCPAddr)\n\tif !ok {\n\t\tbl.log.Msg(\"non-TCP/IP source\",\n\t\t\t\"src_addr\", state.RemoteAddr,\n\t\t\t\"src_host\", state.Hostname)\n\t\treturn nil\n\t}\n\n\tresult := bl.checkLists(ctx, ip.IP, state.Hostname, \"\")\n\tif result.Reject && bl.checkEarly {\n\t\treturn result.Reason\n\t}\n\n\tstate.ModData.Set(bl, true, result)\n\n\treturn nil\n}\n\ntype state struct {\n\tbl      *DNSBL\n\tmsgMeta *module.MsgMetadata\n\tlog     *log.Logger\n}\n\nfunc (bl *DNSBL) CheckStateForMsg(ctx context.Context, msgMeta *module.MsgMetadata) (module.CheckState, error) {\n\treturn &state{\n\t\tbl:      bl,\n\t\tmsgMeta: msgMeta,\n\t\tlog:     target.DeliveryLogger(bl.log, msgMeta),\n\t}, nil\n}\n\nfunc (s *state) CheckConnection(ctx context.Context) module.CheckResult {\n\tdefer trace.StartRegion(ctx, \"dnsbl/CheckConnection\").End()\n\n\tif s.msgMeta.Conn == nil {\n\t\ts.log.Msg(\"locally generated message, ignoring\")\n\t\treturn module.CheckResult{}\n\t}\n\n\tresult := s.msgMeta.Conn.ModData.Get(s.bl, true)\n\tif result != nil {\n\t\treturn result.(module.CheckResult)\n\t}\n\n\treturn module.CheckResult{}\n}\n\nfunc (*state) CheckSender(context.Context, string) module.CheckResult {\n\treturn module.CheckResult{}\n}\n\nfunc (*state) CheckRcpt(context.Context, string) module.CheckResult {\n\treturn module.CheckResult{}\n}\n\nfunc (*state) CheckBody(context.Context, textproto.Header, buffer.Buffer) module.CheckResult {\n\treturn module.CheckResult{}\n}\n\nfunc (*state) Close() error {\n\treturn nil\n}\n\nfunc init() {\n\tmodules.Register(\"check.dnsbl\", New)\n}\n"
  },
  {
    "path": "internal/check/dnsbl/dnsbl_test.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage dnsbl\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"net\"\n\t\"testing\"\n\n\t\"github.com/foxcpp/go-mockdns\"\n\t\"github.com/foxcpp/maddy/internal/testutils\"\n)\n\nfunc TestCheckList(t *testing.T) {\n\ttest := func(zones map[string]mockdns.Zone, cfg List, ip net.IP, ehlo, mailFrom string, expectedErr error) {\n\t\tmod := &DNSBL{\n\t\t\tresolver: &mockdns.Resolver{Zones: zones},\n\t\t\tlog:      testutils.Logger(t, \"dnsbl\"),\n\t\t}\n\t\terr := mod.checkList(context.Background(), cfg, ip, ehlo, mailFrom)\n\t\tif !errors.Is(err, expectedErr) {\n\t\t\tt.Errorf(\"expected err to be '%#v', got '%#v'\", expectedErr, err)\n\t\t}\n\t}\n\n\ttest(nil, List{Zone: \"example.org\"}, net.IPv4(1, 2, 3, 4),\n\t\t\"example.com\", \"foo@example.com\", nil)\n\ttest(map[string]mockdns.Zone{\n\t\t\"4.3.2.1.example.org.\": {\n\t\t\tA: []string{\"127.0.0.1\"},\n\t\t},\n\t}, List{Zone: \"example.org\", ClientIPv4: true}, net.IPv4(1, 2, 3, 4),\n\t\t\"mx.example.com\", \"foo@example.com\", ListedErr{\n\t\t\tIdentity: \"1.2.3.4\",\n\t\t\tList:     \"example.org\",\n\t\t\tReason:   \"127.0.0.1\",\n\t\t},\n\t)\n\ttest(map[string]mockdns.Zone{\n\t\t\"mx.example.com.example.org.\": {\n\t\t\tA: []string{\"127.0.0.1\"},\n\t\t},\n\t}, List{Zone: \"example.org\"}, net.IPv4(1, 2, 3, 4),\n\t\t\"mx.example.com\", \"foo@example.com\", nil,\n\t)\n\ttest(map[string]mockdns.Zone{\n\t\t\"mx.example.com.example.org.\": {\n\t\t\tA: []string{\"127.0.0.1\"},\n\t\t},\n\t}, List{Zone: \"example.org\", EHLO: true}, net.IPv4(1, 2, 3, 4),\n\t\t\"mx.example.com\", \"foo@example.com\", ListedErr{\n\t\t\tIdentity: \"mx.example.com\",\n\t\t\tList:     \"example.org\",\n\t\t\tReason:   \"127.0.0.1\",\n\t\t},\n\t)\n\ttest(map[string]mockdns.Zone{\n\t\t\"[1.2.3.4].example.org.\": {\n\t\t\tA: []string{\"127.0.0.1\"},\n\t\t},\n\t}, List{Zone: \"example.org\", EHLO: true}, net.IPv4(1, 2, 3, 4),\n\t\t\"[1.2.3.4]\", \"foo@example.com\", nil,\n\t)\n\ttest(map[string]mockdns.Zone{\n\t\t\"[IPv6:beef::1].example.org.\": {\n\t\t\tA: []string{\"127.0.0.1\"},\n\t\t},\n\t}, List{Zone: \"example.org\", EHLO: true}, net.IPv4(1, 2, 3, 4),\n\t\t\"[IPv6:beef::1]\", \"foo@example.com\", nil,\n\t)\n\ttest(map[string]mockdns.Zone{\n\t\t\"example.com.example.org.\": {\n\t\t\tA: []string{\"127.0.0.1\"},\n\t\t},\n\t}, List{Zone: \"example.org\"}, net.IPv4(1, 2, 3, 4),\n\t\t\"mx.example.com\", \"foo@example.com\", nil,\n\t)\n\ttest(map[string]mockdns.Zone{\n\t\t\"postmaster.example.org.\": {\n\t\t\tA: []string{\"127.0.0.1\"},\n\t\t},\n\t}, List{Zone: \"example.org\", MAILFROM: true}, net.IPv4(1, 2, 3, 4),\n\t\t\"mx.example.com\", \"postmaster\", nil,\n\t)\n\ttest(map[string]mockdns.Zone{\n\t\t\".example.org.\": {\n\t\t\tA: []string{\"127.0.0.1\"},\n\t\t},\n\t}, List{Zone: \"example.org\", MAILFROM: true}, net.IPv4(1, 2, 3, 4),\n\t\t\"mx.example.com\", \"\", nil,\n\t)\n\ttest(map[string]mockdns.Zone{\n\t\t\"example.com.example.org.\": {\n\t\t\tA: []string{\"127.0.0.1\"},\n\t\t},\n\t}, List{Zone: \"example.org\", MAILFROM: true}, net.IPv4(1, 2, 3, 4),\n\t\t\"mx.example.com\", \"foo@example.com\", ListedErr{\n\t\t\tIdentity: \"example.com\",\n\t\t\tList:     \"example.org\",\n\t\t\tReason:   \"127.0.0.1\",\n\t\t},\n\t)\n}\n\nfunc TestCheckLists(t *testing.T) {\n\ttest := func(zones map[string]mockdns.Zone, bls []List, ip net.IP, ehlo, mailFrom string, reject, quarantine bool) {\n\t\tmod := &DNSBL{\n\t\t\tbls:             bls,\n\t\t\tresolver:        &mockdns.Resolver{Zones: zones},\n\t\t\tlog:             testutils.Logger(t, \"dnsbl\"),\n\t\t\tquarantineThres: 1,\n\t\t\trejectThres:     2,\n\t\t}\n\t\tresult := mod.checkLists(context.Background(), ip, ehlo, mailFrom)\n\n\t\tif result.Reject && !reject {\n\t\t\tt.Errorf(\"Expected message to not be rejected\")\n\t\t}\n\t\tif !result.Reject && reject {\n\t\t\tt.Errorf(\"Expected message to be rejected\")\n\t\t}\n\t\tif result.Quarantine && !quarantine {\n\t\t\tt.Errorf(\"Expected message to not be quarantined\")\n\t\t}\n\t\tif !result.Quarantine && quarantine {\n\t\t\tt.Errorf(\"Expected message to be quarantined\")\n\t\t}\n\t}\n\n\t// Score 2 >= 2, reject\n\ttest(map[string]mockdns.Zone{\n\t\t\"4.3.2.1.example.org.\": {\n\t\t\tA: []string{\"127.0.0.1\"},\n\t\t},\n\t}, []List{\n\t\t{\n\t\t\tZone:       \"example.org\",\n\t\t\tClientIPv4: true,\n\t\t\tScoreAdj:   2,\n\t\t},\n\t}, net.IPv4(1, 2, 3, 4),\n\t\t\"mx.example.com\", \"foo@example.com\", true, false,\n\t)\n\n\t// Score 1 >= 1, quarantine\n\ttest(map[string]mockdns.Zone{\n\t\t\"4.3.2.1.example.org.\": {\n\t\t\tA: []string{\"127.0.0.1\"},\n\t\t},\n\t}, []List{\n\t\t{\n\t\t\tZone:       \"example.org\",\n\t\t\tClientIPv4: true,\n\t\t\tScoreAdj:   1,\n\t\t},\n\t}, net.IPv4(1, 2, 3, 4),\n\t\t\"mx.example.com\", \"foo@example.com\", false, true,\n\t)\n\n\t// Score 0, no action\n\ttest(map[string]mockdns.Zone{\n\t\t\"4.3.2.1.example.org.\": {\n\t\t\tA: []string{\"127.0.0.1\"},\n\t\t},\n\t\t\"4.3.2.1.example.net.\": {\n\t\t\tA: []string{\"127.0.0.1\"},\n\t\t},\n\t},\n\t\t[]List{\n\t\t\t{Zone: \"example.org\", ClientIPv4: true, ScoreAdj: 1},\n\t\t\t{Zone: \"example.net\", ClientIPv4: true, ScoreAdj: -1},\n\t\t},\n\t\tnet.IPv4(1, 2, 3, 4),\n\t\t\"mx.example.com\", \"foo@example.com\",\n\t\tfalse, false,\n\t)\n\n\t// DNS error, hard-fail (reject)\n\ttest(map[string]mockdns.Zone{\n\t\t\"4.3.2.2.example.org.\": {\n\t\t\tErr: &net.DNSError{\n\t\t\t\tErr:         \"i/o timeout\",\n\t\t\t\tIsTimeout:   true,\n\t\t\t\tIsTemporary: true,\n\t\t\t},\n\t\t},\n\t},\n\t\t[]List{\n\t\t\t{Zone: \"example.org\", ClientIPv4: true, ScoreAdj: 1},\n\t\t\t{Zone: \"example.net\", ClientIPv4: true, ScoreAdj: 2},\n\t\t},\n\t\tnet.IPv4(2, 2, 3, 4),\n\t\t\"mx.example.com\", \"foo@example.com\",\n\t\ttrue, false,\n\t)\n}\n\nfunc TestCheckIPWithResponseRules(t *testing.T) {\n\ttest := func(zones map[string]mockdns.Zone, cfg List, ip net.IP, expectedErr error) {\n\t\tt.Helper()\n\t\tresolver := mockdns.Resolver{Zones: zones}\n\t\terr := checkIP(context.Background(), &resolver, cfg, ip)\n\t\tif expectedErr == nil {\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"expected no error, got '%#v'\", err)\n\t\t\t}\n\t\t} else {\n\t\t\tif err == nil {\n\t\t\t\tt.Errorf(\"expected err to be '%#v', got nil\", expectedErr)\n\t\t\t} else {\n\t\t\t\texpectedLE, okExpected := expectedErr.(ListedErr)\n\t\t\t\tactualLE, okActual := err.(ListedErr)\n\t\t\t\tif !okExpected || !okActual {\n\t\t\t\t\tt.Errorf(\"expected err to be '%#v', got '%#v'\", expectedErr, err)\n\t\t\t\t} else {\n\t\t\t\t\tif expectedLE.Identity != actualLE.Identity ||\n\t\t\t\t\t\texpectedLE.List != actualLE.List ||\n\t\t\t\t\t\texpectedLE.Score != actualLE.Score ||\n\t\t\t\t\t\texpectedLE.Message != actualLE.Message {\n\t\t\t\t\t\tt.Errorf(\"expected err to be '%#v', got '%#v'\", expectedErr, err)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Test single response code with score and message\n\ttest(map[string]mockdns.Zone{\n\t\t\"4.3.2.1.example.org.\": {\n\t\t\tA: []string{\"127.0.0.2\"},\n\t\t},\n\t}, List{\n\t\tZone:       \"example.org\",\n\t\tClientIPv4: true,\n\t\tResponseRules: []ResponseRule{\n\t\t\t{\n\t\t\t\tNetworks: []net.IPNet{\n\t\t\t\t\t{IP: net.IPv4(127, 0, 0, 2), Mask: net.IPv4Mask(255, 255, 255, 255)},\n\t\t\t\t},\n\t\t\t\tScore:   10,\n\t\t\t\tMessage: \"Listed in SBL\",\n\t\t\t},\n\t\t},\n\t}, net.IPv4(1, 2, 3, 4), ListedErr{\n\t\tIdentity: \"1.2.3.4\",\n\t\tList:     \"example.org\",\n\t\tScore:    10,\n\t\tMessage:  \"Listed in SBL\",\n\t})\n\n\t// Test multiple response codes with different scores - scores should sum\n\ttest(map[string]mockdns.Zone{\n\t\t\"4.3.2.1.example.org.\": {\n\t\t\tA: []string{\"127.0.0.2\", \"127.0.0.11\"},\n\t\t},\n\t}, List{\n\t\tZone:       \"example.org\",\n\t\tClientIPv4: true,\n\t\tResponseRules: []ResponseRule{\n\t\t\t{\n\t\t\t\tNetworks: []net.IPNet{\n\t\t\t\t\t{IP: net.IPv4(127, 0, 0, 2), Mask: net.IPv4Mask(255, 255, 255, 255)},\n\t\t\t\t\t{IP: net.IPv4(127, 0, 0, 3), Mask: net.IPv4Mask(255, 255, 255, 255)},\n\t\t\t\t},\n\t\t\t\tScore:   10,\n\t\t\t\tMessage: \"Listed in SBL\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tNetworks: []net.IPNet{\n\t\t\t\t\t{IP: net.IPv4(127, 0, 0, 10), Mask: net.IPv4Mask(255, 255, 255, 255)},\n\t\t\t\t\t{IP: net.IPv4(127, 0, 0, 11), Mask: net.IPv4Mask(255, 255, 255, 255)},\n\t\t\t\t},\n\t\t\t\tScore:   5,\n\t\t\t\tMessage: \"Listed in PBL\",\n\t\t\t},\n\t\t},\n\t}, net.IPv4(1, 2, 3, 4), ListedErr{\n\t\tIdentity: \"1.2.3.4\",\n\t\tList:     \"example.org\",\n\t\tScore:    15, // 10 + 5\n\t\tMessage:  \"Listed in SBL\",\n\t})\n\n\t// Test response code that doesn't match any rule - should return nil\n\ttest(map[string]mockdns.Zone{\n\t\t\"4.3.2.1.example.org.\": {\n\t\t\tA: []string{\"127.0.0.99\"},\n\t\t},\n\t}, List{\n\t\tZone:       \"example.org\",\n\t\tClientIPv4: true,\n\t\tResponseRules: []ResponseRule{\n\t\t\t{\n\t\t\t\tNetworks: []net.IPNet{\n\t\t\t\t\t{IP: net.IPv4(127, 0, 0, 2), Mask: net.IPv4Mask(255, 255, 255, 255)},\n\t\t\t\t},\n\t\t\t\tScore:   10,\n\t\t\t\tMessage: \"Listed in SBL\",\n\t\t\t},\n\t\t},\n\t}, net.IPv4(1, 2, 3, 4), nil)\n\n\t// Test low severity only - should get score 5\n\ttest(map[string]mockdns.Zone{\n\t\t\"4.3.2.1.example.org.\": {\n\t\t\tA: []string{\"127.0.0.10\"},\n\t\t},\n\t}, List{\n\t\tZone:       \"example.org\",\n\t\tClientIPv4: true,\n\t\tResponseRules: []ResponseRule{\n\t\t\t{\n\t\t\t\tNetworks: []net.IPNet{\n\t\t\t\t\t{IP: net.IPv4(127, 0, 0, 2), Mask: net.IPv4Mask(255, 255, 255, 255)},\n\t\t\t\t},\n\t\t\t\tScore:   10,\n\t\t\t\tMessage: \"Listed in SBL\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tNetworks: []net.IPNet{\n\t\t\t\t\t{IP: net.IPv4(127, 0, 0, 10), Mask: net.IPv4Mask(255, 255, 255, 255)},\n\t\t\t\t},\n\t\t\t\tScore:   5,\n\t\t\t\tMessage: \"Listed in PBL\",\n\t\t\t},\n\t\t},\n\t}, net.IPv4(1, 2, 3, 4), ListedErr{\n\t\tIdentity: \"1.2.3.4\",\n\t\tList:     \"example.org\",\n\t\tScore:    5,\n\t\tMessage:  \"Listed in PBL\",\n\t})\n\n\t// Test high severity - should get score 10\n\ttest(map[string]mockdns.Zone{\n\t\t\"4.3.2.1.example.org.\": {\n\t\t\tA: []string{\"127.0.0.2\"},\n\t\t},\n\t}, List{\n\t\tZone:       \"example.org\",\n\t\tClientIPv4: true,\n\t\tResponseRules: []ResponseRule{\n\t\t\t{\n\t\t\t\tNetworks: []net.IPNet{\n\t\t\t\t\t{IP: net.IPv4(127, 0, 0, 2), Mask: net.IPv4Mask(255, 255, 255, 255)},\n\t\t\t\t},\n\t\t\t\tScore:   10,\n\t\t\t\tMessage: \"Listed in SBL\",\n\t\t\t},\n\t\t},\n\t}, net.IPv4(1, 2, 3, 4), ListedErr{\n\t\tIdentity: \"1.2.3.4\",\n\t\tList:     \"example.org\",\n\t\tScore:    10,\n\t\tMessage:  \"Listed in SBL\",\n\t})\n}\n\nfunc TestCheckListsWithResponseRules(t *testing.T) {\n\ttest := func(zones map[string]mockdns.Zone, bls []List, ip net.IP, ehlo, mailFrom string, reject, quarantine bool) {\n\t\tmod := &DNSBL{\n\t\t\tbls:             bls,\n\t\t\tresolver:        &mockdns.Resolver{Zones: zones},\n\t\t\tlog:             testutils.Logger(t, \"dnsbl\"),\n\t\t\tquarantineThres: 5,\n\t\t\trejectThres:     10,\n\t\t}\n\t\tresult := mod.checkLists(context.Background(), ip, ehlo, mailFrom)\n\n\t\tif result.Reject && !reject {\n\t\t\tt.Errorf(\"Expected message to not be rejected\")\n\t\t}\n\t\tif !result.Reject && reject {\n\t\t\tt.Errorf(\"Expected message to be rejected\")\n\t\t}\n\t\tif result.Quarantine && !quarantine {\n\t\t\tt.Errorf(\"Expected message to not be quarantined\")\n\t\t}\n\t\tif !result.Quarantine && quarantine {\n\t\t\tt.Errorf(\"Expected message to be quarantined\")\n\t\t}\n\t}\n\n\t// Test: Only low-severity code returned -> quarantine but not reject\n\ttest(map[string]mockdns.Zone{\n\t\t\"4.3.2.1.zen.example.org.\": {\n\t\t\tA: []string{\"127.0.0.11\"},\n\t\t},\n\t}, []List{\n\t\t{\n\t\t\tZone:       \"zen.example.org\",\n\t\t\tClientIPv4: true,\n\t\t\tResponseRules: []ResponseRule{\n\t\t\t\t{\n\t\t\t\t\tNetworks: []net.IPNet{\n\t\t\t\t\t\t{IP: net.IPv4(127, 0, 0, 2), Mask: net.IPv4Mask(255, 255, 255, 255)},\n\t\t\t\t\t},\n\t\t\t\t\tScore:   10,\n\t\t\t\t\tMessage: \"Listed in SBL\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tNetworks: []net.IPNet{\n\t\t\t\t\t\t{IP: net.IPv4(127, 0, 0, 10), Mask: net.IPv4Mask(255, 255, 255, 255)},\n\t\t\t\t\t\t{IP: net.IPv4(127, 0, 0, 11), Mask: net.IPv4Mask(255, 255, 255, 255)},\n\t\t\t\t\t},\n\t\t\t\t\tScore:   5,\n\t\t\t\t\tMessage: \"Listed in PBL\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}, net.IPv4(1, 2, 3, 4), \"mx.example.com\", \"foo@example.com\", false, true)\n\n\t// Test: High-severity code returned -> reject\n\ttest(map[string]mockdns.Zone{\n\t\t\"4.3.2.1.zen.example.org.\": {\n\t\t\tA: []string{\"127.0.0.2\"},\n\t\t},\n\t}, []List{\n\t\t{\n\t\t\tZone:       \"zen.example.org\",\n\t\t\tClientIPv4: true,\n\t\t\tResponseRules: []ResponseRule{\n\t\t\t\t{\n\t\t\t\t\tNetworks: []net.IPNet{\n\t\t\t\t\t\t{IP: net.IPv4(127, 0, 0, 2), Mask: net.IPv4Mask(255, 255, 255, 255)},\n\t\t\t\t\t},\n\t\t\t\t\tScore:   10,\n\t\t\t\t\tMessage: \"Listed in SBL\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}, net.IPv4(1, 2, 3, 4), \"mx.example.com\", \"foo@example.com\", true, false)\n\n\t// Test: Legacy configuration without response blocks -> existing behavior preserved\n\ttest(map[string]mockdns.Zone{\n\t\t\"4.3.2.1.example.org.\": {\n\t\t\tA: []string{\"127.0.0.1\"},\n\t\t},\n\t}, []List{\n\t\t{\n\t\t\tZone:       \"example.org\",\n\t\t\tClientIPv4: true,\n\t\t\tScoreAdj:   10,\n\t\t},\n\t}, net.IPv4(1, 2, 3, 4), \"mx.example.com\", \"foo@example.com\", true, false)\n\n\t// Test: Mixed configuration (some lists with response blocks, some without) -> both work correctly\n\ttest(map[string]mockdns.Zone{\n\t\t\"4.3.2.1.zen.example.org.\": {\n\t\t\tA: []string{\"127.0.0.11\"},\n\t\t},\n\t\t\"4.3.2.1.legacy.example.org.\": {\n\t\t\tA: []string{\"127.0.0.1\"},\n\t\t},\n\t}, []List{\n\t\t{\n\t\t\tZone:       \"zen.example.org\",\n\t\t\tClientIPv4: true,\n\t\t\tResponseRules: []ResponseRule{\n\t\t\t\t{\n\t\t\t\t\tNetworks: []net.IPNet{\n\t\t\t\t\t\t{IP: net.IPv4(127, 0, 0, 11), Mask: net.IPv4Mask(255, 255, 255, 255)},\n\t\t\t\t\t},\n\t\t\t\t\tScore:   5,\n\t\t\t\t\tMessage: \"Listed in PBL\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tZone:       \"legacy.example.org\",\n\t\t\tClientIPv4: true,\n\t\t\tScoreAdj:   3,\n\t\t},\n\t}, net.IPv4(1, 2, 3, 4), \"mx.example.com\", \"foo@example.com\", false, true) // 5 + 3 = 8, quarantine but not reject\n}\n"
  },
  {
    "path": "internal/check/milter/milter.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage milter\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"time\"\n\n\t\"github.com/emersion/go-message/textproto\"\n\t\"github.com/emersion/go-milter\"\n\t\"github.com/foxcpp/maddy/framework/buffer\"\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/exterrors\"\n\t\"github.com/foxcpp/maddy/framework/log\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n\t\"github.com/foxcpp/maddy/internal/target\"\n)\n\nconst modName = \"check.milter\"\n\ntype Check struct {\n\tcl        *milter.Client\n\tmilterUrl string\n\tfailOpen  bool\n\tinstName  string\n\tlog       *log.Logger\n}\n\nfunc New(c *container.C, _, instName string) (module.Module, error) {\n\tchk := &Check{\n\t\tinstName: instName,\n\t\tlog:      c.DefaultLogger.Sublogger(modName),\n\t}\n\n\treturn chk, nil\n}\n\nfunc (c *Check) Name() string {\n\treturn modName\n}\n\nfunc (c *Check) InstanceName() string {\n\treturn c.instName\n}\n\nfunc (c *Check) Configure(inlineArgs []string, cfg *config.Map) error {\n\tswitch len(inlineArgs) {\n\tcase 1:\n\t\tc.milterUrl = inlineArgs[0]\n\tcase 0:\n\tdefault:\n\t\treturn fmt.Errorf(\"%s: unexpected amount of arguments, want 1 or 0\", modName)\n\t}\n\n\tcfg.String(\"endpoint\", false, false, c.milterUrl, &c.milterUrl)\n\tcfg.Bool(\"fail_open\", false, false, &c.failOpen)\n\tif _, err := cfg.Process(); err != nil {\n\t\treturn err\n\t}\n\n\tif c.milterUrl == \"\" {\n\t\treturn fmt.Errorf(\"%s: milter endpoint is not set\", modName)\n\t}\n\n\tendp, err := config.ParseEndpoint(c.milterUrl)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"%s: %v\", modName, err)\n\t}\n\n\tswitch endp.Scheme {\n\tcase \"tcp\", \"unix\":\n\tdefault:\n\t\treturn fmt.Errorf(\"%s: scheme unsupported: %v\", modName, endp.Scheme)\n\t}\n\n\tc.cl = milter.NewClientWithOptions(endp.Network(), endp.Address(), milter.ClientOptions{\n\t\tDialer: &net.Dialer{\n\t\t\tTimeout: 10 * time.Second,\n\t\t},\n\t\tReadTimeout:  10 * time.Second,\n\t\tWriteTimeout: 10 * time.Second,\n\t\tActionMask:   milter.OptAddHeader | milter.OptQuarantine,\n\t\tProtocolMask: 0,\n\t})\n\n\treturn nil\n}\n\ntype state struct {\n\tc          *Check\n\tsession    *milter.ClientSession\n\tmsgMeta    *module.MsgMetadata\n\tskipChecks bool\n\tlog        *log.Logger\n}\n\nfunc (c *Check) CheckStateForMsg(ctx context.Context, msgMeta *module.MsgMetadata) (module.CheckState, error) {\n\tsession, err := c.cl.Session()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &state{\n\t\tc:       c,\n\t\tsession: session,\n\t\tmsgMeta: msgMeta,\n\t\tlog:     target.DeliveryLogger(c.log, msgMeta),\n\t}, nil\n}\n\nfunc (s *state) handleAction(act *milter.Action) module.CheckResult {\n\tswitch act.Code {\n\tcase milter.ActAccept:\n\t\ts.skipChecks = true\n\t\treturn module.CheckResult{}\n\tcase milter.ActContinue:\n\t\treturn module.CheckResult{}\n\tcase milter.ActReplyCode:\n\t\treturn module.CheckResult{\n\t\t\tReject: true,\n\t\t\tReason: &exterrors.SMTPError{\n\t\t\t\tCode:         act.SMTPCode,\n\t\t\t\tEnhancedCode: exterrors.EnhancedCode{5, 7, 1},\n\t\t\t\tMessage:      \"Message rejected due to local policy\",\n\t\t\t\tReason:       \"reply code action\",\n\t\t\t\tCheckName:    \"milter\",\n\t\t\t\tMisc: map[string]interface{}{\n\t\t\t\t\t\"milter\": s.c.milterUrl,\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\tcase milter.ActDiscard:\n\t\ts.log.Msg(\"silent discard is not supported, rejecting message\")\n\t\tfallthrough\n\tcase milter.ActTempFail:\n\t\treturn module.CheckResult{\n\t\t\tReject: true,\n\t\t\tReason: &exterrors.SMTPError{\n\t\t\t\tCode:         450,\n\t\t\t\tEnhancedCode: exterrors.EnhancedCode{4, 7, 1},\n\t\t\t\tMessage:      \"Message rejected due to local policy\",\n\t\t\t\tReason:       \"reject action\",\n\t\t\t\tCheckName:    \"milter\",\n\t\t\t\tMisc: map[string]interface{}{\n\t\t\t\t\t\"milter\": s.c.milterUrl,\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\tcase milter.ActReject:\n\t\treturn module.CheckResult{\n\t\t\tReject: true,\n\t\t\tReason: &exterrors.SMTPError{\n\t\t\t\tCode:         550,\n\t\t\t\tEnhancedCode: exterrors.EnhancedCode{5, 7, 1},\n\t\t\t\tMessage:      \"Message rejected due to local policy\",\n\t\t\t\tReason:       \"reject action\",\n\t\t\t\tCheckName:    \"milter\",\n\t\t\t\tMisc: map[string]interface{}{\n\t\t\t\t\t\"milter\": s.c.milterUrl,\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\tdefault:\n\t\ts.log.Msg(\"unknown action code ignored\", \"code\", act.Code, \"milter\", s.c.milterUrl)\n\t\treturn module.CheckResult{}\n\t}\n}\n\n// apply applies the modification actions returned by milter to the check results object.\nfunc (s *state) apply(modifyActs []milter.ModifyAction, res module.CheckResult) module.CheckResult {\n\tout := res\n\tfor _, act := range modifyActs {\n\t\tswitch act.Code {\n\t\tcase milter.ActAddRcpt, milter.ActDelRcpt:\n\t\t\ts.log.Msg(\"envelope changes are not supported\", \"rcpt\", act.Rcpt, \"code\", act.Code, \"milter\", s.c.milterUrl)\n\t\tcase milter.ActChangeFrom:\n\t\t\ts.log.Msg(\"envelope changes are not supported\", \"from\", act.From, \"code\", act.Code, \"milter\", s.c.milterUrl)\n\t\tcase milter.ActChangeHeader:\n\t\t\ts.log.Msg(\"header field changes are not supported\", \"field\", act.HeaderName, \"milter\", s.c.milterUrl)\n\t\tcase milter.ActInsertHeader:\n\t\t\tif act.HeaderIndex != 1 {\n\t\t\t\ts.log.Msg(\"header inserting not on top is not supported, prepending instead\", \"field\", act.HeaderName, \"milter\", s.c.milterUrl)\n\t\t\t}\n\t\t\tfallthrough\n\t\tcase milter.ActAddHeader:\n\t\t\t// Header field might be arbitarly folded by the caller and we want\n\t\t\t// to preserve that exact format in case it is important (DKIM\n\t\t\t// signature is added by milter).\n\t\t\tfield := make([]byte, 0, len(act.HeaderName)+2+len(act.HeaderValue)+2)\n\t\t\tfield = append(field, act.HeaderName...)\n\t\t\tfield = append(field, ':', ' ')\n\t\t\tfield = append(field, act.HeaderValue...)\n\t\t\tfield = append(field, '\\r', '\\n')\n\t\t\tout.Header.AddRaw(field)\n\t\tcase milter.ActQuarantine:\n\t\t\tout.Quarantine = true\n\t\t\tout.Reason = exterrors.WithFields(errors.New(\"milter quarantine action\"), map[string]interface{}{\n\t\t\t\t\"check\":  \"milter\",\n\t\t\t\t\"milter\": s.c.milterUrl,\n\t\t\t\t\"reason\": act.Reason,\n\t\t\t})\n\t\t}\n\t}\n\treturn out\n}\n\nfunc (s *state) CheckConnection(ctx context.Context) module.CheckResult {\n\tif s.msgMeta.Conn == nil {\n\t\t// Submit some dummy values as the message is likely generated locally.\n\n\t\tact, err := s.session.Conn(\"localhost\", milter.FamilyInet, 25, \"127.0.0.1\")\n\t\tif err != nil {\n\t\t\treturn s.ioError(err)\n\t\t}\n\t\tif act.Code != milter.ActContinue {\n\t\t\treturn s.handleAction(act)\n\t\t}\n\n\t\tact, err = s.session.Helo(\"localhost\")\n\t\tif err != nil {\n\t\t\treturn s.ioError(err)\n\t\t}\n\t\treturn s.handleAction(act)\n\t}\n\n\tif !s.session.ProtocolOption(milter.OptNoConnect) {\n\t\tif err := s.session.Macros(milter.CodeConn,\n\t\t\t\"daemon_name\", \"maddy\",\n\t\t\t\"if_name\", \"unknown\",\n\t\t\t\"if_addr\", \"0.0.0.0\",\n\t\t\t// TODO: $j\n\t\t\t// TODO: $_\n\t\t); err != nil {\n\t\t\treturn s.ioError(err)\n\t\t}\n\n\t\tvar (\n\t\t\tprotoFamily milter.ProtoFamily\n\t\t\tport        uint16\n\t\t\taddr        string\n\t\t)\n\t\tswitch rAddr := s.msgMeta.Conn.RemoteAddr.(type) {\n\t\tcase *net.TCPAddr:\n\t\t\tport = uint16(rAddr.Port)\n\t\t\tif v4 := rAddr.IP.To4(); v4 != nil {\n\t\t\t\t// Make sure to not accidentally send IPv6-mapped IPv4 address.\n\t\t\t\tprotoFamily = milter.FamilyInet\n\t\t\t\taddr = v4.String()\n\t\t\t} else {\n\t\t\t\tprotoFamily = milter.FamilyInet6\n\t\t\t\taddr = rAddr.IP.String()\n\t\t\t}\n\t\tcase *net.UnixAddr:\n\t\t\tprotoFamily = milter.FamilyUnix\n\t\t\taddr = rAddr.Name\n\t\tdefault:\n\t\t\tprotoFamily = milter.FamilyUnknown\n\t\t}\n\n\t\tact, err := s.session.Conn(s.msgMeta.Conn.Hostname, protoFamily, port, addr)\n\t\tif err != nil {\n\t\t\treturn s.ioError(err)\n\t\t}\n\t\tif act.Code != milter.ActContinue {\n\t\t\treturn s.handleAction(act)\n\t\t}\n\t}\n\n\tif !s.session.ProtocolOption(milter.OptNoHelo) {\n\t\tif s.msgMeta.Conn.TLS.HandshakeComplete {\n\t\t\tfields := make([]string, 0, 4*2)\n\t\t\ttlsState := s.msgMeta.Conn.TLS\n\n\t\t\tswitch tlsState.Version {\n\t\t\tcase tls.VersionTLS10:\n\t\t\t\tfields = append(fields, \"tls_version\", \"TLSv1\")\n\t\t\tcase tls.VersionTLS11:\n\t\t\t\tfields = append(fields, \"tls_version\", \"TLSv1.1\")\n\t\t\tcase tls.VersionTLS12:\n\t\t\t\tfields = append(fields, \"tls_version\", \"TLSv1.2\")\n\t\t\tcase tls.VersionTLS13:\n\t\t\t\tfields = append(fields, \"tls_version\", \"TLSv1.3\")\n\t\t\t}\n\t\t\tfields = append(fields, \"cipher\", tls.CipherSuiteName(tlsState.CipherSuite))\n\n\t\t\tif len(tlsState.PeerCertificates) != 0 {\n\t\t\t\tfields = append(fields, \"cert_subject\",\n\t\t\t\t\ttlsState.PeerCertificates[len(tlsState.PeerCertificates)-1].Subject.String())\n\t\t\t\tfields = append(fields, \"cert_issuer\",\n\t\t\t\t\ttlsState.PeerCertificates[len(tlsState.PeerCertificates)-1].Issuer.String())\n\t\t\t}\n\n\t\t\tif err := s.session.Macros(milter.CodeHelo, fields...); err != nil {\n\t\t\t\treturn s.ioError(err)\n\t\t\t}\n\t\t}\n\t\tact, err := s.session.Helo(s.msgMeta.Conn.Hostname)\n\t\tif err != nil {\n\t\t\treturn s.ioError(err)\n\t\t}\n\t\treturn s.handleAction(act)\n\t}\n\n\treturn module.CheckResult{}\n}\n\nfunc (s *state) ioError(err error) module.CheckResult {\n\tif s.c.failOpen {\n\t\ts.skipChecks = true // silently permit processing to continue\n\t\ts.c.log.Error(\"I/O error\", err)\n\t\treturn module.CheckResult{}\n\t}\n\n\treturn module.CheckResult{\n\t\tReject: true,\n\t\tReason: &exterrors.SMTPError{\n\t\t\tCode:         451,\n\t\t\tEnhancedCode: exterrors.EnhancedCode{4, 7, 1},\n\t\t\tMessage:      \"I/O error during policy check\",\n\t\t\tErr:          err,\n\t\t\tCheckName:    \"milter\",\n\t\t\tMisc: map[string]interface{}{\n\t\t\t\t\"milter\": s.c.milterUrl,\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc (s *state) CheckSender(ctx context.Context, mailFrom string) module.CheckResult {\n\tif s.skipChecks || s.session.ProtocolOption(milter.OptNoMailFrom) {\n\t\treturn module.CheckResult{}\n\t}\n\n\tfields := make([]string, 0, 2)\n\tfields = append(fields, \"i\", s.msgMeta.ID)\n\t// TODO: fields = append(fields, \"auth_type\", s.msgMeta.???)\n\tif s.msgMeta.Conn.AuthUser != \"\" {\n\t\tfields = append(fields, \"auth_authen\", s.msgMeta.Conn.AuthUser)\n\t}\n\tif err := s.session.Macros(milter.CodeMail, fields...); err != nil {\n\t\treturn s.ioError(err)\n\t}\n\n\tesmtpArgs := make([]string, 0, 2)\n\tif s.msgMeta.SMTPOpts.UTF8 {\n\t\tesmtpArgs = append(esmtpArgs, \"SMTPUTF8\")\n\t}\n\n\tact, err := s.session.Mail(mailFrom, esmtpArgs)\n\tif err != nil {\n\t\treturn s.ioError(err)\n\t}\n\treturn s.handleAction(act)\n}\n\nfunc (s *state) CheckRcpt(ctx context.Context, rcptTo string) module.CheckResult {\n\tif s.skipChecks {\n\t\treturn module.CheckResult{}\n\t}\n\n\tact, err := s.session.Rcpt(rcptTo, nil)\n\tif err != nil {\n\t\treturn s.ioError(err)\n\t}\n\treturn s.handleAction(act)\n}\n\nfunc (s *state) CheckBody(ctx context.Context, header textproto.Header, body buffer.Buffer) module.CheckResult {\n\tif s.skipChecks {\n\t\treturn module.CheckResult{}\n\t}\n\n\tact, err := s.session.Header(header)\n\tif err != nil {\n\t\treturn s.ioError(err)\n\t}\n\tif act.Code != milter.ActContinue {\n\t\treturn s.handleAction(act)\n\t}\n\n\tvar modifyAct []milter.ModifyAction\n\n\tif !s.session.ProtocolOption(milter.OptNoBody) {\n\t\t// body.Open can be expensive for on-disk buffering.\n\t\tr, err := body.Open()\n\t\tif err != nil {\n\t\t\t// Not ioError(err) because fail_open directive is applied only for external I/O.\n\t\t\treturn module.CheckResult{\n\t\t\t\tReject: true,\n\t\t\t\tReason: &exterrors.SMTPError{\n\t\t\t\t\tCode:         451,\n\t\t\t\t\tEnhancedCode: exterrors.EnhancedCode{4, 7, 1},\n\t\t\t\t\tMessage:      \"Internal error during policy check\",\n\t\t\t\t\tErr:          err,\n\t\t\t\t\tCheckName:    \"milter\",\n\t\t\t\t\tMisc: map[string]interface{}{\n\t\t\t\t\t\t\"milter\": s.c.milterUrl,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t}\n\n\t\tmodifyAct, act, err = s.session.BodyReadFrom(r)\n\t\tif err != nil {\n\t\t\treturn s.ioError(err)\n\t\t}\n\t} else {\n\t\tmodifyAct, act, err = s.session.End()\n\t\tif err != nil {\n\t\t\treturn s.ioError(err)\n\t\t}\n\t}\n\n\tresult := s.handleAction(act)\n\treturn s.apply(modifyAct, result)\n}\n\nfunc (s *state) Close() error {\n\treturn s.session.Close()\n}\n\nvar (\n\t_ module.Check      = &Check{}\n\t_ module.CheckState = &state{}\n)\n\nfunc init() {\n\tmodules.Register(modName, New)\n}\n"
  },
  {
    "path": "internal/check/milter/milter_test.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage milter\n\nimport (\n\t\"testing\"\n\n\t\"github.com/foxcpp/maddy/framework/config\"\n)\n\nfunc TestAcceptValidEndpoints(t *testing.T) {\n\tfor _, endpoint := range []string{\n\t\t\"tcp://0.0.0.0:10025\",\n\t\t\"tcp://[::]:10025\",\n\t\t\"tcp:127.0.0.1:10025\",\n\t\t\"unix://path\",\n\t\t\"unix:path\",\n\t\t\"unix:/path\",\n\t\t\"unix:///path\",\n\t\t\"unix://also/path\",\n\t\t\"unix:///also/path\",\n\t} {\n\t\tc := &Check{milterUrl: endpoint}\n\n\t\terr := c.Configure(nil, &config.Map{})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"Unexpected failure for %s: %v\", endpoint, err)\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc TestRejectInvalidEndpoints(t *testing.T) {\n\tfor _, endpoint := range []string{\n\t\t\"tls://0.0.0.0:10025\",\n\t\t\"tls:0.0.0.0:10025\",\n\t} {\n\t\tc := &Check{milterUrl: endpoint}\n\t\terr := c.Configure(nil, &config.Map{})\n\t\tif err == nil {\n\t\t\tt.Errorf(\"Accepted invalid endpoint: %s\", endpoint)\n\t\t\treturn\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/check/requiretls/requiretls.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage requiretls\n\nimport (\n\tmodconfig \"github.com/foxcpp/maddy/framework/config/module\"\n\t\"github.com/foxcpp/maddy/framework/exterrors\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/internal/check\"\n)\n\nfunc requireTLS(ctx check.StatelessCheckContext) module.CheckResult {\n\tif ctx.MsgMeta.Conn != nil && ctx.MsgMeta.Conn.TLS.HandshakeComplete {\n\t\treturn module.CheckResult{}\n\t}\n\n\treturn module.CheckResult{\n\t\tReason: &exterrors.SMTPError{\n\t\t\tCode:         550,\n\t\t\tEnhancedCode: exterrors.EnhancedCode{5, 7, 1},\n\t\t\tMessage:      \"TLS conversation required\",\n\t\t\tCheckName:    \"require_tls\",\n\t\t},\n\t}\n}\n\nfunc init() {\n\tcheck.RegisterStatelessCheck(\"require_tls\", modconfig.FailAction{Reject: true}, requireTLS, nil, nil, nil)\n}\n"
  },
  {
    "path": "internal/check/rspamd/rspamd.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage rspamd\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/tls\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/emersion/go-message/textproto\"\n\t\"github.com/foxcpp/maddy/framework/buffer\"\n\t\"github.com/foxcpp/maddy/framework/config\"\n\tmodconfig \"github.com/foxcpp/maddy/framework/config/module\"\n\ttls2 \"github.com/foxcpp/maddy/framework/config/tls\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/exterrors\"\n\t\"github.com/foxcpp/maddy/framework/log\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n\t\"github.com/foxcpp/maddy/internal/target\"\n)\n\nconst modName = \"check.rspamd\"\n\ntype Check struct {\n\tinstName string\n\tlog      *log.Logger\n\n\tapiPath    string\n\tflags      string\n\tsettingsID string\n\ttag        string\n\tmtaName    string\n\n\tioErrAction       modconfig.FailAction\n\terrorRespAction   modconfig.FailAction\n\taddHdrAction      modconfig.FailAction\n\trewriteSubjAction modconfig.FailAction\n\trejectAction      modconfig.FailAction\n\tsoftRejectAction  modconfig.FailAction\n\n\tclient *http.Client\n}\n\nfunc New(c *container.C, modName, instName string) (module.Module, error) {\n\tchk := &Check{\n\t\tinstName: instName,\n\t\tclient:   http.DefaultClient,\n\t\tlog:      c.DefaultLogger.Sublogger(modName),\n\t}\n\n\treturn chk, nil\n}\n\nfunc (c *Check) Name() string {\n\treturn modName\n}\n\nfunc (c *Check) InstanceName() string {\n\treturn c.instName\n}\n\nfunc (c *Check) Configure(inlineArgs []string, cfg *config.Map) error {\n\tswitch len(inlineArgs) {\n\tcase 1:\n\t\tc.apiPath = inlineArgs[0]\n\tcase 0:\n\t\tc.apiPath = \"http://127.0.0.1:11333\"\n\tdefault:\n\t\treturn fmt.Errorf(\"%s: unexpected amount of inline arguments\", modName)\n\t}\n\n\tvar (\n\t\ttlsConfig *tls.Config\n\t\tflags     []string\n\t)\n\n\tcfg.Custom(\"tls_client\", true, false, func() (interface{}, error) {\n\t\treturn &tls.Config{}, nil\n\t}, tls2.TLSClientBlock, &tlsConfig)\n\tcfg.String(\"api_path\", false, false, c.apiPath, &c.apiPath)\n\tcfg.String(\"settings_id\", false, false, \"\", &c.settingsID)\n\tcfg.String(\"tag\", false, false, \"maddy\", &c.tag)\n\tcfg.String(\"hostname\", true, false, \"\", &c.mtaName)\n\tcfg.Custom(\"io_error_action\", false, false,\n\t\tfunc() (interface{}, error) {\n\t\t\treturn modconfig.FailAction{}, nil\n\t\t}, modconfig.FailActionDirective, &c.ioErrAction)\n\tcfg.Custom(\"error_resp_action\", false, false,\n\t\tfunc() (interface{}, error) {\n\t\t\treturn modconfig.FailAction{}, nil\n\t\t}, modconfig.FailActionDirective, &c.errorRespAction)\n\tcfg.Custom(\"add_header_action\", false, false,\n\t\tfunc() (interface{}, error) {\n\t\t\treturn modconfig.FailAction{Quarantine: true}, nil\n\t\t}, modconfig.FailActionDirective, &c.addHdrAction)\n\tcfg.Custom(\"rewrite_subj_action\", false, false,\n\t\tfunc() (interface{}, error) {\n\t\t\treturn modconfig.FailAction{Quarantine: true}, nil\n\t\t}, modconfig.FailActionDirective, &c.rewriteSubjAction)\n\tcfg.Custom(\"reject_action\", false, false,\n\t\tfunc() (interface{}, error) {\n\t\t\treturn modconfig.FailAction{Reject: true}, nil\n\t\t}, modconfig.FailActionDirective, &c.rejectAction)\n\tcfg.Custom(\"soft_reject_action\", false, false,\n\t\tfunc() (interface{}, error) {\n\t\t\treturn modconfig.FailAction{Reject: true}, nil\n\t\t}, modconfig.FailActionDirective, &c.softRejectAction)\n\n\tcfg.StringList(\"flags\", false, false, []string{\"pass_all\"}, &flags)\n\tif _, err := cfg.Process(); err != nil {\n\t\treturn err\n\t}\n\n\tc.client = &http.Client{\n\t\tTransport: &http.Transport{\n\t\t\tTLSClientConfig: tlsConfig,\n\t\t},\n\t}\n\tc.flags = strings.Join(flags, \",\")\n\n\treturn nil\n}\n\ntype state struct {\n\tc       *Check\n\tmsgMeta *module.MsgMetadata\n\tlog     *log.Logger\n\n\tmailFrom string\n\trcpt     []string\n}\n\nfunc (c *Check) CheckStateForMsg(ctx context.Context, msgMeta *module.MsgMetadata) (module.CheckState, error) {\n\treturn &state{\n\t\tc:       c,\n\t\tmsgMeta: msgMeta,\n\t\tlog:     target.DeliveryLogger(c.log, msgMeta),\n\t}, nil\n}\n\nfunc (s *state) CheckConnection(ctx context.Context) module.CheckResult {\n\treturn module.CheckResult{}\n}\n\nfunc (s *state) CheckSender(ctx context.Context, addr string) module.CheckResult {\n\ts.mailFrom = addr\n\treturn module.CheckResult{}\n}\n\nfunc (s *state) CheckRcpt(ctx context.Context, addr string) module.CheckResult {\n\ts.rcpt = append(s.rcpt, addr)\n\treturn module.CheckResult{}\n}\n\nfunc addConnHeaders(r *http.Request, meta *module.MsgMetadata, mailFrom string, rcpts []string) {\n\tr.Header.Add(\"From\", mailFrom)\n\tfor _, rcpt := range rcpts {\n\t\tr.Header.Add(\"Rcpt\", rcpt)\n\t}\n\n\tr.Header.Add(\"Queue-ID\", meta.ID)\n\n\tconn := meta.Conn\n\tif conn != nil {\n\t\tif meta.Conn.AuthUser != \"\" {\n\t\t\tr.Header.Add(\"User\", meta.Conn.AuthUser)\n\t\t}\n\n\t\tif tcpAddr, ok := conn.RemoteAddr.(*net.TCPAddr); ok {\n\t\t\tr.Header.Add(\"IP\", tcpAddr.IP.String())\n\t\t}\n\t\tr.Header.Add(\"Helo\", conn.Hostname)\n\t\tname, err := conn.RDNSName.Get()\n\t\tif err == nil && name != nil {\n\t\t\tr.Header.Add(\"Hostname\", name.(string))\n\t\t}\n\n\t\tif conn.TLS.HandshakeComplete {\n\t\t\tr.Header.Add(\"TLS-Cipher\", tls.CipherSuiteName(conn.TLS.CipherSuite))\n\t\t\tswitch conn.TLS.Version {\n\t\t\tcase tls.VersionTLS13:\n\t\t\t\tr.Header.Add(\"TLS-Version\", \"1.3\")\n\t\t\tcase tls.VersionTLS12:\n\t\t\t\tr.Header.Add(\"TLS-Version\", \"1.2\")\n\t\t\tcase tls.VersionTLS11:\n\t\t\t\tr.Header.Add(\"TLS-Version\", \"1.1\")\n\t\t\tcase tls.VersionTLS10:\n\t\t\t\tr.Header.Add(\"TLS-Version\", \"1.0\")\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (s *state) CheckBody(ctx context.Context, hdr textproto.Header, body buffer.Buffer) module.CheckResult {\n\tbodyR, err := body.Open()\n\tif err != nil {\n\t\treturn module.CheckResult{\n\t\t\tReject: true,\n\t\t\tReason: exterrors.WithFields(err, map[string]interface{}{\"check\": modName}),\n\t\t}\n\t}\n\n\tvar buf bytes.Buffer\n\tif err := textproto.WriteHeader(&buf, hdr); err != nil {\n\t\treturn module.CheckResult{\n\t\t\tReject: true,\n\t\t\tReason: exterrors.WithFields(err, map[string]interface{}{\"check\": modName}),\n\t\t}\n\t}\n\n\tr, err := http.NewRequest(\"POST\", s.c.apiPath+\"/checkv2\", io.MultiReader(&buf, bodyR))\n\tif err != nil {\n\t\treturn module.CheckResult{\n\t\t\tReject: true,\n\t\t\tReason: exterrors.WithFields(err, map[string]interface{}{\"check\": modName}),\n\t\t}\n\t}\n\n\tr.Header.Add(\"Pass\", \"all\") // TODO: does that need to be configurable?\n\t// TODO: include version (needs maddy.Version moved somewhere to break circular dependency)\n\tr.Header.Add(\"User-Agent\", \"maddy\")\n\tif s.c.tag != \"\" {\n\t\tr.Header.Add(\"MTA-Tag\", s.c.tag)\n\t}\n\tif s.c.settingsID != \"\" {\n\t\tr.Header.Add(\"Settings-ID\", s.c.settingsID)\n\t}\n\tif s.c.mtaName != \"\" {\n\t\tr.Header.Add(\"MTA-Name\", s.c.mtaName)\n\t}\n\n\taddConnHeaders(r, s.msgMeta, s.mailFrom, s.rcpt)\n\tr.Header.Add(\"Content-Length\", strconv.Itoa(body.Len()))\n\n\tresp, err := s.c.client.Do(r)\n\tif err != nil {\n\t\treturn s.c.ioErrAction.Apply(module.CheckResult{\n\t\t\tReason: &exterrors.SMTPError{\n\t\t\t\tCode:         451,\n\t\t\t\tEnhancedCode: exterrors.EnhancedCode{4, 7, 0},\n\t\t\t\tMessage:      \"Internal error during policy check\",\n\t\t\t\tCheckName:    modName,\n\t\t\t\tErr:          err,\n\t\t\t},\n\t\t})\n\t}\n\tif resp.StatusCode/100 != 2 {\n\t\treturn s.c.errorRespAction.Apply(module.CheckResult{\n\t\t\tReason: &exterrors.SMTPError{\n\t\t\t\tCode:         451,\n\t\t\t\tEnhancedCode: exterrors.EnhancedCode{4, 7, 0},\n\t\t\t\tMessage:      \"Internal error during policy check\",\n\t\t\t\tCheckName:    modName,\n\t\t\t\tErr:          fmt.Errorf(\"HTTP %d\", resp.StatusCode),\n\t\t\t},\n\t\t})\n\t}\n\tdefer func() {\n\t\tif err := resp.Body.Close(); err != nil {\n\t\t\ts.log.Error(\"failed to close response body\", err)\n\t\t}\n\t}()\n\n\tvar respData response\n\tif err := json.NewDecoder(resp.Body).Decode(&respData); err != nil {\n\t\treturn s.c.ioErrAction.Apply(module.CheckResult{\n\t\t\tReason: &exterrors.SMTPError{\n\t\t\t\tCode:         451,\n\t\t\t\tEnhancedCode: exterrors.EnhancedCode{4, 9, 0},\n\t\t\t\tMessage:      \"Internal error during policy check\",\n\t\t\t\tCheckName:    modName,\n\t\t\t\tErr:          err,\n\t\t\t},\n\t\t})\n\t}\n\n\tswitch respData.Action {\n\tcase \"no action\":\n\t\treturn module.CheckResult{}\n\tcase \"greylist\":\n\t\t// uuh... TODO: Implement greylisting?\n\t\thdrAdd := textproto.Header{}\n\t\thdrAdd.Add(\"X-Spam-Score\", strconv.FormatFloat(respData.Score, 'f', 2, 64))\n\t\treturn module.CheckResult{\n\t\t\tHeader: hdrAdd,\n\t\t}\n\tcase \"add header\":\n\t\thdrAdd := textproto.Header{}\n\t\thdrAdd.Add(\"X-Spam-Flag\", \"Yes\")\n\t\thdrAdd.Add(\"X-Spam-Score\", strconv.FormatFloat(respData.Score, 'f', 2, 64))\n\t\treturn s.c.addHdrAction.Apply(module.CheckResult{\n\t\t\tReason: &exterrors.SMTPError{\n\t\t\t\tCode:         450,\n\t\t\t\tEnhancedCode: exterrors.EnhancedCode{4, 7, 0},\n\t\t\t\tMessage:      \"Message rejected due to local policy\",\n\t\t\t\tCheckName:    modName,\n\t\t\t\tMisc:         map[string]interface{}{\"action\": \"add header\"},\n\t\t\t},\n\t\t\tHeader: hdrAdd,\n\t\t})\n\tcase \"rewrite subject\":\n\t\thdrAdd := textproto.Header{}\n\t\thdrAdd.Add(\"X-Spam-Flag\", \"Yes\")\n\t\thdrAdd.Add(\"X-Spam-Score\", strconv.FormatFloat(respData.Score, 'f', 2, 64))\n\t\treturn s.c.rewriteSubjAction.Apply(module.CheckResult{\n\t\t\tReason: &exterrors.SMTPError{\n\t\t\t\tCode:         450,\n\t\t\t\tEnhancedCode: exterrors.EnhancedCode{4, 7, 0},\n\t\t\t\tMessage:      \"Message rejected due to local policy\",\n\t\t\t\tCheckName:    modName,\n\t\t\t\tMisc:         map[string]interface{}{\"action\": \"rewrite subject\"},\n\t\t\t},\n\t\t\tHeader: hdrAdd,\n\t\t})\n\tcase \"soft reject\":\n\t\treturn s.c.softRejectAction.Apply(module.CheckResult{\n\t\t\tReject: true,\n\t\t\tReason: &exterrors.SMTPError{\n\t\t\t\tCode:         450,\n\t\t\t\tEnhancedCode: exterrors.EnhancedCode{4, 7, 0},\n\t\t\t\tMessage:      \"Message rejected due to local policy\",\n\t\t\t\tCheckName:    modName,\n\t\t\t\tMisc:         map[string]interface{}{\"action\": \"soft reject\"},\n\t\t\t},\n\t\t})\n\tcase \"reject\":\n\t\treturn s.c.rejectAction.Apply(module.CheckResult{\n\t\t\tReject: true,\n\t\t\tReason: &exterrors.SMTPError{\n\t\t\t\tCode:         550,\n\t\t\t\tEnhancedCode: exterrors.EnhancedCode{5, 7, 0},\n\t\t\t\tMessage:      \"Message rejected due to local policy\",\n\t\t\t\tCheckName:    modName,\n\t\t\t\tMisc:         map[string]interface{}{\"action\": \"reject\"},\n\t\t\t},\n\t\t})\n\t}\n\n\ts.log.Msg(\"unhandled action\", \"action\", respData.Action)\n\n\treturn module.CheckResult{}\n}\n\ntype response struct {\n\tScore   float64 `json:\"score\"`\n\tAction  string  `json:\"action\"`\n\tSubject string  `json:\"subject\"`\n\tSymbols map[string]struct {\n\t\tName  string  `json:\"name\"`\n\t\tScore float64 `json:\"score\"`\n\t}\n}\n\nfunc (s *state) Close() error {\n\treturn nil\n}\n\nfunc init() {\n\tmodules.Register(modName, New)\n}\n"
  },
  {
    "path": "internal/check/skeleton.go",
    "content": "//go:build ignore\n// +build ignore\n\n/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\n/*\nThis is example of a minimal stateful check module implementation.\nSee HACKING.md in the repo root for implementation recommendations.\n*/\n\npackage directory_name_here\n\nimport (\n\t\"context\"\n\n\t\"github.com/emersion/go-message/textproto\"\n\t\"github.com/foxcpp/maddy/framework/buffer\"\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/log\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n\t\"github.com/foxcpp/maddy/internal/target\"\n)\n\nconst modName = \"check_things\"\n\ntype Check struct {\n\tinstName string\n\tlog      log.Logger\n}\n\nfunc New(modName, instName string, aliases, inlineArgs []string) (module.Module, error) {\n\treturn &Check{\n\t\tinstName: instName,\n\t}, nil\n}\n\nfunc (c *Check) Name() string {\n\treturn modName\n}\n\nfunc (c *Check) InstanceName() string {\n\treturn c.instName\n}\n\nfunc (c *Check) Init(cfg *config.Map) error {\n\treturn nil\n}\n\ntype state struct {\n\tc       *Check\n\tmsgMeta *module.MsgMetadata\n\tlog     log.Logger\n}\n\nfunc (c *Check) CheckStateForMsg(ctx context.Context, msgMeta *module.MsgMetadata) (module.CheckState, error) {\n\treturn &state{\n\t\tc:       c,\n\t\tmsgMeta: msgMeta,\n\t\tlog:     target.DeliveryLogger(c.log, msgMeta),\n\t}, nil\n}\n\nfunc (s *state) CheckConnection(ctx context.Context) module.CheckResult {\n\treturn module.CheckResult{}\n}\n\nfunc (s *state) CheckSender(ctx context.Context, addr string) module.CheckResult {\n\treturn module.CheckResult{}\n}\n\nfunc (s *state) CheckRcpt(ctx context.Context, addr string) module.CheckResult {\n\treturn module.CheckResult{}\n}\n\nfunc (s *state) CheckBody(ctx context.Context, hdr textproto.Header, body buffer.Buffer) module.CheckResult {\n\treturn module.CheckResult{}\n}\n\nfunc (s *state) Close() error {\n\treturn nil\n}\n\nfunc init() {\n\tmodules.Register(modName, New)\n}\n"
  },
  {
    "path": "internal/check/spf/spf.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage spf\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"runtime/debug\"\n\t\"runtime/trace\"\n\n\t\"blitiri.com.ar/go/spf\"\n\t\"github.com/emersion/go-message/textproto\"\n\t\"github.com/emersion/go-msgauth/authres\"\n\t\"github.com/emersion/go-msgauth/dmarc\"\n\t\"github.com/foxcpp/maddy/framework/address\"\n\t\"github.com/foxcpp/maddy/framework/buffer\"\n\t\"github.com/foxcpp/maddy/framework/config\"\n\tmodconfig \"github.com/foxcpp/maddy/framework/config/module\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/dns\"\n\t\"github.com/foxcpp/maddy/framework/exterrors\"\n\t\"github.com/foxcpp/maddy/framework/log\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n\tmaddydmarc \"github.com/foxcpp/maddy/internal/dmarc\"\n\t\"github.com/foxcpp/maddy/internal/target\"\n\t\"golang.org/x/net/idna\"\n)\n\nconst modName = \"check.spf\"\n\ntype Check struct {\n\tinstName     string\n\tenforceEarly bool\n\n\tnoneAction     modconfig.FailAction\n\tneutralAction  modconfig.FailAction\n\tfailAction     modconfig.FailAction\n\tsoftfailAction modconfig.FailAction\n\tpermerrAction  modconfig.FailAction\n\ttemperrAction  modconfig.FailAction\n\n\tlog      *log.Logger\n\tresolver dns.Resolver\n}\n\nfunc New(c *container.C, _, instName string) (module.Module, error) {\n\treturn &Check{\n\t\tinstName: instName,\n\t\tlog:      c.DefaultLogger.Sublogger(modName),\n\t\tresolver: dns.DefaultResolver(),\n\t}, nil\n}\n\nfunc (c *Check) Name() string {\n\treturn modName\n}\n\nfunc (c *Check) InstanceName() string {\n\treturn c.instName\n}\n\nfunc (c *Check) Configure(inlineArgs []string, cfg *config.Map) error {\n\tcfg.Bool(\"debug\", true, false, &c.log.Debug)\n\tcfg.Bool(\"enforce_early\", true, false, &c.enforceEarly)\n\tcfg.Custom(\"none_action\", false, false,\n\t\tfunc() (interface{}, error) {\n\t\t\treturn modconfig.FailAction{}, nil\n\t\t}, modconfig.FailActionDirective, &c.noneAction)\n\tcfg.Custom(\"neutral_action\", false, false,\n\t\tfunc() (interface{}, error) {\n\t\t\treturn modconfig.FailAction{}, nil\n\t\t}, modconfig.FailActionDirective, &c.neutralAction)\n\tcfg.Custom(\"fail_action\", false, false,\n\t\tfunc() (interface{}, error) {\n\t\t\treturn modconfig.FailAction{Quarantine: true}, nil\n\t\t}, modconfig.FailActionDirective, &c.failAction)\n\tcfg.Custom(\"softfail_action\", false, false,\n\t\tfunc() (interface{}, error) {\n\t\t\treturn modconfig.FailAction{}, nil\n\t\t}, modconfig.FailActionDirective, &c.softfailAction)\n\tcfg.Custom(\"permerr_action\", false, false,\n\t\tfunc() (interface{}, error) {\n\t\t\treturn modconfig.FailAction{}, nil\n\t\t}, modconfig.FailActionDirective, &c.permerrAction)\n\tcfg.Custom(\"temperr_action\", false, false,\n\t\tfunc() (interface{}, error) {\n\t\t\treturn modconfig.FailAction{}, nil\n\t\t}, modconfig.FailActionDirective, &c.temperrAction)\n\t_, err := cfg.Process()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\ntype spfRes struct {\n\tres spf.Result\n\terr error\n}\n\ntype state struct {\n\tc        *Check\n\tmsgMeta  *module.MsgMetadata\n\tspfFetch chan spfRes\n\tlog      *log.Logger\n\n\tskip bool\n}\n\nfunc (c *Check) CheckStateForMsg(ctx context.Context, msgMeta *module.MsgMetadata) (module.CheckState, error) {\n\treturn &state{\n\t\tc:        c,\n\t\tmsgMeta:  msgMeta,\n\t\tspfFetch: make(chan spfRes, 1),\n\t\tlog:      target.DeliveryLogger(c.log, msgMeta),\n\t}, nil\n}\n\nfunc (s *state) spfResult(res spf.Result, err error) module.CheckResult {\n\t_, fromDomain, _ := address.Split(s.msgMeta.OriginalFrom)\n\tspfAuth := &authres.SPFResult{\n\t\tValue: authres.ResultNone,\n\t\tHelo:  s.msgMeta.Conn.Hostname,\n\t\tFrom:  fromDomain,\n\t}\n\n\tif err != nil {\n\t\tspfAuth.Reason = err.Error()\n\t} else if res == spf.None {\n\t\tspfAuth.Reason = \"no policy\"\n\t}\n\n\tswitch res {\n\tcase spf.None:\n\t\tspfAuth.Value = authres.ResultNone\n\t\treturn s.c.noneAction.Apply(module.CheckResult{\n\t\t\tReason: &exterrors.SMTPError{\n\t\t\t\tCode:         550,\n\t\t\t\tEnhancedCode: exterrors.EnhancedCode{5, 7, 23},\n\t\t\t\tMessage:      \"No SPF policy\",\n\t\t\t\tCheckName:    modName,\n\t\t\t\tErr:          err,\n\t\t\t},\n\t\t\tAuthResult: []authres.Result{spfAuth},\n\t\t})\n\tcase spf.Neutral:\n\t\tspfAuth.Value = authres.ResultNeutral\n\t\treturn s.c.neutralAction.Apply(module.CheckResult{\n\t\t\tReason: &exterrors.SMTPError{\n\t\t\t\tCode:         550,\n\t\t\t\tEnhancedCode: exterrors.EnhancedCode{5, 7, 23},\n\t\t\t\tMessage:      \"Neutral SPF result is not permitted\",\n\t\t\t\tCheckName:    modName,\n\t\t\t\tErr:          err,\n\t\t\t},\n\t\t\tAuthResult: []authres.Result{spfAuth},\n\t\t})\n\tcase spf.Pass:\n\t\tspfAuth.Value = authres.ResultPass\n\t\treturn module.CheckResult{AuthResult: []authres.Result{spfAuth}}\n\tcase spf.Fail:\n\t\tspfAuth.Value = authres.ResultFail\n\t\treturn s.c.failAction.Apply(module.CheckResult{\n\t\t\tReason: &exterrors.SMTPError{\n\t\t\t\tCode:         550,\n\t\t\t\tEnhancedCode: exterrors.EnhancedCode{5, 7, 23},\n\t\t\t\tMessage:      \"SPF authentication failed\",\n\t\t\t\tCheckName:    modName,\n\t\t\t\tErr:          err,\n\t\t\t},\n\t\t\tAuthResult: []authres.Result{spfAuth},\n\t\t})\n\tcase spf.SoftFail:\n\t\tspfAuth.Value = authres.ResultSoftFail\n\t\treturn s.c.softfailAction.Apply(module.CheckResult{\n\t\t\tReason: &exterrors.SMTPError{\n\t\t\t\tCode:         550,\n\t\t\t\tEnhancedCode: exterrors.EnhancedCode{5, 7, 23},\n\t\t\t\tMessage:      \"SPF authentication soft-failed\",\n\t\t\t\tCheckName:    modName,\n\t\t\t\tErr:          err,\n\t\t\t},\n\t\t\tAuthResult: []authres.Result{spfAuth},\n\t\t})\n\tcase spf.TempError:\n\t\tspfAuth.Value = authres.ResultTempError\n\t\treturn s.c.temperrAction.Apply(module.CheckResult{\n\t\t\tReason: &exterrors.SMTPError{\n\t\t\t\tCode:         451,\n\t\t\t\tEnhancedCode: exterrors.EnhancedCode{4, 7, 23},\n\t\t\t\tMessage:      \"SPF authentication failed with a temporary error\",\n\t\t\t\tCheckName:    modName,\n\t\t\t\tErr:          err,\n\t\t\t},\n\t\t\tAuthResult: []authres.Result{spfAuth},\n\t\t})\n\tcase spf.PermError:\n\t\tspfAuth.Value = authres.ResultPermError\n\t\treturn s.c.permerrAction.Apply(module.CheckResult{\n\t\t\tReason: &exterrors.SMTPError{\n\t\t\t\tCode:         550,\n\t\t\t\tEnhancedCode: exterrors.EnhancedCode{5, 7, 23},\n\t\t\t\tMessage:      \"SPF authentication failed with a permanent error\",\n\t\t\t\tCheckName:    modName,\n\t\t\t\tErr:          err,\n\t\t\t},\n\t\t\tAuthResult: []authres.Result{spfAuth},\n\t\t})\n\t}\n\n\treturn module.CheckResult{\n\t\tReason: &exterrors.SMTPError{\n\t\t\tCode:         550,\n\t\t\tEnhancedCode: exterrors.EnhancedCode{4, 7, 23},\n\t\t\tMessage:      fmt.Sprintf(\"Unknown SPF status: %s\", res),\n\t\t\tCheckName:    modName,\n\t\t\tErr:          err,\n\t\t},\n\t\tAuthResult: []authres.Result{spfAuth},\n\t}\n}\n\nfunc (s *state) relyOnDMARC(ctx context.Context, hdr textproto.Header) bool {\n\tfromDomain, err := maddydmarc.ExtractFromDomain(hdr)\n\tif err != nil {\n\t\ts.log.Error(\"DMARC domains extract\", err)\n\t\treturn false\n\t}\n\n\tpolicyDomain, record, err := maddydmarc.FetchRecord(ctx, s.c.resolver, fromDomain)\n\tif err != nil {\n\t\ts.log.Error(\"DMARC fetch\", err, \"from_domain\", fromDomain)\n\t\treturn false\n\t}\n\tif record == nil {\n\t\treturn false\n\t}\n\n\tpolicy := record.Policy\n\t// We check for subdomain using non-equality since fromDomain is either the\n\t// subdomain of policyDomain or policyDomain itself (due to the way\n\t// FetchRecord handles it).\n\tif !dns.Equal(policyDomain, fromDomain) && record.SubdomainPolicy != \"\" {\n\t\tpolicy = record.SubdomainPolicy\n\t}\n\n\treturn policy != dmarc.PolicyNone\n}\n\nfunc prepareMailFrom(from string) (string, error) {\n\t// INTERNATIONALIZATION: RFC 8616, Section 4\n\t// Hostname is already in A-labels per SMTPUTF8 requirement.\n\t// MAIL FROM domain should be converted to A-labels before doing\n\t// anything.\n\tfromMbox, fromDomain, err := address.Split(from)\n\tif err != nil {\n\t\treturn \"\", &exterrors.SMTPError{\n\t\t\tCode:         550,\n\t\t\tEnhancedCode: exterrors.EnhancedCode{5, 1, 7},\n\t\t\tMessage:      \"Malformed address\",\n\t\t\tCheckName:    \"spf\",\n\t\t}\n\t}\n\tfromDomain, err = idna.ToASCII(fromDomain)\n\tif err != nil {\n\t\treturn \"\", &exterrors.SMTPError{\n\t\t\tCode:         550,\n\t\t\tEnhancedCode: exterrors.EnhancedCode{5, 1, 7},\n\t\t\tMessage:      \"Malformed address\",\n\t\t\tCheckName:    \"spf\",\n\t\t}\n\t}\n\n\t// %{s} and %{l} do not match anything if it is non-ASCII.\n\t// Since spf lib does not seem to care, strip it.\n\tif !address.IsASCII(fromMbox) {\n\t\tfromMbox = \"\"\n\t}\n\n\treturn fromMbox + \"@\" + dns.FQDN(fromDomain), nil\n}\n\nfunc (s *state) CheckConnection(ctx context.Context) module.CheckResult {\n\tdefer trace.StartRegion(ctx, \"check.spf/CheckConnection\").End()\n\n\tif s.msgMeta.Conn == nil {\n\t\ts.skip = true\n\t\ts.log.Println(\"locally generated message, skipping\")\n\t\treturn module.CheckResult{}\n\t}\n\n\tip, ok := s.msgMeta.Conn.RemoteAddr.(*net.TCPAddr)\n\tif !ok {\n\t\ts.skip = true\n\t\ts.log.Println(\"non-IP SrcAddr\")\n\t\treturn module.CheckResult{}\n\t}\n\n\tmailFromOriginal := s.msgMeta.OriginalFrom\n\tif mailFromOriginal == \"\" {\n\t\t// RFC 7208 Section 2.4.\n\t\t// >When the reverse-path is null, this document\n\t\t// >defines the \"MAIL FROM\" identity to be the mailbox composed of the\n\t\t// >local-part \"postmaster\" and the \"HELO\" identity (which might or might\n\t\t// >not have been checked separately before).\n\t\tmailFromOriginal = \"postmaster@\" + s.msgMeta.Conn.Hostname\n\t}\n\n\tmailFrom, err := prepareMailFrom(mailFromOriginal)\n\tif err != nil {\n\t\ts.skip = true\n\t\treturn module.CheckResult{\n\t\t\tReason: err,\n\t\t\tReject: true,\n\t\t}\n\t}\n\n\tif s.c.enforceEarly {\n\t\tres, err := spf.CheckHostWithSender(ip.IP,\n\t\t\tdns.FQDN(s.msgMeta.Conn.Hostname), mailFrom,\n\t\t\tspf.WithContext(ctx), spf.WithResolver(s.c.resolver))\n\t\ts.log.Debugf(\"result: %s (%v)\", res, err)\n\t\treturn s.spfResult(res, err)\n\t}\n\n\t// We start evaluation in parallel to other message processing,\n\t// once we get the body, we fetch DMARC policy and see if it exists\n\t// and not p=none. In that case, we rely on DMARC alignment to define result.\n\t// Otherwise, we take action based on SPF only.\n\n\tgo func() {\n\t\tdefer func() {\n\t\t\tif err := recover(); err != nil {\n\t\t\t\tstack := debug.Stack()\n\t\t\t\tlog.Printf(\"panic during spf.CheckHostWithSender: %v\\n%s\", err, stack)\n\t\t\t\tclose(s.spfFetch)\n\t\t\t}\n\t\t}()\n\n\t\tdefer trace.StartRegion(ctx, \"check.spf/CheckConnection (Async)\").End()\n\n\t\tres, err := spf.CheckHostWithSender(ip.IP, dns.FQDN(s.msgMeta.Conn.Hostname), mailFrom,\n\t\t\tspf.WithContext(ctx), spf.WithResolver(s.c.resolver))\n\t\ts.log.Debugf(\"result: %s (%v)\", res, err)\n\t\ts.spfFetch <- spfRes{res, err}\n\t}()\n\n\treturn module.CheckResult{}\n}\n\nfunc (s *state) CheckSender(ctx context.Context, mailFrom string) module.CheckResult {\n\treturn module.CheckResult{}\n}\n\nfunc (s *state) CheckRcpt(ctx context.Context, rcptTo string) module.CheckResult {\n\treturn module.CheckResult{}\n}\n\nfunc (s *state) CheckBody(ctx context.Context, header textproto.Header, body buffer.Buffer) module.CheckResult {\n\tif s.c.enforceEarly || s.skip {\n\t\t// Already applied in CheckConnection.\n\t\treturn module.CheckResult{}\n\t}\n\n\tdefer trace.StartRegion(ctx, \"check.spf/CheckBody\").End()\n\n\tres, ok := <-s.spfFetch\n\tif !ok {\n\t\treturn module.CheckResult{\n\t\t\tReject: true,\n\t\t\tReason: exterrors.WithTemporary(\n\t\t\t\texterrors.WithFields(errors.New(\"panic recovered\"), map[string]interface{}{\n\t\t\t\t\t\"check\":    \"spf\",\n\t\t\t\t\t\"smtp_msg\": \"Internal error during policy check\",\n\t\t\t\t}),\n\t\t\t\ttrue,\n\t\t\t),\n\t\t}\n\t}\n\tif s.relyOnDMARC(ctx, header) {\n\t\tif res.res != spf.Pass {\n\t\t\ts.log.Msg(\"deferring action due to a DMARC policy\", \"result\", res.res, \"err\", res.err)\n\t\t} else {\n\t\t\ts.log.DebugMsg(\"deferring action due to a DMARC policy\", \"result\", res.res, \"err\", res.err)\n\t\t}\n\n\t\tcheckRes := s.spfResult(res.res, res.err)\n\t\tcheckRes.Quarantine = false\n\t\tcheckRes.Reject = false\n\t\treturn checkRes\n\t}\n\n\treturn s.spfResult(res.res, res.err)\n}\n\nfunc (s *state) Close() error {\n\treturn nil\n}\n\nfunc init() {\n\tmodules.Register(modName, New)\n}\n"
  },
  {
    "path": "internal/check/stateless_check.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage check\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"runtime/trace\"\n\n\t\"github.com/emersion/go-message/textproto\"\n\t\"github.com/foxcpp/maddy/framework/buffer\"\n\t\"github.com/foxcpp/maddy/framework/config\"\n\tmodconfig \"github.com/foxcpp/maddy/framework/config/module\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/dns\"\n\t\"github.com/foxcpp/maddy/framework/log\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n\t\"github.com/foxcpp/maddy/internal/target\"\n)\n\ntype (\n\tStatelessCheckContext struct {\n\t\t// Embedded context.Context value, used for tracing, cancellation and\n\t\t// timeouts.\n\t\tcontext.Context\n\n\t\t// Resolver that should be used by the check for DNS queries.\n\t\tResolver dns.Resolver\n\n\t\tMsgMeta *module.MsgMetadata\n\n\t\t// Logger that should be used by the check for logging, note that it is\n\t\t// already wrapped to append Msg ID to all messages so check code\n\t\t// should not do the same.\n\t\tLogger *log.Logger\n\t}\n\tFuncConnCheck   func(checkContext StatelessCheckContext) module.CheckResult\n\tFuncSenderCheck func(checkContext StatelessCheckContext, mailFrom string) module.CheckResult\n\tFuncRcptCheck   func(checkContext StatelessCheckContext, rcptTo string) module.CheckResult\n\tFuncBodyCheck   func(checkContext StatelessCheckContext, header textproto.Header, body buffer.Buffer) module.CheckResult\n)\n\ntype statelessCheck struct {\n\tmodName  string\n\tinstName string\n\tresolver dns.Resolver\n\tlogger   *log.Logger\n\n\t// One used by Init if config option is not passed by a user.\n\tdefaultFailAction modconfig.FailAction\n\t// The actual fail action that should be applied.\n\tfailAction modconfig.FailAction\n\n\tconnCheck   FuncConnCheck\n\tsenderCheck FuncSenderCheck\n\trcptCheck   FuncRcptCheck\n\tbodyCheck   FuncBodyCheck\n}\n\ntype statelessCheckState struct {\n\tc       *statelessCheck\n\tmsgMeta *module.MsgMetadata\n}\n\nfunc (s *statelessCheckState) String() string {\n\treturn s.c.modName + \":\" + s.c.instName\n}\n\nfunc (s *statelessCheckState) CheckConnection(ctx context.Context) module.CheckResult {\n\tif s.c.connCheck == nil {\n\t\treturn module.CheckResult{}\n\t}\n\tdefer trace.StartRegion(ctx, s.c.modName+\"/CheckConnection\").End()\n\n\toriginalRes := s.c.connCheck(StatelessCheckContext{\n\t\tContext:  ctx,\n\t\tResolver: s.c.resolver,\n\t\tMsgMeta:  s.msgMeta,\n\t\tLogger:   target.DeliveryLogger(s.c.logger, s.msgMeta),\n\t})\n\treturn s.c.failAction.Apply(originalRes)\n}\n\nfunc (s *statelessCheckState) CheckSender(ctx context.Context, mailFrom string) module.CheckResult {\n\tif s.c.senderCheck == nil {\n\t\treturn module.CheckResult{}\n\t}\n\tdefer trace.StartRegion(ctx, s.c.modName+\"/CheckSender\").End()\n\n\toriginalRes := s.c.senderCheck(StatelessCheckContext{\n\t\tContext:  ctx,\n\t\tResolver: s.c.resolver,\n\t\tMsgMeta:  s.msgMeta,\n\t\tLogger:   target.DeliveryLogger(s.c.logger, s.msgMeta),\n\t}, mailFrom)\n\treturn s.c.failAction.Apply(originalRes)\n}\n\nfunc (s *statelessCheckState) CheckRcpt(ctx context.Context, rcptTo string) module.CheckResult {\n\tif s.c.rcptCheck == nil {\n\t\treturn module.CheckResult{}\n\t}\n\tdefer trace.StartRegion(ctx, s.c.modName+\"/CheckRcpt\").End()\n\n\toriginalRes := s.c.rcptCheck(StatelessCheckContext{\n\t\tContext:  ctx,\n\t\tResolver: s.c.resolver,\n\t\tMsgMeta:  s.msgMeta,\n\t\tLogger:   target.DeliveryLogger(s.c.logger, s.msgMeta),\n\t}, rcptTo)\n\treturn s.c.failAction.Apply(originalRes)\n}\n\nfunc (s *statelessCheckState) CheckBody(ctx context.Context, header textproto.Header, body buffer.Buffer) module.CheckResult {\n\tif s.c.bodyCheck == nil {\n\t\treturn module.CheckResult{}\n\t}\n\tdefer trace.StartRegion(ctx, s.c.modName+\"/CheckBody\").End()\n\n\toriginalRes := s.c.bodyCheck(StatelessCheckContext{\n\t\tContext:  ctx,\n\t\tResolver: s.c.resolver,\n\t\tMsgMeta:  s.msgMeta,\n\t\tLogger:   target.DeliveryLogger(s.c.logger, s.msgMeta),\n\t}, header, body)\n\treturn s.c.failAction.Apply(originalRes)\n}\n\nfunc (s *statelessCheckState) Close() error {\n\treturn nil\n}\n\nfunc (c *statelessCheck) CheckStateForMsg(ctx context.Context, msgMeta *module.MsgMetadata) (module.CheckState, error) {\n\treturn &statelessCheckState{\n\t\tc:       c,\n\t\tmsgMeta: msgMeta,\n\t}, nil\n}\n\nfunc (c *statelessCheck) Configure(inlineArgs []string, cfg *config.Map) error {\n\tif len(inlineArgs) != 0 {\n\t\treturn fmt.Errorf(\"%s: inline arguments are not used\", c.modName)\n\t}\n\n\tcfg.Bool(\"debug\", true, false, &c.logger.Debug)\n\tcfg.Custom(\"fail_action\", false, false,\n\t\tfunc() (interface{}, error) {\n\t\t\treturn c.defaultFailAction, nil\n\t\t}, modconfig.FailActionDirective, &c.failAction)\n\t_, err := cfg.Process()\n\treturn err\n}\n\nfunc (c *statelessCheck) Name() string {\n\treturn c.modName\n}\n\nfunc (c *statelessCheck) InstanceName() string {\n\treturn c.instName\n}\n\n// RegisterStatelessCheck is helper function to create stateless message check modules\n// that run one simple check during one stage.\n//\n// It creates the module and its instance with the specified name that implement module.Check interface\n// and runs passed functions when corresponding module.CheckState methods are called.\n//\n// Note about CheckResult that is returned by the functions:\n// StatelessCheck supports different action types based on the user configuration, but the particular check\n// code doesn't need to know about it. It should assume that it is always \"Reject\" and hence it should\n// populate Reason field of the result object with the relevant error description.\nfunc RegisterStatelessCheck(name string, defaultFailAction modconfig.FailAction, connCheck FuncConnCheck, senderCheck FuncSenderCheck, rcptCheck FuncRcptCheck, bodyCheck FuncBodyCheck) {\n\tmodules.Register(name, func(c *container.C, modName, instName string) (module.Module, error) {\n\t\treturn &statelessCheck{\n\t\t\tmodName:  modName,\n\t\t\tinstName: instName,\n\t\t\tresolver: dns.DefaultResolver(),\n\t\t\tlogger:   c.DefaultLogger.Sublogger(modName),\n\n\t\t\tdefaultFailAction: defaultFailAction,\n\n\t\t\tconnCheck:   connCheck,\n\t\t\tsenderCheck: senderCheck,\n\t\t\trcptCheck:   rcptCheck,\n\t\t\tbodyCheck:   bodyCheck,\n\t\t}, nil\n\t})\n}\n"
  },
  {
    "path": "internal/cli/app.go",
    "content": "package maddycli\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/foxcpp/maddy/framework/log\"\n\t\"github.com/urfave/cli/v2\"\n)\n\nvar app *cli.App\n\nfunc init() {\n\tapp = cli.NewApp()\n\tapp.Usage = \"composable all-in-one mail server\"\n\tapp.Description = `Maddy is Mail Transfer agent (MTA), Mail Delivery Agent (MDA), Mail Submission\nAgent (MSA), IMAP server and a set of other essential protocols/schemes\nnecessary to run secure email server implemented in one executable.\n\nThis executable can be used to start the server ('run') and to manipulate\ndatabases used by it (all other subcommands).\n`\n\tapp.Authors = []*cli.Author{\n\t\t{\n\t\t\tName:  \"Maddy Mail Server maintainers & contributors\",\n\t\t\tEmail: \"~foxcpp/maddy@lists.sr.ht\",\n\t\t},\n\t}\n\tapp.ExitErrHandler = func(c *cli.Context, err error) {\n\t\tif err == nil {\n\t\t\treturn\n\t\t}\n\n\t\tvar exitErr cli.ExitCoder\n\t\tif errors.As(err, &exitErr) {\n\t\t\tif err.Error() != \"\" {\n\t\t\t\tif _, ok := exitErr.(cli.ErrorFormatter); ok {\n\t\t\t\t\t_, _ = fmt.Fprintf(os.Stderr, \"Error: %+v\\n\", err)\n\t\t\t\t} else {\n\t\t\t\t\t_, _ = fmt.Fprintln(os.Stderr, \"Error:\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t\tcli.OsExiter(exitErr.ExitCode())\n\t\t\treturn\n\t\t}\n\t}\n\tapp.EnableBashCompletion = true\n\tapp.Commands = []*cli.Command{\n\t\t{\n\t\t\tName:   \"generate-man\",\n\t\t\tHidden: true,\n\t\t\tAction: func(c *cli.Context) error {\n\t\t\t\tman, err := app.ToMan()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tfmt.Println(man)\n\t\t\t\treturn nil\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:   \"generate-fish-completion\",\n\t\t\tHidden: true,\n\t\t\tAction: func(c *cli.Context) error {\n\t\t\t\tcp, err := app.ToFishCompletion()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tfmt.Println(cp)\n\t\t\t\treturn nil\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc AddGlobalFlag(f cli.Flag) {\n\tapp.Flags = append(app.Flags, f)\n}\n\nfunc AddSubcommand(cmd *cli.Command) {\n\tapp.Commands = append(app.Commands, cmd)\n}\n\n// RunWithoutExit is like Run but returns exit code instead of calling os.Exit\n// To be used in maddy.cover.\nfunc RunWithoutExit() int {\n\tcode := 0\n\n\tcli.OsExiter = func(c int) { code = c }\n\tdefer func() {\n\t\tcli.OsExiter = os.Exit\n\t}()\n\n\tRun()\n\n\treturn code\n}\n\nfunc Run() {\n\tmapStdlibFlags(app)\n\n\t// Actual entry point is registered in maddy.go.\n\n\tif err := app.Run(os.Args); err != nil {\n\t\tlog.DefaultLogger.Error(\"app.Run failed\", err)\n\t}\n}\n"
  },
  {
    "path": "internal/cli/clitools/clitools.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage clitools\n\nimport (\n\t\"bufio\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n)\n\nvar stdinScanner = bufio.NewScanner(os.Stdin)\n\nfunc Confirmation(prompt string, def bool) bool {\n\tselection := \"y/N\"\n\tif def {\n\t\tselection = \"Y/n\"\n\t}\n\n\tfmt.Fprintf(os.Stderr, \"%s [%s]: \", prompt, selection)\n\tif !stdinScanner.Scan() {\n\t\tfmt.Fprintln(os.Stderr, stdinScanner.Err())\n\t\treturn false\n\t}\n\n\tswitch stdinScanner.Text() {\n\tcase \"Y\", \"y\":\n\t\treturn true\n\tcase \"N\", \"n\":\n\t\treturn false\n\tdefault:\n\t\treturn def\n\t}\n}\n\nfunc readPass(tty *os.File, output []byte) ([]byte, error) {\n\tcursor := output[0:1]\n\treaden := 0\n\tfor {\n\t\tn, err := tty.Read(cursor)\n\t\tif n != 1 {\n\t\t\treturn nil, errors.New(\"ReadPassword: invalid read size when not in canonical mode\")\n\t\t}\n\t\tif err != nil {\n\t\t\treturn nil, errors.New(\"ReadPassword: \" + err.Error())\n\t\t}\n\t\tif cursor[0] == '\\n' {\n\t\t\tbreak\n\t\t}\n\t\t// Esc or Ctrl+D or Ctrl+C.\n\t\tif cursor[0] == '\\x1b' || cursor[0] == '\\x04' || cursor[0] == '\\x03' {\n\t\t\treturn nil, errors.New(\"ReadPassword: prompt rejected\")\n\t\t}\n\t\tif cursor[0] == '\\x7F' /* DEL */ {\n\t\t\tif readen != 0 {\n\t\t\t\treaden--\n\t\t\t\tcursor = output[readen : readen+1]\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tif readen == cap(output) {\n\t\t\treturn nil, errors.New(\"ReadPassword: too long password\")\n\t\t}\n\n\t\treaden++\n\t\tcursor = output[readen : readen+1]\n\t}\n\n\treturn output[0:readen], nil\n}\n\nfunc ReadPassword(prompt string) (string, error) {\n\ttermios, err := TurnOnRawIO(os.Stdin)\n\thiddenPass := true\n\tif err != nil {\n\t\thiddenPass = false\n\t\tfmt.Fprintln(os.Stderr, \"Failed to disable terminal output:\", err)\n\t}\n\n\t// There is no meaningful way to handle error here.\n\t//nolint:errcheck\n\tdefer TcSetAttr(os.Stdin.Fd(), &termios)\n\n\tfmt.Fprintf(os.Stderr, \"%s: \", prompt)\n\n\tif hiddenPass {\n\t\tbuf := make([]byte, 512)\n\t\tbuf, err = readPass(os.Stdin, buf)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tfmt.Println()\n\n\t\treturn string(buf), nil\n\t}\n\tif !stdinScanner.Scan() {\n\t\treturn \"\", stdinScanner.Err()\n\t}\n\n\treturn stdinScanner.Text(), nil\n}\n"
  },
  {
    "path": "internal/cli/clitools/termios.go",
    "content": "//go:build linux\n// +build linux\n\n/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage clitools\n\n// Copied from github.com/foxcpp/ttyprompt\n// Commit 087a574, terminal/termios.go\n\nimport (\n\t\"errors\"\n\t\"os\"\n\t\"syscall\"\n\t\"unsafe\"\n)\n\ntype Termios struct {\n\tIflag  uint32\n\tOflag  uint32\n\tCflag  uint32\n\tLflag  uint32\n\tCc     [20]byte\n\tIspeed uint32\n\tOspeed uint32\n}\n\n/*\nTurnOnRawIO sets flags suitable for raw I/O (no echo, per-character input, etc)\nand returns original flags.\n*/\nfunc TurnOnRawIO(tty *os.File) (orig Termios, err error) {\n\ttermios, err := TcGetAttr(tty.Fd())\n\tif err != nil {\n\t\treturn Termios{}, errors.New(\"TurnOnRawIO: failed to get flags: \" + err.Error())\n\t}\n\ttermiosOrig := *termios\n\n\ttermios.Lflag &^= syscall.ECHO\n\ttermios.Lflag &^= syscall.ICANON\n\ttermios.Iflag &^= syscall.IXON\n\ttermios.Lflag &^= syscall.ISIG\n\ttermios.Iflag |= syscall.IUTF8\n\terr = TcSetAttr(tty.Fd(), termios)\n\tif err != nil {\n\t\treturn Termios{}, errors.New(\"TurnOnRawIO: flags to set flags: \" + err.Error())\n\t}\n\treturn termiosOrig, nil\n}\n\nfunc TcSetAttr(fd uintptr, termios *Termios) error {\n\t_, _, err := syscall.Syscall(syscall.SYS_IOCTL, fd, syscall.TCSETS, uintptr(unsafe.Pointer(termios)))\n\tif err != 0 {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc TcGetAttr(fd uintptr) (*Termios, error) {\n\ttermios := &Termios{}\n\t_, _, err := syscall.Syscall(syscall.SYS_IOCTL, fd, syscall.TCGETS, uintptr(unsafe.Pointer(termios)))\n\tif err != 0 {\n\t\treturn nil, err\n\t}\n\treturn termios, nil\n}\n"
  },
  {
    "path": "internal/cli/clitools/termios_stub.go",
    "content": "//go:build !linux\n// +build !linux\n\n/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage clitools\n\nimport (\n\t\"errors\"\n\t\"os\"\n)\n\ntype Termios struct {\n\tIflag  uint32\n\tOflag  uint32\n\tCflag  uint32\n\tLflag  uint32\n\tCc     [20]byte\n\tIspeed uint32\n\tOspeed uint32\n}\n\nfunc TurnOnRawIO(tty *os.File) (orig Termios, err error) {\n\treturn Termios{}, errors.New(\"not implemented\")\n}\n\nfunc TcSetAttr(fd uintptr, termios *Termios) error {\n\treturn errors.New(\"not implemented\")\n}\n\nfunc TcGetAttr(fd uintptr) (*Termios, error) {\n\treturn nil, errors.New(\"not implemented\")\n}\n"
  },
  {
    "path": "internal/cli/ctl/appendlimit.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage ctl\n\nimport (\n\t\"fmt\"\n\n\timapbackend \"github.com/emersion/go-imap/backend\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/urfave/cli/v2\"\n)\n\n// Copied from go-imap-backend-tests.\n\n// AppendLimitUser is extension for backend.User interface which allows to\n// set append limit value for testing and administration purposes.\ntype AppendLimitUser interface {\n\timapbackend.AppendLimitUser\n\n\t// SetMessageLimit sets new value for limit.\n\t// nil pointer means no limit.\n\tSetMessageLimit(val *uint32) error\n}\n\nfunc imapAcctAppendlimit(be module.Storage, ctx *cli.Context) error {\n\tusername := ctx.Args().First()\n\tif username == \"\" {\n\t\treturn cli.Exit(\"Error: USERNAME is required\", 2)\n\t}\n\n\tu, err := be.GetIMAPAcct(username)\n\tif err != nil {\n\t\treturn err\n\t}\n\tuserAL, ok := u.(AppendLimitUser)\n\tif !ok {\n\t\treturn cli.Exit(\"Error: module.Storage does not support per-user append limit\", 2)\n\t}\n\n\tif ctx.IsSet(\"value\") {\n\t\tval := ctx.Int(\"value\")\n\n\t\tvar err error\n\t\tif val == -1 {\n\t\t\terr = userAL.SetMessageLimit(nil)\n\t\t} else {\n\t\t\tval32 := uint32(val)\n\t\t\terr = userAL.SetMessageLimit(&val32)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t} else {\n\t\tlim := userAL.CreateMessageLimit()\n\t\tif lim == nil {\n\t\t\tfmt.Println(\"No limit\")\n\t\t} else {\n\t\t\tfmt.Println(*lim)\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/cli/ctl/hash.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage ctl\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/foxcpp/maddy/internal/auth/pass_table\"\n\tmaddycli \"github.com/foxcpp/maddy/internal/cli\"\n\tclitools2 \"github.com/foxcpp/maddy/internal/cli/clitools\"\n\t\"github.com/urfave/cli/v2\"\n\t\"golang.org/x/crypto/bcrypt\"\n)\n\nfunc init() {\n\tmaddycli.AddSubcommand(\n\t\t&cli.Command{\n\t\t\tName:   \"hash\",\n\t\t\tUsage:  \"Generate password hashes for use with pass_table\",\n\t\t\tAction: hashCommand,\n\t\t\tFlags: []cli.Flag{\n\t\t\t\t&cli.StringFlag{\n\t\t\t\t\tName:    \"password\",\n\t\t\t\t\tAliases: []string{\"p\"},\n\t\t\t\t\tUsage:   \"Use `PASSWORD instead of reading password from stdin\\n\\t\\tWARNING: Provided only for debugging convenience. Don't leave your passwords in shell history!\",\n\t\t\t\t},\n\t\t\t\t&cli.StringFlag{\n\t\t\t\t\tName:  \"hash\",\n\t\t\t\t\tUsage: \"Use specified hash algorithm\",\n\t\t\t\t\tValue: \"bcrypt\",\n\t\t\t\t},\n\t\t\t\t&cli.IntFlag{\n\t\t\t\t\tName:  \"bcrypt-cost\",\n\t\t\t\t\tUsage: \"Specify bcrypt cost value\",\n\t\t\t\t\tValue: bcrypt.DefaultCost,\n\t\t\t\t},\n\t\t\t\t&cli.IntFlag{\n\t\t\t\t\tName:  \"argon2-time\",\n\t\t\t\t\tUsage: \"Time factor for Argon2id\",\n\t\t\t\t\tValue: 3,\n\t\t\t\t},\n\t\t\t\t&cli.IntFlag{\n\t\t\t\t\tName:  \"argon2-memory\",\n\t\t\t\t\tUsage: \"Memory in KiB to use for Argon2id\",\n\t\t\t\t\tValue: 1024,\n\t\t\t\t},\n\t\t\t\t&cli.IntFlag{\n\t\t\t\t\tName:  \"argon2-threads\",\n\t\t\t\t\tUsage: \"Threads to use for Argon2id\",\n\t\t\t\t\tValue: 1,\n\t\t\t\t},\n\t\t\t},\n\t\t})\n}\n\nfunc hashCommand(ctx *cli.Context) error {\n\thashFunc := ctx.String(\"hash\")\n\tif hashFunc == \"\" {\n\t\thashFunc = pass_table.DefaultHash\n\t}\n\n\thashCompute := pass_table.HashCompute[hashFunc]\n\tif hashCompute == nil {\n\t\tfuncs := make([]string, 0, len(pass_table.HashCompute))\n\t\tfor k := range pass_table.HashCompute {\n\t\t\tfuncs = append(funcs, k)\n\t\t}\n\n\t\treturn cli.Exit(fmt.Sprintf(\"Error: Unknown hash function, available: %s\", strings.Join(funcs, \", \")), 2)\n\t}\n\n\topts := pass_table.HashOpts{\n\t\tBcryptCost:    bcrypt.DefaultCost,\n\t\tArgon2Memory:  1024,\n\t\tArgon2Time:    2,\n\t\tArgon2Threads: 1,\n\t}\n\tif ctx.IsSet(\"bcrypt-cost\") {\n\t\tif ctx.Int(\"bcrypt-cost\") > bcrypt.MaxCost {\n\t\t\treturn cli.Exit(\"Error: too big bcrypt cost\", 2)\n\t\t}\n\t\tif ctx.Int(\"bcrypt-cost\") < bcrypt.MinCost {\n\t\t\treturn cli.Exit(\"Error: too small bcrypt cost\", 2)\n\t\t}\n\t\topts.BcryptCost = ctx.Int(\"bcrypt-cost\")\n\t}\n\tif ctx.IsSet(\"argon2-memory\") {\n\t\topts.Argon2Memory = uint32(ctx.Int(\"argon2-memory\"))\n\t}\n\tif ctx.IsSet(\"argon2-time\") {\n\t\topts.Argon2Time = uint32(ctx.Int(\"argon2-time\"))\n\t}\n\tif ctx.IsSet(\"argon2-threads\") {\n\t\topts.Argon2Threads = uint8(ctx.Int(\"argon2-threads\"))\n\t}\n\n\tvar pass string\n\tif ctx.IsSet(\"password\") {\n\t\tpass = ctx.String(\"password\")\n\t} else {\n\t\tvar err error\n\t\tpass, err = clitools2.ReadPassword(\"Password\")\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif pass == \"\" {\n\t\tfmt.Fprintln(os.Stderr, \"WARNING: This is the hash of an empty string\")\n\t}\n\tif strings.TrimSpace(pass) != pass {\n\t\tfmt.Fprintln(os.Stderr, \"WARNING: There is leading/trailing whitespace in the string\")\n\t}\n\n\thash, err := hashCompute(opts, pass)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfmt.Println(hashFunc + \":\" + hash)\n\treturn nil\n}\n"
  },
  {
    "path": "internal/cli/ctl/imap.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage ctl\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/emersion/go-imap\"\n\timapsql \"github.com/foxcpp/go-imap-sql\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\tmaddycli \"github.com/foxcpp/maddy/internal/cli\"\n\tclitools2 \"github.com/foxcpp/maddy/internal/cli/clitools\"\n\t\"github.com/urfave/cli/v2\"\n)\n\nfunc init() {\n\tmaddycli.AddSubcommand(\n\t\t&cli.Command{\n\t\t\tName:  \"imap-mboxes\",\n\t\t\tUsage: \"IMAP mailboxes (folders) management\",\n\t\t\tSubcommands: []*cli.Command{\n\t\t\t\t{\n\t\t\t\t\tName:      \"list\",\n\t\t\t\t\tUsage:     \"Show mailboxes of user\",\n\t\t\t\t\tArgsUsage: \"USERNAME\",\n\t\t\t\t\tFlags: []cli.Flag{\n\t\t\t\t\t\t&cli.StringFlag{\n\t\t\t\t\t\t\tName:    \"cfg-block\",\n\t\t\t\t\t\t\tUsage:   \"Module configuration block to use\",\n\t\t\t\t\t\t\tEnvVars: []string{\"MADDY_CFGBLOCK\"},\n\t\t\t\t\t\t\tValue:   \"local_mailboxes\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\t\t\tName:    \"subscribed\",\n\t\t\t\t\t\t\tAliases: []string{\"s\"},\n\t\t\t\t\t\t\tUsage:   \"List only subscribed mailboxes\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tAction: func(ctx *cli.Context) error {\n\t\t\t\t\t\tbe, err := openStorage(ctx)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\t\t\t\t\t\tdefer closeIfNeeded(be)\n\t\t\t\t\t\treturn mboxesList(be, ctx)\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName:      \"create\",\n\t\t\t\t\tUsage:     \"Create mailbox\",\n\t\t\t\t\tArgsUsage: \"USERNAME NAME\",\n\t\t\t\t\tFlags: []cli.Flag{\n\t\t\t\t\t\t&cli.StringFlag{\n\t\t\t\t\t\t\tName:    \"cfg-block\",\n\t\t\t\t\t\t\tUsage:   \"Module configuration block to use\",\n\t\t\t\t\t\t\tEnvVars: []string{\"MADDY_CFGBLOCK\"},\n\t\t\t\t\t\t\tValue:   \"local_mailboxes\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t&cli.StringFlag{\n\t\t\t\t\t\t\tName:  \"special\",\n\t\t\t\t\t\t\tUsage: \"Set SPECIAL-USE attribute on mailbox; valid values: archive, drafts, junk, sent, trash\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tAction: func(ctx *cli.Context) error {\n\t\t\t\t\t\tbe, err := openStorage(ctx)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\t\t\t\t\t\tdefer closeIfNeeded(be)\n\t\t\t\t\t\treturn mboxesCreate(be, ctx)\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName:        \"remove\",\n\t\t\t\t\tUsage:       \"Remove mailbox\",\n\t\t\t\t\tDescription: \"WARNING: All contents of mailbox will be irrecoverably lost.\",\n\t\t\t\t\tArgsUsage:   \"USERNAME MAILBOX\",\n\t\t\t\t\tFlags: []cli.Flag{\n\t\t\t\t\t\t&cli.StringFlag{\n\t\t\t\t\t\t\tName:    \"cfg-block\",\n\t\t\t\t\t\t\tUsage:   \"Module configuration block to use\",\n\t\t\t\t\t\t\tEnvVars: []string{\"MADDY_CFGBLOCK\"},\n\t\t\t\t\t\t\tValue:   \"local_mailboxes\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\t\t\tName:    \"yes\",\n\t\t\t\t\t\t\tAliases: []string{\"y\"},\n\t\t\t\t\t\t\tUsage:   \"Don't ask for confirmation\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tAction: func(ctx *cli.Context) error {\n\t\t\t\t\t\tbe, err := openStorage(ctx)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\t\t\t\t\t\tdefer closeIfNeeded(be)\n\t\t\t\t\t\treturn mboxesRemove(be, ctx)\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName:        \"rename\",\n\t\t\t\t\tUsage:       \"Rename mailbox\",\n\t\t\t\t\tDescription: \"Rename may cause unexpected failures on client-side so be careful.\",\n\t\t\t\t\tArgsUsage:   \"USERNAME OLDNAME NEWNAME\",\n\t\t\t\t\tFlags: []cli.Flag{\n\t\t\t\t\t\t&cli.StringFlag{\n\t\t\t\t\t\t\tName:    \"cfg-block\",\n\t\t\t\t\t\t\tUsage:   \"Module configuration block to use\",\n\t\t\t\t\t\t\tEnvVars: []string{\"MADDY_CFGBLOCK\"},\n\t\t\t\t\t\t\tValue:   \"local_mailboxes\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tAction: func(ctx *cli.Context) error {\n\t\t\t\t\t\tbe, err := openStorage(ctx)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\t\t\t\t\t\tdefer closeIfNeeded(be)\n\t\t\t\t\t\treturn mboxesRename(be, ctx)\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\tmaddycli.AddSubcommand(&cli.Command{\n\t\tName:  \"imap-msgs\",\n\t\tUsage: \"IMAP messages management\",\n\t\tSubcommands: []*cli.Command{\n\t\t\t{\n\t\t\t\tName:        \"add\",\n\t\t\t\tUsage:       \"Add message to mailbox\",\n\t\t\t\tArgsUsage:   \"USERNAME MAILBOX\",\n\t\t\t\tDescription: \"Reads message body (with headers) from stdin. Prints UID of created message on success.\",\n\t\t\t\tFlags: []cli.Flag{\n\t\t\t\t\t&cli.StringFlag{\n\t\t\t\t\t\tName:    \"cfg-block\",\n\t\t\t\t\t\tUsage:   \"Module configuration block to use\",\n\t\t\t\t\t\tEnvVars: []string{\"MADDY_CFGBLOCK\"},\n\t\t\t\t\t\tValue:   \"local_mailboxes\",\n\t\t\t\t\t},\n\t\t\t\t\t&cli.StringSliceFlag{\n\t\t\t\t\t\tName:    \"flag\",\n\t\t\t\t\t\tAliases: []string{\"f\"},\n\t\t\t\t\t\tUsage:   \"Add flag to message. Can be specified multiple times\",\n\t\t\t\t\t},\n\t\t\t\t\t&cli.TimestampFlag{\n\t\t\t\t\t\tLayout:  time.RFC3339,\n\t\t\t\t\t\tName:    \"date\",\n\t\t\t\t\t\tAliases: []string{\"d\"},\n\t\t\t\t\t\tUsage:   \"Set internal date value to specified one in ISO 8601 format (2006-01-02T15:04:05Z07:00)\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tAction: func(ctx *cli.Context) error {\n\t\t\t\t\tbe, err := openStorage(ctx)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t\tdefer closeIfNeeded(be)\n\t\t\t\t\treturn msgsAdd(be, ctx)\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tName:        \"add-flags\",\n\t\t\t\tUsage:       \"Add flags to messages\",\n\t\t\t\tArgsUsage:   \"USERNAME MAILBOX SEQ FLAGS...\",\n\t\t\t\tDescription: \"Add flags to all messages matched by SEQ.\",\n\t\t\t\tFlags: []cli.Flag{\n\t\t\t\t\t&cli.StringFlag{\n\t\t\t\t\t\tName:    \"cfg-block\",\n\t\t\t\t\t\tUsage:   \"Module configuration block to use\",\n\t\t\t\t\t\tEnvVars: []string{\"MADDY_CFGBLOCK\"},\n\t\t\t\t\t\tValue:   \"local_mailboxes\",\n\t\t\t\t\t},\n\t\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\t\tName:    \"uid\",\n\t\t\t\t\t\tAliases: []string{\"u\"},\n\t\t\t\t\t\tUsage:   \"Use UIDs for SEQSET instead of sequence numbers\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tAction: func(ctx *cli.Context) error {\n\t\t\t\t\tbe, err := openStorage(ctx)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t\tdefer closeIfNeeded(be)\n\t\t\t\t\treturn msgsFlags(be, ctx)\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tName:        \"rem-flags\",\n\t\t\t\tUsage:       \"Remove flags from messages\",\n\t\t\t\tArgsUsage:   \"USERNAME MAILBOX SEQ FLAGS...\",\n\t\t\t\tDescription: \"Remove flags from all messages matched by SEQ.\",\n\t\t\t\tFlags: []cli.Flag{\n\t\t\t\t\t&cli.StringFlag{\n\t\t\t\t\t\tName:    \"cfg-block\",\n\t\t\t\t\t\tUsage:   \"Module configuration block to use\",\n\t\t\t\t\t\tEnvVars: []string{\"MADDY_CFGBLOCK\"},\n\t\t\t\t\t\tValue:   \"local_mailboxes\",\n\t\t\t\t\t},\n\t\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\t\tName:    \"uid\",\n\t\t\t\t\t\tAliases: []string{\"u\"},\n\t\t\t\t\t\tUsage:   \"Use UIDs for SEQSET instead of sequence numbers\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tAction: func(ctx *cli.Context) error {\n\t\t\t\t\tbe, err := openStorage(ctx)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t\tdefer closeIfNeeded(be)\n\t\t\t\t\treturn msgsFlags(be, ctx)\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tName:        \"set-flags\",\n\t\t\t\tUsage:       \"Set flags on messages\",\n\t\t\t\tArgsUsage:   \"USERNAME MAILBOX SEQ FLAGS...\",\n\t\t\t\tDescription: \"Set flags on all messages matched by SEQ.\",\n\t\t\t\tFlags: []cli.Flag{\n\t\t\t\t\t&cli.StringFlag{\n\t\t\t\t\t\tName:    \"cfg-block\",\n\t\t\t\t\t\tUsage:   \"Module configuration block to use\",\n\t\t\t\t\t\tEnvVars: []string{\"MADDY_CFGBLOCK\"},\n\t\t\t\t\t\tValue:   \"local_mailboxes\",\n\t\t\t\t\t},\n\t\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\t\tName:    \"uid\",\n\t\t\t\t\t\tAliases: []string{\"u\"},\n\t\t\t\t\t\tUsage:   \"Use UIDs for SEQSET instead of sequence numbers\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tAction: func(ctx *cli.Context) error {\n\t\t\t\t\tbe, err := openStorage(ctx)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t\tdefer closeIfNeeded(be)\n\t\t\t\t\treturn msgsFlags(be, ctx)\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tName:      \"remove\",\n\t\t\t\tUsage:     \"Remove messages from mailbox\",\n\t\t\t\tArgsUsage: \"USERNAME MAILBOX SEQSET\",\n\t\t\t\tFlags: []cli.Flag{\n\t\t\t\t\t&cli.StringFlag{\n\t\t\t\t\t\tName:    \"cfg-block\",\n\t\t\t\t\t\tUsage:   \"Module configuration block to use\",\n\t\t\t\t\t\tEnvVars: []string{\"MADDY_CFGBLOCK\"},\n\t\t\t\t\t\tValue:   \"local_mailboxes\",\n\t\t\t\t\t},\n\t\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\t\tName:    \"uid,u\",\n\t\t\t\t\t\tAliases: []string{\"u\"},\n\t\t\t\t\t\tUsage:   \"Use UIDs for SEQSET instead of sequence numbers\",\n\t\t\t\t\t},\n\t\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\t\tName:    \"yes\",\n\t\t\t\t\t\tAliases: []string{\"y\"},\n\t\t\t\t\t\tUsage:   \"Don't ask for confirmation\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tAction: func(ctx *cli.Context) error {\n\t\t\t\t\tbe, err := openStorage(ctx)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t\tdefer closeIfNeeded(be)\n\t\t\t\t\treturn msgsRemove(be, ctx)\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tName:        \"copy\",\n\t\t\t\tUsage:       \"Copy messages between mailboxes\",\n\t\t\t\tDescription: \"Note: You can't copy between mailboxes of different users. APPENDLIMIT of target mailbox is not enforced.\",\n\t\t\t\tArgsUsage:   \"USERNAME SRCMAILBOX SEQSET TGTMAILBOX\",\n\t\t\t\tFlags: []cli.Flag{\n\t\t\t\t\t&cli.StringFlag{\n\t\t\t\t\t\tName:    \"cfg-block\",\n\t\t\t\t\t\tUsage:   \"Module configuration block to use\",\n\t\t\t\t\t\tEnvVars: []string{\"MADDY_CFGBLOCK\"},\n\t\t\t\t\t\tValue:   \"local_mailboxes\",\n\t\t\t\t\t},\n\t\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\t\tName:    \"uid\",\n\t\t\t\t\t\tAliases: []string{\"u\"},\n\t\t\t\t\t\tUsage:   \"Use UIDs for SEQSET instead of sequence numbers\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tAction: func(ctx *cli.Context) error {\n\t\t\t\t\tbe, err := openStorage(ctx)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t\tdefer closeIfNeeded(be)\n\t\t\t\t\treturn msgsCopy(be, ctx)\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tName:        \"move\",\n\t\t\t\tUsage:       \"Move messages between mailboxes\",\n\t\t\t\tDescription: \"Note: You can't move between mailboxes of different users. APPENDLIMIT of target mailbox is not enforced.\",\n\t\t\t\tArgsUsage:   \"USERNAME SRCMAILBOX SEQSET TGTMAILBOX\",\n\t\t\t\tFlags: []cli.Flag{\n\t\t\t\t\t&cli.StringFlag{\n\t\t\t\t\t\tName:    \"cfg-block\",\n\t\t\t\t\t\tUsage:   \"Module configuration block to use\",\n\t\t\t\t\t\tEnvVars: []string{\"MADDY_CFGBLOCK\"},\n\t\t\t\t\t\tValue:   \"local_mailboxes\",\n\t\t\t\t\t},\n\t\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\t\tName:    \"uid\",\n\t\t\t\t\t\tAliases: []string{\"u\"},\n\t\t\t\t\t\tUsage:   \"Use UIDs for SEQSET instead of sequence numbers\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tAction: func(ctx *cli.Context) error {\n\t\t\t\t\tbe, err := openStorage(ctx)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t\tdefer closeIfNeeded(be)\n\t\t\t\t\treturn msgsMove(be, ctx)\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tName:        \"list\",\n\t\t\t\tUsage:       \"List messages in mailbox\",\n\t\t\t\tDescription: \"If SEQSET is specified - only show messages that match it.\",\n\t\t\t\tArgsUsage:   \"USERNAME MAILBOX [SEQSET]\",\n\t\t\t\tFlags: []cli.Flag{\n\t\t\t\t\t&cli.StringFlag{\n\t\t\t\t\t\tName:    \"cfg-block\",\n\t\t\t\t\t\tUsage:   \"Module configuration block to use\",\n\t\t\t\t\t\tEnvVars: []string{\"MADDY_CFGBLOCK\"},\n\t\t\t\t\t\tValue:   \"local_mailboxes\",\n\t\t\t\t\t},\n\t\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\t\tName:    \"uid\",\n\t\t\t\t\t\tAliases: []string{\"u\"},\n\t\t\t\t\t\tUsage:   \"Use UIDs for SEQSET instead of sequence numbers\",\n\t\t\t\t\t},\n\t\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\t\tName:    \"full,f\",\n\t\t\t\t\t\tAliases: []string{\"f\"},\n\t\t\t\t\t\tUsage:   \"Show entire envelope and all server meta-data\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tAction: func(ctx *cli.Context) error {\n\t\t\t\t\tbe, err := openStorage(ctx)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t\tdefer closeIfNeeded(be)\n\t\t\t\t\treturn msgsList(be, ctx)\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tName:        \"dump\",\n\t\t\t\tUsage:       \"Dump message body\",\n\t\t\t\tDescription: \"If passed SEQ matches multiple messages - they will be joined.\",\n\t\t\t\tArgsUsage:   \"USERNAME MAILBOX SEQ\",\n\t\t\t\tFlags: []cli.Flag{\n\t\t\t\t\t&cli.StringFlag{\n\t\t\t\t\t\tName:    \"cfg-block\",\n\t\t\t\t\t\tUsage:   \"Module configuration block to use\",\n\t\t\t\t\t\tEnvVars: []string{\"MADDY_CFGBLOCK\"},\n\t\t\t\t\t\tValue:   \"local_mailboxes\",\n\t\t\t\t\t},\n\t\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\t\tName:    \"uid\",\n\t\t\t\t\t\tAliases: []string{\"u\"},\n\t\t\t\t\t\tUsage:   \"Use UIDs for SEQ instead of sequence numbers\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tAction: func(ctx *cli.Context) error {\n\t\t\t\t\tbe, err := openStorage(ctx)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t\tdefer closeIfNeeded(be)\n\t\t\t\t\treturn msgsDump(be, ctx)\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t})\n}\n\nfunc FormatAddress(addr *imap.Address) string {\n\treturn fmt.Sprintf(\"%s <%s@%s>\", addr.PersonalName, addr.MailboxName, addr.HostName)\n}\n\nfunc FormatAddressList(addrs []*imap.Address) string {\n\tres := make([]string, 0, len(addrs))\n\tfor _, addr := range addrs {\n\t\tres = append(res, FormatAddress(addr))\n\t}\n\treturn strings.Join(res, \", \")\n}\n\nfunc mboxesList(be module.Storage, ctx *cli.Context) error {\n\tusername := ctx.Args().First()\n\tif username == \"\" {\n\t\treturn cli.Exit(\"Error: USERNAME is required\", 2)\n\t}\n\n\tu, err := be.GetIMAPAcct(username)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tmboxes, err := u.ListMailboxes(ctx.Bool(\"subscribed,s\"))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif len(mboxes) == 0 && !ctx.Bool(\"quiet\") {\n\t\tfmt.Fprintln(os.Stderr, \"No mailboxes.\")\n\t}\n\n\tfor _, info := range mboxes {\n\t\tif len(info.Attributes) != 0 {\n\t\t\tfmt.Print(info.Name, \"\\t\", info.Attributes, \"\\n\")\n\t\t} else {\n\t\t\tfmt.Println(info.Name)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc mboxesCreate(be module.Storage, ctx *cli.Context) error {\n\tusername := ctx.Args().First()\n\tif username == \"\" {\n\t\treturn cli.Exit(\"Error: USERNAME is required\", 2)\n\t}\n\tname := ctx.Args().Get(1)\n\tif name == \"\" {\n\t\treturn cli.Exit(\"Error: NAME is required\", 2)\n\t}\n\n\tu, err := be.GetIMAPAcct(username)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif ctx.IsSet(\"special\") {\n\t\tattr := \"\\\\\" + strings.Title(ctx.String(\"special\")) //nolint:staticcheck\n\t\t// (nolint) strings.Title is perfectly fine there since special mailbox tags will never use Unicode.\n\n\t\tsuu, ok := u.(SpecialUseUser)\n\t\tif !ok {\n\t\t\treturn cli.Exit(\"Error: storage backend does not support SPECIAL-USE IMAP extension\", 2)\n\t\t}\n\n\t\treturn suu.CreateMailboxSpecial(name, attr)\n\t}\n\n\treturn u.CreateMailbox(name)\n}\n\nfunc mboxesRemove(be module.Storage, ctx *cli.Context) error {\n\tusername := ctx.Args().First()\n\tif username == \"\" {\n\t\treturn cli.Exit(\"Error: USERNAME is required\", 2)\n\t}\n\tname := ctx.Args().Get(1)\n\tif name == \"\" {\n\t\treturn cli.Exit(\"Error: NAME is required\", 2)\n\t}\n\n\tu, err := be.GetIMAPAcct(username)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !ctx.Bool(\"yes\") {\n\t\tstatus, err := u.Status(name, []imap.StatusItem{imap.StatusMessages})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif status.Messages != 0 {\n\t\t\tfmt.Fprintf(os.Stderr, \"Mailbox %s contains %d messages.\\n\", name, status.Messages)\n\t\t}\n\n\t\tif !clitools2.Confirmation(\"Are you sure you want to delete that mailbox?\", false) {\n\t\t\treturn errors.New(\"Cancelled\")\n\t\t}\n\t}\n\n\treturn u.DeleteMailbox(name)\n}\n\nfunc mboxesRename(be module.Storage, ctx *cli.Context) error {\n\tusername := ctx.Args().First()\n\tif username == \"\" {\n\t\treturn cli.Exit(\"Error: USERNAME is required\", 2)\n\t}\n\toldName := ctx.Args().Get(1)\n\tif oldName == \"\" {\n\t\treturn cli.Exit(\"Error: OLDNAME is required\", 2)\n\t}\n\tnewName := ctx.Args().Get(2)\n\tif newName == \"\" {\n\t\treturn cli.Exit(\"Error: NEWNAME is required\", 2)\n\t}\n\n\tu, err := be.GetIMAPAcct(username)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn u.RenameMailbox(oldName, newName)\n}\n\nfunc msgsAdd(be module.Storage, ctx *cli.Context) error {\n\tusername := ctx.Args().First()\n\tif username == \"\" {\n\t\treturn cli.Exit(\"Error: USERNAME is required\", 2)\n\t}\n\tname := ctx.Args().Get(1)\n\tif name == \"\" {\n\t\treturn cli.Exit(\"Error: MAILBOX is required\", 2)\n\t}\n\n\tu, err := be.GetIMAPAcct(username)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tflags := ctx.StringSlice(\"flag\")\n\tif flags == nil {\n\t\tflags = []string{}\n\t}\n\n\tdate := time.Now()\n\tif ctx.IsSet(\"date\") {\n\t\tdate = *ctx.Timestamp(\"date\")\n\t}\n\n\tbuf := bytes.Buffer{}\n\tif _, err := io.Copy(&buf, os.Stdin); err != nil {\n\t\treturn err\n\t}\n\n\tif buf.Len() == 0 {\n\t\treturn cli.Exit(\"Error: Empty message, refusing to continue\", 2)\n\t}\n\n\tstatus, err := u.Status(name, []imap.StatusItem{imap.StatusUidNext})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif err := u.CreateMessage(name, flags, date, &buf, nil); err != nil {\n\t\treturn err\n\t}\n\n\t// TODO: Use APPENDUID\n\tfmt.Println(status.UidNext)\n\n\treturn nil\n}\n\nfunc msgsRemove(be module.Storage, ctx *cli.Context) error {\n\tusername := ctx.Args().First()\n\tif username == \"\" {\n\t\treturn cli.Exit(\"Error: USERNAME is required\", 2)\n\t}\n\tname := ctx.Args().Get(1)\n\tif name == \"\" {\n\t\treturn cli.Exit(\"Error: MAILBOX is required\", 2)\n\t}\n\tseqset := ctx.Args().Get(2)\n\tif seqset == \"\" {\n\t\treturn cli.Exit(\"Error: SEQSET is required\", 2)\n\t}\n\n\tif !ctx.Bool(\"uid\") {\n\t\tfmt.Fprintln(os.Stderr, \"WARNING: --uid=true will be the default in 0.7\")\n\t}\n\n\tseq, err := imap.ParseSeqSet(seqset)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tu, err := be.GetIMAPAcct(username)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, mbox, err := u.GetMailbox(name, true, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !ctx.Bool(\"yes\") {\n\t\tif !clitools2.Confirmation(\"Are you sure you want to delete these messages?\", false) {\n\t\t\treturn errors.New(\"Cancelled\")\n\t\t}\n\t}\n\n\tmboxB := mbox.(*imapsql.Mailbox)\n\treturn mboxB.DelMessages(ctx.Bool(\"uid\"), seq)\n}\n\nfunc msgsCopy(be module.Storage, ctx *cli.Context) error {\n\tusername := ctx.Args().First()\n\tif username == \"\" {\n\t\treturn cli.Exit(\"Error: USERNAME is required\", 2)\n\t}\n\tsrcName := ctx.Args().Get(1)\n\tif srcName == \"\" {\n\t\treturn cli.Exit(\"Error: SRCMAILBOX is required\", 2)\n\t}\n\tseqset := ctx.Args().Get(2)\n\tif seqset == \"\" {\n\t\treturn cli.Exit(\"Error: SEQSET is required\", 2)\n\t}\n\ttgtName := ctx.Args().Get(3)\n\tif tgtName == \"\" {\n\t\treturn cli.Exit(\"Error: TGTMAILBOX is required\", 2)\n\t}\n\n\tif !ctx.Bool(\"uid\") {\n\t\tfmt.Fprintln(os.Stderr, \"WARNING: --uid=true will be the default in 0.7\")\n\t}\n\n\tseq, err := imap.ParseSeqSet(seqset)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tu, err := be.GetIMAPAcct(username)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, srcMbox, err := u.GetMailbox(srcName, true, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn srcMbox.CopyMessages(ctx.Bool(\"uid\"), seq, tgtName)\n}\n\nfunc msgsMove(be module.Storage, ctx *cli.Context) error {\n\tusername := ctx.Args().First()\n\tif username == \"\" {\n\t\treturn cli.Exit(\"Error: USERNAME is required\", 2)\n\t}\n\tsrcName := ctx.Args().Get(1)\n\tif srcName == \"\" {\n\t\treturn cli.Exit(\"Error: SRCMAILBOX is required\", 2)\n\t}\n\tseqset := ctx.Args().Get(2)\n\tif seqset == \"\" {\n\t\treturn cli.Exit(\"Error: SEQSET is required\", 2)\n\t}\n\ttgtName := ctx.Args().Get(3)\n\tif tgtName == \"\" {\n\t\treturn cli.Exit(\"Error: TGTMAILBOX is required\", 2)\n\t}\n\n\tif !ctx.Bool(\"uid\") {\n\t\tfmt.Fprintln(os.Stderr, \"WARNING: --uid=true will be the default in 0.7\")\n\t}\n\n\tseq, err := imap.ParseSeqSet(seqset)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tu, err := be.GetIMAPAcct(username)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, srcMbox, err := u.GetMailbox(srcName, true, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tmoveMbox := srcMbox.(*imapsql.Mailbox)\n\n\treturn moveMbox.MoveMessages(ctx.Bool(\"uid\"), seq, tgtName)\n}\n\nfunc msgsList(be module.Storage, ctx *cli.Context) error {\n\tusername := ctx.Args().First()\n\tif username == \"\" {\n\t\treturn cli.Exit(\"Error: USERNAME is required\", 2)\n\t}\n\tmboxName := ctx.Args().Get(1)\n\tif mboxName == \"\" {\n\t\treturn cli.Exit(\"Error: MAILBOX is required\", 2)\n\t}\n\tseqset := ctx.Args().Get(2)\n\tuid := ctx.Bool(\"uid\")\n\tif seqset == \"\" {\n\t\tseqset = \"1:*\"\n\t\tuid = true\n\t} else if !uid {\n\t\tfmt.Fprintln(os.Stderr, \"WARNING: --uid=true will be the default in 0.7\")\n\t}\n\n\tseq, err := imap.ParseSeqSet(seqset)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tu, err := be.GetIMAPAcct(username)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, mbox, err := u.GetMailbox(mboxName, true, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tch := make(chan *imap.Message, 10)\n\tgo func() {\n\t\terr = mbox.ListMessages(uid, seq, []imap.FetchItem{imap.FetchEnvelope, imap.FetchInternalDate, imap.FetchRFC822Size, imap.FetchFlags, imap.FetchUid}, ch)\n\t}()\n\n\tfor msg := range ch {\n\t\tif !ctx.Bool(\"full\") {\n\t\t\tfmt.Printf(\"UID %d: %s - %s\\n  %v, %v\\n\\n\", msg.Uid, FormatAddressList(msg.Envelope.From), msg.Envelope.Subject, msg.Flags, msg.Envelope.Date)\n\t\t\tcontinue\n\t\t}\n\n\t\tfmt.Println(\"- Server meta-data:\")\n\t\tfmt.Println(\"UID:\", msg.Uid)\n\t\tfmt.Println(\"Sequence number:\", msg.SeqNum)\n\t\tfmt.Println(\"Flags:\", msg.Flags)\n\t\tfmt.Println(\"Body size:\", msg.Size)\n\t\tfmt.Println(\"Internal date:\", msg.InternalDate.Unix(), msg.InternalDate)\n\t\tfmt.Println(\"- Envelope:\")\n\t\tif len(msg.Envelope.From) != 0 {\n\t\t\tfmt.Println(\"From:\", FormatAddressList(msg.Envelope.From))\n\t\t}\n\t\tif len(msg.Envelope.To) != 0 {\n\t\t\tfmt.Println(\"To:\", FormatAddressList(msg.Envelope.To))\n\t\t}\n\t\tif len(msg.Envelope.Cc) != 0 {\n\t\t\tfmt.Println(\"CC:\", FormatAddressList(msg.Envelope.Cc))\n\t\t}\n\t\tif len(msg.Envelope.Bcc) != 0 {\n\t\t\tfmt.Println(\"BCC:\", FormatAddressList(msg.Envelope.Bcc))\n\t\t}\n\t\tif msg.Envelope.InReplyTo != \"\" {\n\t\t\tfmt.Println(\"In-Reply-To:\", msg.Envelope.InReplyTo)\n\t\t}\n\t\tif msg.Envelope.MessageId != \"\" {\n\t\t\tfmt.Println(\"Message-Id:\", msg.Envelope.MessageId)\n\t\t}\n\t\tif !msg.Envelope.Date.IsZero() {\n\t\t\tfmt.Println(\"Date:\", msg.Envelope.Date.Unix(), msg.Envelope.Date)\n\t\t}\n\t\tif msg.Envelope.Subject != \"\" {\n\t\t\tfmt.Println(\"Subject:\", msg.Envelope.Subject)\n\t\t}\n\t\tfmt.Println()\n\t}\n\treturn err\n}\n\nfunc msgsDump(be module.Storage, ctx *cli.Context) error {\n\tusername := ctx.Args().First()\n\tif username == \"\" {\n\t\treturn cli.Exit(\"Error: USERNAME is required\", 2)\n\t}\n\tmboxName := ctx.Args().Get(1)\n\tif mboxName == \"\" {\n\t\treturn cli.Exit(\"Error: MAILBOX is required\", 2)\n\t}\n\tseqset := ctx.Args().Get(2)\n\tuid := ctx.Bool(\"uid\")\n\tif seqset == \"\" {\n\t\tseqset = \"1:*\"\n\t\tuid = true\n\t} else if !uid {\n\t\tfmt.Fprintln(os.Stderr, \"WARNING: --uid=true will be the default in 0.7\")\n\t}\n\n\tseq, err := imap.ParseSeqSet(seqset)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tu, err := be.GetIMAPAcct(username)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, mbox, err := u.GetMailbox(mboxName, true, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tch := make(chan *imap.Message, 10)\n\tgo func() {\n\t\terr = mbox.ListMessages(uid, seq, []imap.FetchItem{imap.FetchRFC822}, ch)\n\t}()\n\n\tfor msg := range ch {\n\t\tfor _, v := range msg.Body {\n\t\t\tif _, err := io.Copy(os.Stdout, v); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\treturn err\n}\n\nfunc msgsFlags(be module.Storage, ctx *cli.Context) error {\n\tusername := ctx.Args().First()\n\tif username == \"\" {\n\t\treturn cli.Exit(\"Error: USERNAME is required\", 2)\n\t}\n\tname := ctx.Args().Get(1)\n\tif name == \"\" {\n\t\treturn cli.Exit(\"Error: MAILBOX is required\", 2)\n\t}\n\tseqStr := ctx.Args().Get(2)\n\tif seqStr == \"\" {\n\t\treturn cli.Exit(\"Error: SEQ is required\", 2)\n\t}\n\n\tif !ctx.Bool(\"uid\") {\n\t\tfmt.Fprintln(os.Stderr, \"WARNING: --uid=true will be the default in 0.7\")\n\t}\n\n\tseq, err := imap.ParseSeqSet(seqStr)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tu, err := be.GetIMAPAcct(username)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, mbox, err := u.GetMailbox(name, false, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tflags := ctx.Args().Slice()[3:]\n\tif len(flags) == 0 {\n\t\treturn cli.Exit(\"Error: at least once FLAG is required\", 2)\n\t}\n\n\tvar op imap.FlagsOp\n\tswitch ctx.Command.Name {\n\tcase \"add-flags\":\n\t\top = imap.AddFlags\n\tcase \"rem-flags\":\n\t\top = imap.RemoveFlags\n\tcase \"set-flags\":\n\t\top = imap.SetFlags\n\tdefault:\n\t\tpanic(\"unknown command: \" + ctx.Command.Name)\n\t}\n\n\treturn mbox.UpdateMessagesFlags(ctx.Bool(\"uid\"), seq, op, true, flags)\n}\n"
  },
  {
    "path": "internal/cli/ctl/imapacct.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage ctl\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/emersion/go-imap\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\tmaddycli \"github.com/foxcpp/maddy/internal/cli\"\n\tclitools2 \"github.com/foxcpp/maddy/internal/cli/clitools\"\n\t\"github.com/urfave/cli/v2\"\n)\n\nfunc init() {\n\tmaddycli.AddSubcommand(\n\t\t&cli.Command{\n\t\t\tName:  \"imap-acct\",\n\t\t\tUsage: \"IMAP storage accounts management\",\n\t\t\tDescription: `These subcommands can be used to list/create/delete IMAP storage\naccounts for any storage backend supported by maddy.\n\nThe corresponding storage backend should be configured in maddy.conf and be\ndefined in a top-level configuration block. By default, the name of that\nblock should be local_mailboxes but this can be changed using --cfg-block\nflag for subcommands.\n\nNote that in default configuration it is not enough to create an IMAP storage\naccount to grant server access. Additionally, user credentials should\nbe created using 'creds' subcommand.\n`,\n\t\t\tSubcommands: []*cli.Command{\n\t\t\t\t{\n\t\t\t\t\tName:  \"list\",\n\t\t\t\t\tUsage: \"List storage accounts\",\n\t\t\t\t\tFlags: []cli.Flag{\n\t\t\t\t\t\t&cli.StringFlag{\n\t\t\t\t\t\t\tName:    \"cfg-block\",\n\t\t\t\t\t\t\tUsage:   \"Module configuration block to use\",\n\t\t\t\t\t\t\tEnvVars: []string{\"MADDY_CFGBLOCK\"},\n\t\t\t\t\t\t\tValue:   \"local_mailboxes\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tAction: func(ctx *cli.Context) error {\n\t\t\t\t\t\tbe, err := openStorage(ctx)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\t\t\t\t\t\tdefer closeIfNeeded(be)\n\t\t\t\t\t\treturn imapAcctList(be, ctx)\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName:  \"create\",\n\t\t\t\t\tUsage: \"Create IMAP storage account\",\n\t\t\t\t\tDescription: `In addition to account creation, this command\ncreates a set of default folder (mailboxes) with special-use attribute set.`,\n\t\t\t\t\tArgsUsage: \"USERNAME\",\n\t\t\t\t\tFlags: []cli.Flag{\n\t\t\t\t\t\t&cli.StringFlag{\n\t\t\t\t\t\t\tName:    \"cfg-block\",\n\t\t\t\t\t\t\tUsage:   \"Module configuration block to use\",\n\t\t\t\t\t\t\tEnvVars: []string{\"MADDY_CFGBLOCK\"},\n\t\t\t\t\t\t\tValue:   \"local_mailboxes\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\t\t\tName:  \"no-specialuse\",\n\t\t\t\t\t\t\tUsage: \"Do not create special-use folders\",\n\t\t\t\t\t\t\tValue: false,\n\t\t\t\t\t\t},\n\t\t\t\t\t\t&cli.StringFlag{\n\t\t\t\t\t\t\tName:  \"sent-name\",\n\t\t\t\t\t\t\tUsage: \"Name of special mailbox for sent messages, use empty string to not create any\",\n\t\t\t\t\t\t\tValue: \"Sent\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t&cli.StringFlag{\n\t\t\t\t\t\t\tName:  \"trash-name\",\n\t\t\t\t\t\t\tUsage: \"Name of special mailbox for trash, use empty string to not create any\",\n\t\t\t\t\t\t\tValue: \"Trash\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t&cli.StringFlag{\n\t\t\t\t\t\t\tName:  \"junk-name\",\n\t\t\t\t\t\t\tUsage: \"Name of special mailbox for 'junk' (spam), use empty string to not create any\",\n\t\t\t\t\t\t\tValue: \"Junk\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t&cli.StringFlag{\n\t\t\t\t\t\t\tName:  \"drafts-name\",\n\t\t\t\t\t\t\tUsage: \"Name of special mailbox for drafts, use empty string to not create any\",\n\t\t\t\t\t\t\tValue: \"Drafts\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t&cli.StringFlag{\n\t\t\t\t\t\t\tName:  \"archive-name\",\n\t\t\t\t\t\t\tUsage: \"Name of special mailbox for archive, use empty string to not create any\",\n\t\t\t\t\t\t\tValue: \"Archive\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tAction: func(ctx *cli.Context) error {\n\t\t\t\t\t\tbe, err := openStorage(ctx)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\t\t\t\t\t\tdefer closeIfNeeded(be)\n\t\t\t\t\t\treturn imapAcctCreate(be, ctx)\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName:  \"remove\",\n\t\t\t\t\tUsage: \"Delete IMAP storage account\",\n\t\t\t\t\tDescription: `If IMAP connections are open and using the specified account,\nmessages access will be killed off immediately though connection will remain open. No cache\nor other buffering takes effect.`,\n\t\t\t\t\tArgsUsage: \"USERNAME\",\n\t\t\t\t\tFlags: []cli.Flag{\n\t\t\t\t\t\t&cli.StringFlag{\n\t\t\t\t\t\t\tName:    \"cfg-block\",\n\t\t\t\t\t\t\tUsage:   \"Module configuration block to use\",\n\t\t\t\t\t\t\tEnvVars: []string{\"MADDY_CFGBLOCK\"},\n\t\t\t\t\t\t\tValue:   \"local_mailboxes\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\t\t\tName:    \"yes\",\n\t\t\t\t\t\t\tAliases: []string{\"y\"},\n\t\t\t\t\t\t\tUsage:   \"Don't ask for confirmation\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tAction: func(ctx *cli.Context) error {\n\t\t\t\t\t\tbe, err := openStorage(ctx)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\t\t\t\t\t\tdefer closeIfNeeded(be)\n\t\t\t\t\t\treturn imapAcctRemove(be, ctx)\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName:  \"appendlimit\",\n\t\t\t\t\tUsage: \"Query or set accounts's APPENDLIMIT value\",\n\t\t\t\t\tDescription: `APPENDLIMIT value determines the size of a message that\ncan be saved into a mailbox using IMAP APPEND command. This does not affect the size\nof messages that can be delivered to the mailbox from non-IMAP sources (e.g. SMTP).\n\nGlobal APPENDLIMIT value set via server configuration takes precedence over\nper-account values configured using this command.\n\nAPPENDLIMIT value (either global or per-account) cannot be larger than\n4 GiB due to IMAP protocol limitations.\n`,\n\t\t\t\t\tArgsUsage: \"USERNAME\",\n\t\t\t\t\tFlags: []cli.Flag{\n\t\t\t\t\t\t&cli.StringFlag{\n\t\t\t\t\t\t\tName:    \"cfg-block\",\n\t\t\t\t\t\t\tUsage:   \"Module configuration block to use\",\n\t\t\t\t\t\t\tEnvVars: []string{\"MADDY_CFGBLOCK\"},\n\t\t\t\t\t\t\tValue:   \"local_mailboxes\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t&cli.IntFlag{\n\t\t\t\t\t\t\tName:    \"value\",\n\t\t\t\t\t\t\tAliases: []string{\"v\"},\n\t\t\t\t\t\t\tUsage:   \"Set APPENDLIMIT to specified value (in bytes)\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tAction: func(ctx *cli.Context) error {\n\t\t\t\t\t\tbe, err := openStorage(ctx)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\t\t\t\t\t\tdefer closeIfNeeded(be)\n\t\t\t\t\t\treturn imapAcctAppendlimit(be, ctx)\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t})\n}\n\ntype SpecialUseUser interface {\n\tCreateMailboxSpecial(name, specialUseAttr string) error\n}\n\nfunc imapAcctList(be module.Storage, ctx *cli.Context) error {\n\tmbe, ok := be.(module.ManageableStorage)\n\tif !ok {\n\t\treturn cli.Exit(\"Error: storage backend does not support accounts management using maddy command\", 2)\n\t}\n\n\tlist, err := mbe.ListIMAPAccts()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif len(list) == 0 && !ctx.Bool(\"quiet\") {\n\t\tfmt.Fprintln(os.Stderr, \"No users.\")\n\t}\n\n\tfor _, user := range list {\n\t\tfmt.Println(user)\n\t}\n\treturn nil\n}\n\nfunc imapAcctCreate(be module.Storage, ctx *cli.Context) error {\n\tmbe, ok := be.(module.ManageableStorage)\n\tif !ok {\n\t\treturn cli.Exit(\"Error: storage backend does not support accounts management using maddy command\", 2)\n\t}\n\n\tusername := ctx.Args().First()\n\tif username == \"\" {\n\t\treturn cli.Exit(\"Error: USERNAME is required\", 2)\n\t}\n\n\tif err := mbe.CreateIMAPAcct(username); err != nil {\n\t\treturn err\n\t}\n\n\tact, err := mbe.GetIMAPAcct(username)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get user: %w\", err)\n\t}\n\n\tsuu, ok := act.(SpecialUseUser)\n\tif !ok {\n\t\tfmt.Fprintf(os.Stderr, \"Note: Storage backend does not support SPECIAL-USE IMAP extension\")\n\t}\n\n\tif ctx.Bool(\"no-specialuse\") {\n\t\treturn nil\n\t}\n\n\tcreateMbox := func(name, specialUseAttr string) error {\n\t\tif suu == nil {\n\t\t\treturn act.CreateMailbox(name)\n\t\t}\n\t\treturn suu.CreateMailboxSpecial(name, specialUseAttr)\n\t}\n\n\tif name := ctx.String(\"sent-name\"); name != \"\" {\n\t\tif err := createMbox(name, imap.SentAttr); err != nil {\n\t\t\tfmt.Fprintf(os.Stderr, \"Failed to create sent folder: %v\", err)\n\t\t}\n\t}\n\tif name := ctx.String(\"trash-name\"); name != \"\" {\n\t\tif err := createMbox(name, imap.TrashAttr); err != nil {\n\t\t\tfmt.Fprintf(os.Stderr, \"Failed to create trash folder: %v\", err)\n\t\t}\n\t}\n\tif name := ctx.String(\"junk-name\"); name != \"\" {\n\t\tif err := createMbox(name, imap.JunkAttr); err != nil {\n\t\t\tfmt.Fprintf(os.Stderr, \"Failed to create junk folder: %v\", err)\n\t\t}\n\t}\n\tif name := ctx.String(\"drafts-name\"); name != \"\" {\n\t\tif err := createMbox(name, imap.DraftsAttr); err != nil {\n\t\t\tfmt.Fprintf(os.Stderr, \"Failed to create drafts folder: %v\", err)\n\t\t}\n\t}\n\tif name := ctx.String(\"archive-name\"); name != \"\" {\n\t\tif err := createMbox(name, imap.ArchiveAttr); err != nil {\n\t\t\tfmt.Fprintf(os.Stderr, \"Failed to create archive folder: %v\", err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc imapAcctRemove(be module.Storage, ctx *cli.Context) error {\n\tmbe, ok := be.(module.ManageableStorage)\n\tif !ok {\n\t\treturn cli.Exit(\"Error: storage backend does not support accounts management using maddy command\", 2)\n\t}\n\n\tusername := ctx.Args().First()\n\tif username == \"\" {\n\t\treturn cli.Exit(\"Error: USERNAME is required\", 2)\n\t}\n\n\tif !ctx.Bool(\"yes\") {\n\t\tif !clitools2.Confirmation(\"Are you sure you want to delete this user account?\", false) {\n\t\t\treturn errors.New(\"Cancelled\")\n\t\t}\n\t}\n\n\treturn mbe.DeleteIMAPAcct(username)\n}\n"
  },
  {
    "path": "internal/cli/ctl/moduleinit.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage ctl\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/foxcpp/maddy\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/log\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/internal/updatepipe\"\n\t\"github.com/urfave/cli/v2\"\n)\n\nfunc closeIfNeeded(i any) {\n\tif c, ok := i.(container.LifetimeModule); ok {\n\t\tif err := c.Stop(); err != nil {\n\t\t\tlog.DefaultLogger.Error(\"failed to stop module\", err)\n\t\t}\n\t}\n}\n\ntype managedStorage struct {\n\tmodule.ManageableStorage\n\tstarted bool\n}\n\nfunc (m *managedStorage) Close() error {\n\tif !m.started {\n\t\treturn nil\n\t}\n\tif lm, ok := m.ManageableStorage.(container.LifetimeModule); ok {\n\t\treturn lm.Stop()\n\t}\n\treturn nil\n}\n\ntype managedUserDB struct {\n\tmodule.PlainUserDB\n\tstarted bool\n}\n\nfunc (m *managedUserDB) Close() error {\n\tif !m.started {\n\t\treturn nil\n\t}\n\tif lm, ok := m.PlainUserDB.(container.LifetimeModule); ok {\n\t\treturn lm.Stop()\n\t}\n\treturn nil\n}\n\nfunc getCfgBlockModule(ctx *cli.Context) (*container.C, module.Module, error) {\n\tcfgPath := ctx.String(\"config\")\n\tif cfgPath == \"\" {\n\t\treturn nil, nil, cli.Exit(\"Error: config is required\", 2)\n\t}\n\n\tc := container.New()\n\tcontainer.Global = c\n\n\tcfg, err := maddy.ReadConfig(cfgPath)\n\tif err != nil {\n\t\treturn nil, nil, cli.Exit(fmt.Sprintf(\"Error: failed to open config: %v\", err), 2)\n\t}\n\n\tglobals, cfgNodes, err := maddy.ReadGlobals(c, cfg)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\t// For CLI management we force-rollback configured logger and consider only\n\t// --log so messages relevant to command execution will go where admin would\n\t// see them.\n\tc.DefaultLogger.Out = log.DefaultLogger.Out\n\n\tif err := maddy.InitDirs(c); err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\terr = maddy.RegisterModules(c, globals, cfgNodes)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\tcfgBlock := ctx.String(\"cfg-block\")\n\tif cfgBlock == \"\" {\n\t\treturn nil, nil, cli.Exit(\"Error: cfg-block is required\", 2)\n\t}\n\n\tmod, err := c.Modules.Get(cfgBlock)\n\tif err != nil {\n\t\tif errors.Is(err, container.ErrInstanceUnknown) {\n\t\t\treturn nil, nil, cli.Exit(fmt.Sprintf(\"Error: unknown configuration block: %s\", cfgBlock), 2)\n\t\t}\n\t\treturn nil, nil, err\n\t}\n\n\treturn c, mod, nil\n}\n\nfunc openStorage(ctx *cli.Context) (module.Storage, error) {\n\t_, mod, err := getCfgBlockModule(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tstorage, ok := mod.(module.Storage)\n\tif !ok {\n\t\treturn nil, cli.Exit(fmt.Sprintf(\"Error: configuration block %s is not an IMAP storage\", ctx.String(\"cfg-block\")), 2)\n\t}\n\n\tstarted := false\n\tif lt, ok := storage.(container.LifetimeModule); ok {\n\t\tif err := lt.Start(); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tstarted = true\n\t}\n\n\tif updStore, ok := mod.(updatepipe.Backend); ok {\n\t\tif err := updStore.EnableUpdatePipe(updatepipe.ModePush); err != nil && !errors.Is(err, os.ErrNotExist) {\n\t\t\tfmt.Fprintf(os.Stderr, \"Failed to initialize update pipe, do not remove messages from mailboxes open by clients: %v\\n\", err)\n\t\t}\n\t} else {\n\t\tfmt.Fprintf(os.Stderr, \"No update pipe support, do not remove messages from mailboxes open by clients\\n\")\n\t}\n\n\tif started {\n\t\tif ms, ok := storage.(module.ManageableStorage); ok {\n\t\t\treturn &managedStorage{ManageableStorage: ms, started: started}, nil\n\t\t}\n\t}\n\treturn storage, nil\n}\n\nfunc openUserDB(ctx *cli.Context) (module.PlainUserDB, error) {\n\t_, mod, err := getCfgBlockModule(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tuserDB, ok := mod.(module.PlainUserDB)\n\tif !ok {\n\t\treturn nil, cli.Exit(fmt.Sprintf(\"Error: configuration block %s is not a local credentials store\", ctx.String(\"cfg-block\")), 2)\n\t}\n\n\tstarted := false\n\tif lt, ok := userDB.(container.LifetimeModule); ok {\n\t\tif err := lt.Start(); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tstarted = true\n\t}\n\n\tif started {\n\t\treturn &managedUserDB{PlainUserDB: userDB, started: started}, nil\n\t}\n\treturn userDB, nil\n}\n"
  },
  {
    "path": "internal/cli/ctl/users.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage ctl\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/internal/auth/pass_table\"\n\tmaddycli \"github.com/foxcpp/maddy/internal/cli\"\n\tclitools2 \"github.com/foxcpp/maddy/internal/cli/clitools\"\n\t\"github.com/urfave/cli/v2\"\n\t\"golang.org/x/crypto/bcrypt\"\n)\n\nfunc init() {\n\tmaddycli.AddSubcommand(\n\t\t&cli.Command{\n\t\t\tName:  \"creds\",\n\t\t\tUsage: \"Local credentials management\",\n\t\t\tDescription: `These commands manipulate credential databases used by\nmaddy mail server.\n\nCorresponding credential database should be defined in maddy.conf as\na top-level config block. By default the block name should be local_authdb (\ncan be changed using --cfg-block argument for subcommands).\n\nNote that it is not enough to create user credentials in order to grant\nIMAP access - IMAP account should be also created using 'imap-acct create' subcommand.\n`,\n\t\t\tSubcommands: []*cli.Command{\n\t\t\t\t{\n\t\t\t\t\tName:  \"list\",\n\t\t\t\t\tUsage: \"List created credentials\",\n\t\t\t\t\tFlags: []cli.Flag{\n\t\t\t\t\t\t&cli.StringFlag{\n\t\t\t\t\t\t\tName:    \"cfg-block\",\n\t\t\t\t\t\t\tUsage:   \"Module configuration block to use\",\n\t\t\t\t\t\t\tEnvVars: []string{\"MADDY_CFGBLOCK\"},\n\t\t\t\t\t\t\tValue:   \"local_authdb\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tAction: func(ctx *cli.Context) error {\n\t\t\t\t\t\tbe, err := openUserDB(ctx)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\t\t\t\t\t\tdefer closeIfNeeded(be)\n\t\t\t\t\t\treturn usersList(be, ctx)\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName:  \"create\",\n\t\t\t\t\tUsage: \"Create user account\",\n\t\t\t\t\tDescription: `Reads password from stdin.\n\nIf configuration block uses auth.pass_table, then hash algorithm can be configured\nusing command flags. Otherwise, these options cannot be used.\n`,\n\t\t\t\t\tArgsUsage: \"USERNAME\",\n\t\t\t\t\tFlags: []cli.Flag{\n\t\t\t\t\t\t&cli.StringFlag{\n\t\t\t\t\t\t\tName:    \"cfg-block\",\n\t\t\t\t\t\t\tUsage:   \"Module configuration block to use\",\n\t\t\t\t\t\t\tEnvVars: []string{\"MADDY_CFGBLOCK\"},\n\t\t\t\t\t\t\tValue:   \"local_authdb\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t&cli.StringFlag{\n\t\t\t\t\t\t\tName:    \"password\",\n\t\t\t\t\t\t\tAliases: []string{\"p\"},\n\t\t\t\t\t\t\tUsage:   \"Use `PASSWORD instead of reading password from stdin.\\n\\t\\tWARNING: Provided only for debugging convenience. Don't leave your passwords in shell history!\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t&cli.StringFlag{\n\t\t\t\t\t\t\tName:  \"hash\",\n\t\t\t\t\t\t\tUsage: \"Use specified hash algorithm. Valid values: \" + strings.Join(pass_table.Hashes, \", \"),\n\t\t\t\t\t\t\tValue: \"bcrypt\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t&cli.IntFlag{\n\t\t\t\t\t\t\tName:  \"bcrypt-cost\",\n\t\t\t\t\t\t\tUsage: \"Specify bcrypt cost value\",\n\t\t\t\t\t\t\tValue: bcrypt.DefaultCost,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tAction: func(ctx *cli.Context) error {\n\t\t\t\t\t\tbe, err := openUserDB(ctx)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\t\t\t\t\t\tdefer closeIfNeeded(be)\n\t\t\t\t\t\treturn usersCreate(be, ctx)\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName:      \"remove\",\n\t\t\t\t\tUsage:     \"Delete user account\",\n\t\t\t\t\tArgsUsage: \"USERNAME\",\n\t\t\t\t\tFlags: []cli.Flag{\n\t\t\t\t\t\t&cli.StringFlag{\n\t\t\t\t\t\t\tName:    \"cfg-block\",\n\t\t\t\t\t\t\tUsage:   \"Module configuration block to use\",\n\t\t\t\t\t\t\tEnvVars: []string{\"MADDY_CFGBLOCK\"},\n\t\t\t\t\t\t\tValue:   \"local_authdb\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\t\t\tName:    \"yes\",\n\t\t\t\t\t\t\tAliases: []string{\"y\"},\n\t\t\t\t\t\t\tUsage:   \"Don't ask for confirmation\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tAction: func(ctx *cli.Context) error {\n\t\t\t\t\t\tbe, err := openUserDB(ctx)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\t\t\t\t\t\tdefer closeIfNeeded(be)\n\t\t\t\t\t\treturn usersRemove(be, ctx)\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName:        \"password\",\n\t\t\t\t\tUsage:       \"Change account password\",\n\t\t\t\t\tDescription: \"Reads password from stdin\",\n\t\t\t\t\tArgsUsage:   \"USERNAME\",\n\t\t\t\t\tFlags: []cli.Flag{\n\t\t\t\t\t\t&cli.StringFlag{\n\t\t\t\t\t\t\tName:    \"cfg-block\",\n\t\t\t\t\t\t\tUsage:   \"Module configuration block to use\",\n\t\t\t\t\t\t\tEnvVars: []string{\"MADDY_CFGBLOCK\"},\n\t\t\t\t\t\t\tValue:   \"local_authdb\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t&cli.StringFlag{\n\t\t\t\t\t\t\tName:    \"password\",\n\t\t\t\t\t\t\tAliases: []string{\"p\"},\n\t\t\t\t\t\t\tUsage:   \"Use `PASSWORD` instead of reading password from stdin.\\n\\t\\tWARNING: Provided only for debugging convenience. Don't leave your passwords in shell history!\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tAction: func(ctx *cli.Context) error {\n\t\t\t\t\t\tbe, err := openUserDB(ctx)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\t\t\t\t\t\tdefer closeIfNeeded(be)\n\t\t\t\t\t\treturn usersPassword(be, ctx)\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t})\n}\n\nfunc usersList(be module.PlainUserDB, ctx *cli.Context) error {\n\tlist, err := be.ListUsers()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif len(list) == 0 && !ctx.Bool(\"quiet\") {\n\t\tfmt.Fprintln(os.Stderr, \"No users.\")\n\t}\n\n\tfor _, user := range list {\n\t\tfmt.Println(user)\n\t}\n\treturn nil\n}\n\nfunc usersCreate(be module.PlainUserDB, ctx *cli.Context) error {\n\tusername := ctx.Args().First()\n\tif username == \"\" {\n\t\treturn cli.Exit(\"Error: USERNAME is required\", 2)\n\t}\n\n\tvar pass string\n\tif ctx.IsSet(\"password\") {\n\t\tpass = ctx.String(\"password\")\n\t} else {\n\t\tvar err error\n\t\tpass, err = clitools2.ReadPassword(\"Enter password for new user\")\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif beHash, ok := be.(*pass_table.Auth); ok {\n\t\treturn beHash.CreateUserHash(username, pass, ctx.String(\"hash\"), pass_table.HashOpts{\n\t\t\tBcryptCost: ctx.Int(\"bcrypt-cost\"),\n\t\t})\n\t} else if ctx.IsSet(\"hash\") || ctx.IsSet(\"bcrypt-cost\") {\n\t\treturn cli.Exit(\"Error: --hash cannot be used with non-pass_table credentials DB\", 2)\n\t} else {\n\t\treturn be.CreateUser(username, pass)\n\t}\n}\n\nfunc usersRemove(be module.PlainUserDB, ctx *cli.Context) error {\n\tusername := ctx.Args().First()\n\tif username == \"\" {\n\t\treturn errors.New(\"error: USERNAME is required\")\n\t}\n\n\tif !ctx.Bool(\"yes\") {\n\t\tif !clitools2.Confirmation(\"Are you sure you want to delete this user account?\", false) {\n\t\t\treturn errors.New(\"cancelled\")\n\t\t}\n\t}\n\n\treturn be.DeleteUser(username)\n}\n\nfunc usersPassword(be module.PlainUserDB, ctx *cli.Context) error {\n\tusername := ctx.Args().First()\n\tif username == \"\" {\n\t\treturn errors.New(\"error: USERNAME is required\")\n\t}\n\n\tvar pass string\n\tif ctx.IsSet(\"password\") {\n\t\tpass = ctx.String(\"password\")\n\t} else {\n\t\tvar err error\n\t\tpass, err = clitools2.ReadPassword(\"Enter new password\")\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn be.SetUserPassword(username, pass)\n}\n"
  },
  {
    "path": "internal/cli/extflag.go",
    "content": "package maddycli\n\nimport (\n\t\"flag\"\n\n\t\"github.com/urfave/cli/v2\"\n)\n\n// extFlag implements cli.Flag via standard flag.Flag.\ntype extFlag struct {\n\tf *flag.Flag\n}\n\nfunc (e *extFlag) Apply(fs *flag.FlagSet) error {\n\tfs.Var(e.f.Value, e.f.Name, e.f.Usage)\n\treturn nil\n}\n\nfunc (e *extFlag) Names() []string {\n\treturn []string{e.f.Name}\n}\n\nfunc (e *extFlag) IsSet() bool {\n\treturn false\n}\n\nfunc (e *extFlag) String() string {\n\treturn cli.FlagStringer(e)\n}\n\nfunc (e *extFlag) IsVisible() bool {\n\treturn true\n}\n\nfunc (e *extFlag) TakesValue() bool {\n\treturn false\n}\n\nfunc (e *extFlag) GetUsage() string {\n\treturn e.f.Usage\n}\n\nfunc (e *extFlag) GetValue() string {\n\treturn e.f.Value.String()\n}\n\nfunc (e *extFlag) GetDefaultText() string {\n\treturn e.f.DefValue\n}\n\nfunc (e *extFlag) GetEnvVars() []string {\n\treturn nil\n}\n\nfunc mapStdlibFlags(app *cli.App) {\n\t// Modified AllowExtFlags from cli lib with -test.* exception removed.\n\tflag.VisitAll(func(f *flag.Flag) {\n\t\tapp.Flags = append(app.Flags, &extFlag{f})\n\t})\n}\n"
  },
  {
    "path": "internal/dmarc/dmarc.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage dmarc\n\nimport (\n\t\"context\"\n\n\t\"github.com/emersion/go-msgauth/dmarc\"\n)\n\ntype (\n\tResolver interface {\n\t\tLookupTXT(context.Context, string) ([]string, error)\n\t}\n\n\tRecord         = dmarc.Record\n\tPolicy         = dmarc.Policy\n\tAlignmentMode  = dmarc.AlignmentMode\n\tFailureOptions = dmarc.FailureOptions\n)\n\nconst (\n\tPolicyNone       = dmarc.PolicyNone\n\tPolicyReject     = dmarc.PolicyReject\n\tPolicyQuarantine = dmarc.PolicyQuarantine\n)\n"
  },
  {
    "path": "internal/dmarc/evaluate.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage dmarc\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/mail\"\n\t\"strings\"\n\n\t\"github.com/emersion/go-message/textproto\"\n\t\"github.com/emersion/go-msgauth/authres\"\n\t\"github.com/emersion/go-msgauth/dmarc\"\n\t\"github.com/foxcpp/maddy/framework/address\"\n\t\"github.com/foxcpp/maddy/framework/dns\"\n\t\"golang.org/x/net/publicsuffix\"\n)\n\n// FetchRecord looks up the DMARC record relevant for the RFC5322.From domain.\n// It returns the record and the domain it was found with (may not be\n// equal to the RFC5322.From domain).\nfunc FetchRecord(ctx context.Context, r Resolver, fromDomain string) (policyDomain string, rec *Record, err error) {\n\tpolicyDomain = fromDomain\n\n\t// 1. Lookup using From Domain.\n\ttxts, err := r.LookupTXT(ctx, dns.FQDN(\"_dmarc.\"+fromDomain))\n\tif err != nil {\n\t\tdnsErr, ok := err.(*net.DNSError)\n\t\tif !ok || !dnsErr.IsNotFound {\n\t\t\treturn \"\", nil, err\n\t\t}\n\t}\n\tif len(txts) == 0 {\n\t\t// No records or 'no such host', try orgDomain.\n\t\torgDomain, err := publicsuffix.EffectiveTLDPlusOne(fromDomain)\n\t\tif err != nil {\n\t\t\treturn \"\", nil, err\n\t\t}\n\n\t\tpolicyDomain = orgDomain\n\n\t\ttxts, err = r.LookupTXT(ctx, dns.FQDN(\"_dmarc.\"+orgDomain))\n\t\tif err != nil {\n\t\t\tdnsErr, ok := err.(*net.DNSError)\n\t\t\tif !ok || !dnsErr.IsNotFound {\n\t\t\t\treturn \"\", nil, err\n\t\t\t}\n\t\t}\n\t\t// Still nothing? Bail out.\n\t\tif len(txts) == 0 {\n\t\t\treturn \"\", nil, nil\n\t\t}\n\t}\n\n\t// Exclude records that are not DMARC policies.\n\trecords := txts[:0]\n\tfor _, txt := range txts {\n\t\tif strings.HasPrefix(txt, \"v=DMARC1\") {\n\t\t\trecords = append(records, txt)\n\t\t}\n\t}\n\t// Multiple records => no record.\n\tif len(records) > 1 || len(records) == 0 {\n\t\treturn \"\", nil, nil\n\t}\n\n\trec, err = dmarc.Parse(records[0])\n\n\treturn policyDomain, rec, err\n}\n\ntype EvalResult struct {\n\t// The Authentication-Results field generated as a result of the DMARC\n\t// check.\n\tAuthres authres.DMARCResult\n\n\t// The Authentication-Results field for SPF that was considered during\n\t// alignment check. May be empty.\n\tSPFResult authres.SPFResult\n\n\t// Whether HELO or MAIL FROM match the RFC5322.From domain.\n\tSPFAligned bool\n\n\t// The Authentication-Results field for the DKIM signature that is aligned,\n\t// if no signatures are aligned - this field contains the result for the\n\t// first signature. May be empty.\n\tDKIMResult authres.DKIMResult\n\n\t// Whether there is a DKIM signature with the d= field matching the\n\t// RFC5322.From domain.\n\tDKIMAligned bool\n}\n\n// EvaluateAlignment checks whether identifiers authenticated by SPF and DKIM are in alignment\n// with the RFC5322.Domain.\n//\n// It returns EvalResult which contains the Authres field with the actual check result and\n// a bunch of other trace information that can be useful for troubleshooting\n// (and also report generation).\nfunc EvaluateAlignment(fromDomain string, record *Record, results []authres.Result) EvalResult {\n\tvar (\n\t\tspfAligned   = false\n\t\tspfResult    = authres.SPFResult{}\n\t\tdkimAligned  = false\n\t\tdkimResult   = authres.DKIMResult{}\n\t\tdkimPresent  = false\n\t\tdkimTempFail = false\n\t)\n\tfor _, res := range results {\n\t\tif dkimRes, ok := res.(*authres.DKIMResult); ok {\n\t\t\tdkimPresent = true\n\n\t\t\t// We want to return DKIM result for a signature provided by the orgDomain,\n\t\t\t// in case there is none - return any (possibly misaligned) for reference.\n\t\t\tif dkimResult.Value == \"\" {\n\t\t\t\tdkimResult = *dkimRes\n\t\t\t}\n\t\t\tif isAligned(fromDomain, dkimRes.Domain, record.DKIMAlignment) {\n\t\t\t\tdkimResult = *dkimRes\n\t\t\t\tswitch dkimRes.Value {\n\t\t\t\tcase authres.ResultPass:\n\t\t\t\t\tdkimAligned = true\n\t\t\t\tcase authres.ResultTempError:\n\t\t\t\t\tdkimTempFail = true\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif spfRes, ok := res.(*authres.SPFResult); ok {\n\t\t\tspfResult = *spfRes\n\t\t\tvar aligned bool\n\t\t\tif spfRes.From == \"\" {\n\t\t\t\taligned = isAligned(fromDomain, spfRes.Helo, record.SPFAlignment)\n\t\t\t} else {\n\t\t\t\taligned = isAligned(fromDomain, spfRes.From, record.SPFAlignment)\n\t\t\t}\n\t\t\tif aligned && spfRes.Value == authres.ResultPass {\n\t\t\t\tspfAligned = true\n\t\t\t}\n\t\t}\n\t}\n\n\tres := EvalResult{\n\t\tSPFResult:   spfResult,\n\t\tSPFAligned:  spfAligned,\n\t\tDKIMResult:  dkimResult,\n\t\tDKIMAligned: dkimAligned,\n\t}\n\n\tif !dkimPresent || spfResult.Value == \"\" {\n\t\tres.Authres = authres.DMARCResult{\n\t\t\tValue:  authres.ResultNone,\n\t\t\tReason: \"Not enough information (required checks are disabled)\",\n\t\t\tFrom:   fromDomain,\n\t\t}\n\t\treturn res\n\t}\n\n\tif dkimTempFail && !dkimAligned && !spfAligned {\n\t\t// We can't be sure whether it is aligned or not. Bail out.\n\t\tres.Authres = authres.DMARCResult{\n\t\t\tValue:  authres.ResultTempError,\n\t\t\tReason: \"DKIM authentication temp error\",\n\t\t\tFrom:   fromDomain,\n\t\t}\n\t\treturn res\n\t}\n\tif !dkimAligned && spfResult.Value == authres.ResultTempError {\n\t\t// We can't be sure whether it is aligned or not. Bail out.\n\t\tres.Authres = authres.DMARCResult{\n\t\t\tValue:  authres.ResultTempError,\n\t\t\tReason: \"SPF authentication temp error\",\n\t\t\tFrom:   fromDomain,\n\t\t}\n\t\treturn res\n\t}\n\n\tres.Authres.From = fromDomain\n\tif dkimAligned || spfAligned {\n\t\tres.Authres.Value = authres.ResultPass\n\t} else {\n\t\tres.Authres.Value = authres.ResultFail\n\t\tres.Authres.Reason = \"No aligned identifiers\"\n\t}\n\treturn res\n}\n\nfunc isAligned(fromDomain, authDomain string, mode AlignmentMode) bool {\n\tif mode == dmarc.AlignmentStrict {\n\t\treturn strings.EqualFold(fromDomain, authDomain)\n\t}\n\n\ttld, _ := publicsuffix.PublicSuffix(fromDomain)\n\tif strings.EqualFold(fromDomain, tld) {\n\t\treturn strings.EqualFold(fromDomain, authDomain)\n\t}\n\torgDomainFrom, err := publicsuffix.EffectiveTLDPlusOne(fromDomain)\n\tif err != nil {\n\t\treturn false\n\t}\n\tauthDomainFrom, err := publicsuffix.EffectiveTLDPlusOne(authDomain)\n\tif err != nil {\n\t\treturn false\n\t}\n\n\treturn strings.EqualFold(orgDomainFrom, authDomainFrom)\n}\n\nfunc ExtractFromDomain(hdr textproto.Header) (string, error) {\n\t// TODO(GH emersion/go-message#75): Add textproto.Header.Count method.\n\tvar firstFrom string\n\tfor fields := hdr.FieldsByKey(\"From\"); fields.Next(); {\n\t\tif firstFrom == \"\" {\n\t\t\tfirstFrom = fields.Value()\n\t\t} else {\n\t\t\treturn \"\", errors.New(\"dmarc: multiple From header fields are not allowed\")\n\t\t}\n\t}\n\tif firstFrom == \"\" {\n\t\treturn \"\", errors.New(\"dmarc: missing From header field\")\n\t}\n\n\thdrFromList, err := mail.ParseAddressList(firstFrom)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"dmarc: malformed From header field: %s\", strings.TrimPrefix(err.Error(), \"mail: \"))\n\t}\n\tif len(hdrFromList) > 1 {\n\t\treturn \"\", errors.New(\"dmarc: multiple addresses in From field are not allowed\")\n\t}\n\tif len(hdrFromList) == 0 {\n\t\treturn \"\", errors.New(\"dmarc: missing address in From field\")\n\t}\n\t_, domain, err := address.Split(hdrFromList[0].Address)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"dmarc: malformed From header field: %w\", err)\n\t}\n\n\treturn domain, nil\n}\n"
  },
  {
    "path": "internal/dmarc/evaluate_test.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage dmarc\n\nimport (\n\t\"bufio\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/emersion/go-message/textproto\"\n\t\"github.com/emersion/go-msgauth/authres\"\n\t\"github.com/emersion/go-msgauth/dmarc\"\n)\n\nfunc TestEvaluateAlignment(t *testing.T) {\n\ttype tCase struct {\n\t\tfromDomain string\n\t\trecord     *Record\n\t\tresults    []authres.Result\n\n\t\toutput authres.ResultValue\n\t}\n\ttest := func(i int, c tCase) {\n\t\tout := EvaluateAlignment(c.fromDomain, c.record, c.results)\n\t\tt.Logf(\"%d - %+v\", i, out)\n\t\tif out.Authres.Value != c.output {\n\t\t\tt.Errorf(\"%d: Wrong eval result, want '%s', got '%s' (%+v)\", i, c.output, out.Authres.Value, out)\n\t\t}\n\t}\n\n\tcases := []tCase{\n\t\t{ // 0\n\t\t\tfromDomain: \"example.org\",\n\t\t\trecord:     &Record{},\n\n\t\t\toutput: authres.ResultNone,\n\t\t},\n\t\t{ // 1\n\t\t\tfromDomain: \"example.org\",\n\t\t\trecord:     &Record{},\n\t\t\tresults: []authres.Result{\n\t\t\t\t&authres.SPFResult{\n\t\t\t\t\tValue: authres.ResultFail,\n\t\t\t\t\tFrom:  \"example.org\",\n\t\t\t\t\tHelo:  \"mx.example.org\",\n\t\t\t\t},\n\t\t\t\t&authres.DKIMResult{\n\t\t\t\t\tValue:  authres.ResultNone,\n\t\t\t\t\tDomain: \"example.org\",\n\t\t\t\t},\n\t\t\t},\n\t\t\toutput: authres.ResultFail,\n\t\t},\n\t\t{ // 2\n\t\t\tfromDomain: \"example.org\",\n\t\t\trecord:     &Record{},\n\t\t\tresults: []authres.Result{\n\t\t\t\t&authres.SPFResult{\n\t\t\t\t\tValue: authres.ResultPass,\n\t\t\t\t\tFrom:  \"example.org\",\n\t\t\t\t\tHelo:  \"mx.example.org\",\n\t\t\t\t},\n\t\t\t\t&authres.DKIMResult{\n\t\t\t\t\tValue:  authres.ResultNone,\n\t\t\t\t\tDomain: \"example.org\",\n\t\t\t\t},\n\t\t\t},\n\t\t\toutput: authres.ResultPass,\n\t\t},\n\t\t{ // 3\n\t\t\tfromDomain: \"example.org\",\n\t\t\trecord:     &Record{},\n\t\t\tresults: []authres.Result{\n\t\t\t\t&authres.SPFResult{\n\t\t\t\t\tValue: authres.ResultFail,\n\t\t\t\t\tFrom:  \"example.org\",\n\t\t\t\t\tHelo:  \"mx.example.org\",\n\t\t\t\t},\n\t\t\t\t&authres.DKIMResult{\n\t\t\t\t\tValue:  authres.ResultNone,\n\t\t\t\t\tDomain: \"example.org\",\n\t\t\t\t},\n\t\t\t},\n\t\t\toutput: authres.ResultFail,\n\t\t},\n\t\t{ // 4\n\t\t\tfromDomain: \"example.org\",\n\t\t\trecord:     &Record{},\n\t\t\tresults: []authres.Result{\n\t\t\t\t&authres.SPFResult{\n\t\t\t\t\tValue: authres.ResultPass,\n\t\t\t\t\tFrom:  \"example.com\",\n\t\t\t\t\tHelo:  \"mx.example.com\",\n\t\t\t\t},\n\t\t\t\t&authres.DKIMResult{\n\t\t\t\t\tValue:  authres.ResultNone,\n\t\t\t\t\tDomain: \"example.org\",\n\t\t\t\t},\n\t\t\t},\n\t\t\toutput: authres.ResultFail,\n\t\t},\n\t\t{ // 5\n\t\t\tfromDomain: \"example.com\",\n\t\t\trecord:     &Record{},\n\t\t\tresults: []authres.Result{\n\t\t\t\t&authres.SPFResult{\n\t\t\t\t\tValue: authres.ResultPass,\n\t\t\t\t\tFrom:  \"cbg.bounces.example.com\",\n\t\t\t\t\tHelo:  \"mx.example.com\",\n\t\t\t\t},\n\t\t\t\t&authres.DKIMResult{\n\t\t\t\t\tValue:  authres.ResultNone,\n\t\t\t\t\tDomain: \"example.org\",\n\t\t\t\t},\n\t\t\t},\n\t\t\toutput: authres.ResultPass,\n\t\t},\n\t\t{ // 6\n\t\t\tfromDomain: \"example.com\",\n\t\t\trecord: &Record{\n\t\t\t\tSPFAlignment: dmarc.AlignmentStrict,\n\t\t\t},\n\t\t\tresults: []authres.Result{\n\t\t\t\t&authres.SPFResult{\n\t\t\t\t\tValue: authres.ResultPass,\n\t\t\t\t\tFrom:  \"cbg.bounces.example.com\",\n\t\t\t\t\tHelo:  \"mx.example.com\",\n\t\t\t\t},\n\t\t\t\t&authres.DKIMResult{\n\t\t\t\t\tValue:  authres.ResultNone,\n\t\t\t\t\tDomain: \"example.org\",\n\t\t\t\t},\n\t\t\t},\n\t\t\toutput: authres.ResultFail,\n\t\t},\n\t\t{ // 7\n\t\t\tfromDomain: \"example.org\",\n\t\t\trecord:     &Record{},\n\t\t\tresults: []authres.Result{\n\t\t\t\t&authres.DKIMResult{\n\t\t\t\t\tValue:  authres.ResultFail,\n\t\t\t\t\tDomain: \"example.org\",\n\t\t\t\t},\n\t\t\t\t&authres.SPFResult{\n\t\t\t\t\tValue: authres.ResultNone,\n\t\t\t\t\tFrom:  \"example.org\",\n\t\t\t\t\tHelo:  \"mx.example.org\",\n\t\t\t\t},\n\t\t\t},\n\t\t\toutput: authres.ResultFail,\n\t\t},\n\t\t{ // 8\n\t\t\tfromDomain: \"example.org\",\n\t\t\trecord:     &Record{},\n\t\t\tresults: []authres.Result{\n\t\t\t\t&authres.DKIMResult{\n\t\t\t\t\tValue:  authres.ResultPass,\n\t\t\t\t\tDomain: \"example.org\",\n\t\t\t\t},\n\t\t\t\t&authres.SPFResult{\n\t\t\t\t\tValue: authres.ResultNone,\n\t\t\t\t\tFrom:  \"example.org\",\n\t\t\t\t\tHelo:  \"mx.example.org\",\n\t\t\t\t},\n\t\t\t},\n\t\t\toutput: authres.ResultPass,\n\t\t},\n\t\t{ // 9\n\t\t\tfromDomain: \"example.com\",\n\t\t\trecord:     &Record{},\n\t\t\tresults: []authres.Result{\n\t\t\t\t&authres.SPFResult{\n\t\t\t\t\tValue: authres.ResultPass,\n\t\t\t\t\tFrom:  \"cbg.bounces.example.com\",\n\t\t\t\t\tHelo:  \"mx.example.com\",\n\t\t\t\t},\n\t\t\t\t&authres.DKIMResult{\n\t\t\t\t\tValue:  authres.ResultPass,\n\t\t\t\t\tDomain: \"example.com\",\n\t\t\t\t},\n\t\t\t},\n\t\t\toutput: authres.ResultPass,\n\t\t},\n\t\t{ // 10\n\t\t\tfromDomain: \"example.com\",\n\t\t\trecord: &Record{\n\t\t\t\tSPFAlignment: dmarc.AlignmentRelaxed,\n\t\t\t},\n\t\t\tresults: []authres.Result{\n\t\t\t\t&authres.SPFResult{\n\t\t\t\t\tValue: authres.ResultPass,\n\t\t\t\t\tFrom:  \"cbg.bounces.example.com\",\n\t\t\t\t\tHelo:  \"mx.example.com\",\n\t\t\t\t},\n\t\t\t\t&authres.DKIMResult{\n\t\t\t\t\tValue:  authres.ResultFail,\n\t\t\t\t\tDomain: \"example.com\",\n\t\t\t\t},\n\t\t\t},\n\t\t\toutput: authres.ResultPass,\n\t\t},\n\t\t{ // 11\n\t\t\tfromDomain: \"example.com\",\n\t\t\trecord: &Record{\n\t\t\t\tSPFAlignment: dmarc.AlignmentStrict,\n\t\t\t},\n\t\t\tresults: []authres.Result{\n\t\t\t\t&authres.SPFResult{\n\t\t\t\t\tValue: authres.ResultPass,\n\t\t\t\t\tFrom:  \"cbg.bounces.example.com\",\n\t\t\t\t\tHelo:  \"mx.example.com\",\n\t\t\t\t},\n\t\t\t\t&authres.DKIMResult{\n\t\t\t\t\tValue:  authres.ResultPass,\n\t\t\t\t\tDomain: \"example.com\",\n\t\t\t\t},\n\t\t\t},\n\t\t\toutput: authres.ResultPass,\n\t\t},\n\t\t{ // 12\n\t\t\tfromDomain: \"example.com\",\n\t\t\trecord: &Record{\n\t\t\t\tSPFAlignment:  dmarc.AlignmentStrict,\n\t\t\t\tDKIMAlignment: dmarc.AlignmentStrict,\n\t\t\t},\n\t\t\tresults: []authres.Result{\n\t\t\t\t&authres.SPFResult{\n\t\t\t\t\tValue: authres.ResultPass,\n\t\t\t\t\tFrom:  \"cbg.bounces.example.com\",\n\t\t\t\t\tHelo:  \"mx.example.com\",\n\t\t\t\t},\n\t\t\t\t&authres.DKIMResult{\n\t\t\t\t\tValue:  authres.ResultFail,\n\t\t\t\t\tDomain: \"cbg.example.com\",\n\t\t\t\t},\n\t\t\t},\n\t\t\toutput: authres.ResultFail,\n\t\t},\n\t\t{ // 13\n\t\t\tfromDomain: \"example.org\",\n\t\t\trecord:     &Record{},\n\t\t\tresults: []authres.Result{\n\t\t\t\t&authres.DKIMResult{\n\t\t\t\t\tValue:  authres.ResultFail,\n\t\t\t\t\tDomain: \"example.org\",\n\t\t\t\t},\n\t\t\t\t&authres.DKIMResult{\n\t\t\t\t\tValue:  authres.ResultPass,\n\t\t\t\t\tDomain: \"example.net\",\n\t\t\t\t},\n\t\t\t\t&authres.DKIMResult{\n\t\t\t\t\tValue:  authres.ResultPass,\n\t\t\t\t\tDomain: \"example.org\",\n\t\t\t\t},\n\t\t\t\t&authres.DKIMResult{\n\t\t\t\t\tValue:  authres.ResultFail,\n\t\t\t\t\tDomain: \"example.com\",\n\t\t\t\t},\n\t\t\t\t&authres.SPFResult{\n\t\t\t\t\tValue: authres.ResultNone,\n\t\t\t\t\tFrom:  \"example.org\",\n\t\t\t\t\tHelo:  \"mx.example.org\",\n\t\t\t\t},\n\t\t\t},\n\t\t\toutput: authres.ResultPass,\n\t\t},\n\t\t{ // 14\n\t\t\tfromDomain: \"example.com\",\n\t\t\trecord:     &Record{},\n\t\t\tresults: []authres.Result{\n\t\t\t\t&authres.SPFResult{\n\t\t\t\t\tValue: authres.ResultPass,\n\t\t\t\t\tFrom:  \"\",\n\t\t\t\t\tHelo:  \"mx.example.com\",\n\t\t\t\t},\n\t\t\t\t&authres.DKIMResult{\n\t\t\t\t\tValue:  authres.ResultNone,\n\t\t\t\t\tDomain: \"example.org\",\n\t\t\t\t},\n\t\t\t},\n\t\t\toutput: authres.ResultPass,\n\t\t},\n\t\t{ // 15\n\t\t\tfromDomain: \"example.com\",\n\t\t\trecord: &Record{\n\t\t\t\tSPFAlignment: dmarc.AlignmentStrict,\n\t\t\t},\n\t\t\tresults: []authres.Result{\n\t\t\t\t&authres.SPFResult{\n\t\t\t\t\tValue: authres.ResultPass,\n\t\t\t\t\tFrom:  \"\",\n\t\t\t\t\tHelo:  \"mx.example.com\",\n\t\t\t\t},\n\t\t\t\t&authres.DKIMResult{\n\t\t\t\t\tValue:  authres.ResultNone,\n\t\t\t\t\tDomain: \"example.org\",\n\t\t\t\t},\n\t\t\t},\n\t\t\toutput: authres.ResultFail,\n\t\t},\n\t\t{ // 16\n\t\t\tfromDomain: \"example.com\",\n\t\t\trecord:     &Record{},\n\t\t\tresults: []authres.Result{\n\t\t\t\t&authres.SPFResult{\n\t\t\t\t\tValue: authres.ResultTempError,\n\t\t\t\t\tFrom:  \"\",\n\t\t\t\t\tHelo:  \"mx.example.com\",\n\t\t\t\t},\n\t\t\t\t&authres.DKIMResult{\n\t\t\t\t\tValue:  authres.ResultNone,\n\t\t\t\t\tDomain: \"example.org\",\n\t\t\t\t},\n\t\t\t},\n\t\t\toutput: authres.ResultTempError,\n\t\t},\n\t\t{ // 17\n\t\t\tfromDomain: \"example.com\",\n\t\t\trecord:     &Record{},\n\t\t\tresults: []authres.Result{\n\t\t\t\t&authres.DKIMResult{\n\t\t\t\t\tValue:  authres.ResultTempError,\n\t\t\t\t\tDomain: \"example.com\",\n\t\t\t\t},\n\t\t\t\t&authres.SPFResult{\n\t\t\t\t\tValue: authres.ResultNone,\n\t\t\t\t\tFrom:  \"example.org\",\n\t\t\t\t\tHelo:  \"mx.example.org\",\n\t\t\t\t},\n\t\t\t},\n\t\t\toutput: authres.ResultTempError,\n\t\t},\n\t\t{ // 18\n\t\t\tfromDomain: \"example.com\",\n\t\t\trecord:     &Record{},\n\t\t\tresults: []authres.Result{\n\t\t\t\t&authres.SPFResult{\n\t\t\t\t\tValue: authres.ResultTempError,\n\t\t\t\t\tFrom:  \"\",\n\t\t\t\t\tHelo:  \"mx.example.com\",\n\t\t\t\t},\n\t\t\t\t&authres.DKIMResult{\n\t\t\t\t\tValue:  authres.ResultPass,\n\t\t\t\t\tDomain: \"example.com\",\n\t\t\t\t},\n\t\t\t},\n\t\t\toutput: authres.ResultPass,\n\t\t},\n\t\t{ // 19\n\t\t\tfromDomain: \"example.com\",\n\t\t\trecord:     &Record{},\n\t\t\tresults: []authres.Result{\n\t\t\t\t&authres.SPFResult{\n\t\t\t\t\tValue: authres.ResultPass,\n\t\t\t\t\tFrom:  \"\",\n\t\t\t\t\tHelo:  \"mx.example.com\",\n\t\t\t\t},\n\t\t\t\t&authres.DKIMResult{\n\t\t\t\t\tValue:  authres.ResultTempError,\n\t\t\t\t\tDomain: \"example.com\",\n\t\t\t\t},\n\t\t\t},\n\t\t\toutput: authres.ResultPass,\n\t\t},\n\t\t{ // 20\n\t\t\tfromDomain: \"example.org\",\n\t\t\trecord:     &Record{},\n\t\t\tresults: []authres.Result{\n\t\t\t\t&authres.DKIMResult{\n\t\t\t\t\tValue:  authres.ResultPass,\n\t\t\t\t\tDomain: \"example.org\",\n\t\t\t\t},\n\t\t\t\t&authres.DKIMResult{\n\t\t\t\t\tValue:  authres.ResultTempError,\n\t\t\t\t\tDomain: \"example.org\",\n\t\t\t\t},\n\t\t\t\t&authres.SPFResult{\n\t\t\t\t\tValue: authres.ResultNone,\n\t\t\t\t\tFrom:  \"example.org\",\n\t\t\t\t\tHelo:  \"mx.example.org\",\n\t\t\t\t},\n\t\t\t},\n\t\t\toutput: authres.ResultPass,\n\t\t},\n\t\t{ // 21\n\t\t\tfromDomain: \"example.org\",\n\t\t\trecord:     &Record{},\n\t\t\tresults: []authres.Result{\n\t\t\t\t&authres.DKIMResult{\n\t\t\t\t\tValue:  authres.ResultFail,\n\t\t\t\t\tDomain: \"example.org\",\n\t\t\t\t},\n\t\t\t\t&authres.DKIMResult{\n\t\t\t\t\tValue:  authres.ResultTempError,\n\t\t\t\t\tDomain: \"example.org\",\n\t\t\t\t},\n\t\t\t\t&authres.SPFResult{\n\t\t\t\t\tValue: authres.ResultNone,\n\t\t\t\t\tFrom:  \"example.org\",\n\t\t\t\t\tHelo:  \"mx.example.org\",\n\t\t\t\t},\n\t\t\t},\n\t\t\toutput: authres.ResultTempError,\n\t\t},\n\t\t{ // 22\n\t\t\tfromDomain: \"example.org\",\n\t\t\trecord:     &Record{},\n\t\t\tresults: []authres.Result{\n\t\t\t\t&authres.DKIMResult{\n\t\t\t\t\tValue:  authres.ResultNone,\n\t\t\t\t\tDomain: \"example.org\",\n\t\t\t\t},\n\t\t\t\t&authres.SPFResult{\n\t\t\t\t\tValue: authres.ResultNone,\n\t\t\t\t\tFrom:  \"example.org\",\n\t\t\t\t\tHelo:  \"mx.example.org\",\n\t\t\t\t},\n\t\t\t},\n\t\t\toutput: authres.ResultFail,\n\t\t},\n\t\t{ // 23\n\t\t\tfromDomain: \"sub.example.org\",\n\t\t\trecord:     &Record{},\n\t\t\tresults: []authres.Result{\n\t\t\t\t&authres.DKIMResult{\n\t\t\t\t\tValue:  authres.ResultPass,\n\t\t\t\t\tDomain: \"mx.example.org\",\n\t\t\t\t},\n\t\t\t\t&authres.SPFResult{\n\t\t\t\t\tValue: authres.ResultNone,\n\t\t\t\t\tFrom:  \"example.org\",\n\t\t\t\t\tHelo:  \"mx.example.org\",\n\t\t\t\t},\n\t\t\t},\n\t\t\toutput: authres.ResultPass,\n\t\t},\n\t}\n\tfor i, case_ := range cases {\n\t\ttest(i, case_)\n\t}\n}\n\nfunc TestExtractDomains(t *testing.T) {\n\ttype tCase struct {\n\t\thdr string\n\n\t\tfromDomain string\n\t}\n\ttest := func(i int, c tCase) {\n\t\thdr, err := textproto.ReadHeader(bufio.NewReader(strings.NewReader(c.hdr + \"\\n\\n\")))\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\tdomain, err := ExtractFromDomain(hdr)\n\t\tif c.fromDomain == \"\" && err == nil {\n\t\t\tt.Errorf(\"%d: expected failure, got fromDomain = %s\", i, domain)\n\t\t\treturn\n\t\t}\n\t\tif c.fromDomain != \"\" && err != nil {\n\t\t\tt.Errorf(\"%d: unexpected error: %v\", i, err)\n\t\t\treturn\n\t\t}\n\t\tif domain != c.fromDomain {\n\t\t\tt.Errorf(\"%d: want fromDomain = %v but got %s\", i, c.fromDomain, domain)\n\t\t}\n\t}\n\n\tcases := []tCase{\n\t\t{\n\t\t\thdr:        `From: <test@example.org>`,\n\t\t\tfromDomain: \"example.org\",\n\t\t},\n\t\t{\n\t\t\thdr:        `From: <test@foo.example.org>`,\n\t\t\tfromDomain: \"foo.example.org\",\n\t\t},\n\t\t{\n\t\t\thdr: `From: <test@foo.example.org>, <test@bar.example.org>`,\n\t\t},\n\t\t{\n\t\t\thdr: `From: <test@foo.example.org>,\nFrom: <test@bar.example.org>`,\n\t\t},\n\t\t{\n\t\t\thdr: `From: <test@>`,\n\t\t},\n\t\t{\n\t\t\thdr: `From: `,\n\t\t},\n\t\t{\n\t\t\thdr: `From: foo`,\n\t\t},\n\t}\n\tfor i, case_ := range cases {\n\t\ttest(i, case_)\n\t}\n}\n"
  },
  {
    "path": "internal/dmarc/verifier.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage dmarc\n\nimport (\n\t\"context\"\n\t\"math/rand\"\n\t\"net\"\n\t\"runtime/trace\"\n\t\"strings\"\n\n\t\"github.com/emersion/go-message/textproto\"\n\t\"github.com/emersion/go-msgauth/authres\"\n\t\"github.com/emersion/go-msgauth/dmarc\"\n)\n\ntype verifyData struct {\n\tpolicyDomain string\n\tfromDomain   string\n\trecord       *Record\n\trecordErr    error\n}\n\n// errPanic is used to propagate the panic() from the FetchRecord\n// goroutine to the goroutine that called Apply.\ntype errPanic struct {\n\terr interface{}\n}\n\nfunc (errPanic) Error() string {\n\treturn \"panic during policy fetch\"\n}\n\n// Verifier is the structure that wraps all state necessary to verify a\n// single message using DMARC checks.\n//\n// It cannot be reused.\ntype Verifier struct {\n\tfetchCh     chan verifyData\n\tfetchCancel context.CancelFunc\n\n\tresolver Resolver\n\n\t// TODO(GH #206): DMARC reporting\n\t// FailureReportFunc is the callback that is called when a failure report\n\t// is generated. If it is nil - failure reports generation is disabled.\n\t// FailureReportFunc func(textproto.Header, io.Reader)\n}\n\nfunc NewVerifier(r Resolver) *Verifier {\n\treturn &Verifier{\n\t\tfetchCh:  make(chan verifyData, 1),\n\t\tresolver: r,\n\t}\n}\n\nfunc (v *Verifier) Close() error {\n\tif v.fetchCancel != nil {\n\t\tv.fetchCancel()\n\t}\n\treturn nil\n}\n\n// FetchRecord prepares the Verifier by starting the policy lookup. Lookup is\n// performed asynchronously to improve performance.\n//\n// If panic occurs in the lookup goroutine - call to Apply will panic.\nfunc (v *Verifier) FetchRecord(ctx context.Context, header textproto.Header) {\n\tfromDomain, err := ExtractFromDomain(header)\n\tif err != nil {\n\t\tv.fetchCh <- verifyData{\n\t\t\trecordErr: err,\n\t\t}\n\t\treturn\n\t}\n\n\tctx, v.fetchCancel = context.WithCancel(ctx)\n\tgo func() {\n\t\tdefer func() {\n\t\t\tif err := recover(); err != nil {\n\t\t\t\tv.fetchCh <- verifyData{\n\t\t\t\t\trecordErr: errPanic{err: err},\n\t\t\t\t}\n\t\t\t}\n\t\t}()\n\n\t\tdefer trace.StartRegion(ctx, \"DMARC/FetchRecord\").End()\n\n\t\tpolicyDomain, record, err := FetchRecord(ctx, v.resolver, fromDomain)\n\t\tv.fetchCh <- verifyData{\n\t\t\tpolicyDomain: policyDomain,\n\t\t\tfromDomain:   fromDomain,\n\t\t\trecord:       record,\n\t\t\trecordErr:    err,\n\t\t}\n\t}()\n}\n\n// Apply actually performs all actions necessary to apply a DMARC policy to the message.\n//\n// The authRes slice should contain results for DKIM and SPF checks. FetchRecord should be\n// caled before calling this function.\n//\n// It returns the Authentication-Result field to be included in the message (as\n// a part of the EvalResult struct) and the appropriate action that should be\n// taken by the MTA. In case of PolicyReject, caller should inspect the\n// Result.Value to determine whether to use a temporary or permanent error code\n// as Apply implements the 'fail closed' strategy for handling of temporary\n// errors.\n//\n// Additionally, it relies on the math/rand default source to be initialized to determine\n// whether to apply a policy with the pct key.\nfunc (v *Verifier) Apply(authRes []authres.Result) (EvalResult, Policy) {\n\tdata := <-v.fetchCh\n\tif data.recordErr != nil {\n\t\tresult := authres.DMARCResult{\n\t\t\tValue:  authres.ResultPermError,\n\t\t\tReason: \"Policy lookup failed: \" + data.recordErr.Error(),\n\t\t\t// If may be empty, but it is fine (it will not be included in the field then).\n\t\t\tFrom: data.fromDomain,\n\t\t}\n\t\tif dnsErr, ok := data.recordErr.(*net.DNSError); ok && dnsErr.Temporary() {\n\t\t\tresult.Value = authres.ResultTempError\n\t\t\t// 'fail closed' behavior, reject the message if a temporary error\n\t\t\t// occurs.\n\t\t\treturn EvalResult{\n\t\t\t\tAuthres: result,\n\t\t\t}, dmarc.PolicyReject\n\t\t}\n\t\treturn EvalResult{\n\t\t\tAuthres: result,\n\t\t}, dmarc.PolicyNone\n\t}\n\tif data.record == nil {\n\t\treturn EvalResult{\n\t\t\tAuthres: authres.DMARCResult{\n\t\t\t\tValue: authres.ResultNone,\n\t\t\t\tFrom:  data.fromDomain,\n\t\t\t},\n\t\t}, dmarc.PolicyNone\n\t}\n\n\tresult := EvaluateAlignment(data.fromDomain, data.record, authRes)\n\tif result.Authres.Value == authres.ResultPass || result.Authres.Value == authres.ResultNone {\n\t\treturn result, dmarc.PolicyNone\n\t}\n\n\tif data.record.Percent != nil && rand.Int31n(100) > int32(*data.record.Percent) {\n\t\treturn result, dmarc.PolicyNone\n\t}\n\n\tpolicy := data.record.Policy\n\tif !strings.EqualFold(data.policyDomain, data.fromDomain) && data.record.SubdomainPolicy != \"\" {\n\t\tpolicy = data.record.SubdomainPolicy\n\t}\n\n\treturn result, policy\n}\n"
  },
  {
    "path": "internal/dmarc/verifier_test.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage dmarc\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"errors\"\n\t\"net\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/emersion/go-message/textproto\"\n\t\"github.com/emersion/go-msgauth/authres\"\n\t\"github.com/foxcpp/go-mockdns\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestDMARC(t *testing.T) {\n\ttest := func(zones map[string]mockdns.Zone, hdr string, authres []authres.Result, policyApplied Policy, dmarcRes authres.ResultValue) {\n\t\tt.Helper()\n\t\tv := NewVerifier(&mockdns.Resolver{Zones: zones})\n\t\tdefer func() {\n\t\t\trequire.NoError(t, v.Close())\n\t\t}()\n\n\t\thdrParsed, err := textproto.ReadHeader(bufio.NewReader(strings.NewReader(hdr)))\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tv.FetchRecord(context.Background(), hdrParsed)\n\t\tevalRes, policy := v.Apply(authres)\n\n\t\tif policy != policyApplied {\n\t\t\tt.Errorf(\"expected applied policy to be '%v', got '%v'\", policyApplied, policy)\n\t\t}\n\t\tif evalRes.Authres.Value != dmarcRes {\n\t\t\tt.Errorf(\"expected DMARC result to be '%v', got '%v'\", dmarcRes, evalRes.Authres.Value)\n\t\t}\n\t}\n\n\t// No policy => DMARC 'none'\n\ttest(map[string]mockdns.Zone{}, \"From: hello@example.org\\r\\n\\r\\n\", []authres.Result{\n\t\t&authres.DKIMResult{Value: authres.ResultPass, Domain: \"example.org\"},\n\t\t&authres.SPFResult{Value: authres.ResultNone, From: \"example.org\", Helo: \"mx.example.org\"},\n\t}, PolicyNone, authres.ResultNone)\n\n\t// Policy present & identifiers align => DMARC 'pass'\n\ttest(map[string]mockdns.Zone{\n\t\t\"_dmarc.example.org.\": {\n\t\t\tTXT: []string{\"v=DMARC1; p=none\"},\n\t\t},\n\t}, \"From: hello@example.org\\r\\n\\r\\n\", []authres.Result{\n\t\t&authres.DKIMResult{Value: authres.ResultPass, Domain: \"example.org\"},\n\t\t&authres.SPFResult{Value: authres.ResultNone, From: \"example.org\", Helo: \"mx.example.org\"},\n\t}, PolicyNone, authres.ResultPass)\n\n\t// No SPF check run => DMARC 'none', no action taken\n\ttest(map[string]mockdns.Zone{\n\t\t\"_dmarc.example.org.\": {\n\t\t\tTXT: []string{\"v=DMARC1; p=reject\"},\n\t\t},\n\t}, \"From: hello@example.org\\r\\n\\r\\n\", []authres.Result{\n\t\t&authres.DKIMResult{Value: authres.ResultPass, Domain: \"example.org\"},\n\t}, PolicyNone, authres.ResultNone)\n\n\t// No DKIM check run => DMARC 'none', no action taken\n\ttest(map[string]mockdns.Zone{\n\t\t\"_dmarc.example.org.\": {\n\t\t\tTXT: []string{\"v=DMARC1; p=reject\"},\n\t\t},\n\t}, \"From: hello@example.org\\r\\n\\r\\n\", []authres.Result{\n\t\t&authres.SPFResult{Value: authres.ResultPass, From: \"example.org\", Helo: \"mx.example.org\"},\n\t}, PolicyNone, authres.ResultNone)\n\n\t// Check org. domain and from domain, prefer from domain.\n\t// https://tools.ietf.org/html/rfc7489#section-6.6.3\n\ttest(map[string]mockdns.Zone{\n\t\t\"_dmarc.example.org.\": {\n\t\t\tTXT: []string{\"v=DMARC1; p=none\"},\n\t\t},\n\t}, \"From: hello@sub.example.org\\r\\n\\r\\n\", []authres.Result{\n\t\t&authres.DKIMResult{Value: authres.ResultPass, Domain: \"example.org\"},\n\t\t&authres.SPFResult{Value: authres.ResultNone, From: \"example.org\", Helo: \"mx.example.org\"},\n\t}, PolicyNone, authres.ResultPass)\n\ttest(map[string]mockdns.Zone{\n\t\t\"_dmarc.sub.example.org.\": {\n\t\t\tTXT: []string{\"v=DMARC1; p=none\"},\n\t\t},\n\t}, \"From: hello@sub.example.org\\r\\n\\r\\n\", []authres.Result{\n\t\t&authres.DKIMResult{Value: authres.ResultPass, Domain: \"example.org\"},\n\t\t&authres.SPFResult{Value: authres.ResultNone, From: \"example.org\", Helo: \"mx.example.org\"},\n\t}, PolicyNone, authres.ResultPass)\n\ttest(map[string]mockdns.Zone{\n\t\t\"_dmarc.sub.example.org.\": {\n\t\t\tTXT: []string{\"v=DMARC1; p=none\"},\n\t\t},\n\t\t\"_dmarc.example.org.\": {\n\t\t\tTXT: []string{\"v=malformed\"},\n\t\t},\n\t}, \"From: hello@sub.example.org\\r\\n\\r\\n\", []authres.Result{\n\t\t&authres.DKIMResult{Value: authres.ResultPass, Domain: \"example.org\"},\n\t\t&authres.SPFResult{Value: authres.ResultNone, From: \"example.org\", Helo: \"mx.example.org\"},\n\t}, PolicyNone, authres.ResultPass)\n\n\t// Non-DMARC records are ignored.\n\t// https://tools.ietf.org/html/rfc7489#section-6.6.3\n\ttest(map[string]mockdns.Zone{\n\t\t\"_dmarc.example.org.\": {\n\t\t\tTXT: []string{\"ignore\", \"v=DMARC1; p=none\"},\n\t\t},\n\t}, \"From: hello@sub.example.org\\r\\n\\r\\n\", []authres.Result{\n\t\t&authres.DKIMResult{Value: authres.ResultPass, Domain: \"example.org\"},\n\t\t&authres.SPFResult{Value: authres.ResultNone, From: \"example.org\", Helo: \"mx.example.org\"},\n\t}, PolicyNone, authres.ResultPass)\n\n\t// Multiple policies => no policy.\n\t// https://tools.ietf.org/html/rfc7489#section-6.6.3\n\ttest(map[string]mockdns.Zone{\n\t\t\"_dmarc.example.org.\": {\n\t\t\tTXT: []string{\"v=DMARC1; p=reject\", \"v=DMARC1; p=none\"},\n\t\t},\n\t}, \"From: hello@sub.example.org\\r\\n\\r\\n\", []authres.Result{\n\t\t&authres.DKIMResult{Value: authres.ResultPass, Domain: \"example.org\"},\n\t\t&authres.SPFResult{Value: authres.ResultNone, From: \"example.org\", Helo: \"mx.example.org\"},\n\t}, PolicyNone, authres.ResultNone)\n\n\t// Malformed policy => no policy\n\ttest(map[string]mockdns.Zone{\n\t\t\"_dmarc.example.com.\": {\n\t\t\tTXT: []string{\"v=aaaa\"},\n\t\t},\n\t}, \"From: hello@example.com\\r\\n\\r\\n\", []authres.Result{\n\t\t&authres.DKIMResult{Value: authres.ResultPass, Domain: \"example.org\"},\n\t\t&authres.SPFResult{Value: authres.ResultNone, From: \"example.org\", Helo: \"mx.example.org\"},\n\t}, PolicyNone, authres.ResultNone)\n\n\t// Policy fetch error => DMARC 'permerror' but the message\n\t// is accepted.\n\ttest(map[string]mockdns.Zone{\n\t\t\"_dmarc.example.com.\": {\n\t\t\tErr: errors.New(\"the dns server is going insane\"),\n\t\t},\n\t}, \"From: hello@example.com\\r\\n\\r\\n\", []authres.Result{\n\t\t&authres.DKIMResult{Value: authres.ResultPass, Domain: \"example.org\"},\n\t\t&authres.SPFResult{Value: authres.ResultNone, From: \"example.org\", Helo: \"mx.example.org\"},\n\t}, PolicyNone, authres.ResultPermError)\n\n\t// Policy fetch error => DMARC 'temperror' but the message\n\t// is accepted (\"fail closed\")\n\ttest(map[string]mockdns.Zone{\n\t\t\"_dmarc.example.com.\": {\n\t\t\tErr: &net.DNSError{\n\t\t\t\tErr:         \"the dns server is going insane, temporary\",\n\t\t\t\tIsTemporary: true,\n\t\t\t},\n\t\t},\n\t}, \"From: hello@example.com\\r\\n\\r\\n\", []authres.Result{\n\t\t&authres.DKIMResult{Value: authres.ResultPass, Domain: \"example.org\"},\n\t\t&authres.SPFResult{Value: authres.ResultNone, From: \"example.org\", Helo: \"mx.example.org\"},\n\t}, PolicyReject, authres.ResultTempError)\n\n\t// Misaligned From vs DKIM => DMARC 'fail'.\n\t// Side note: More comprehensive tests for alignment evaluation\n\t// can be found in check/dmarc/evaluate_test.go. This test merely checks\n\t// that the correct action is taken based on the policy.\n\ttest(map[string]mockdns.Zone{\n\t\t\"_dmarc.example.com.\": {\n\t\t\tTXT: []string{\"v=DMARC1; p=none\"},\n\t\t},\n\t}, \"From: hello@example.com\\r\\n\\r\\n\", []authres.Result{\n\t\t&authres.DKIMResult{Value: authres.ResultPass, Domain: \"example.org\"},\n\t\t&authres.SPFResult{Value: authres.ResultNone, From: \"example.org\", Helo: \"mx.example.org\"},\n\t}, PolicyNone, authres.ResultFail)\n\n\t// Misaligned From vs DKIM => DMARC 'fail', policy says to reject\n\ttest(map[string]mockdns.Zone{\n\t\t\"_dmarc.example.com.\": {\n\t\t\tTXT: []string{\"v=DMARC1; p=reject\"},\n\t\t},\n\t}, \"From: hello@example.com\\r\\n\\r\\n\", []authres.Result{\n\t\t&authres.DKIMResult{Value: authres.ResultPass, Domain: \"example.org\"},\n\t\t&authres.SPFResult{Value: authres.ResultNone, From: \"example.org\", Helo: \"mx.example.org\"},\n\t}, PolicyReject, authres.ResultFail)\n\n\t// Misaligned From vs DKIM => DMARC 'fail'\n\t// Subdomain policy requests no action, main domain policy says to reject.\n\ttest(map[string]mockdns.Zone{\n\t\t\"_dmarc.example.com.\": {\n\t\t\tTXT: []string{\"v=DMARC1; sp=none; p=reject\"},\n\t\t},\n\t}, \"From: hello@sub.example.com\\r\\n\\r\\n\", []authres.Result{\n\t\t&authres.DKIMResult{Value: authres.ResultPass, Domain: \"example.org\"},\n\t\t&authres.SPFResult{Value: authres.ResultNone, From: \"example.org\", Helo: \"mx.example.org\"},\n\t}, PolicyNone, authres.ResultFail)\n\n\t// Misaligned From vs DKIM => DMARC 'fail', policy says to quarantine.\n\ttest(map[string]mockdns.Zone{\n\t\t\"_dmarc.example.com.\": {\n\t\t\tTXT: []string{\"v=DMARC1; p=quarantine\"},\n\t\t},\n\t}, \"From: hello@example.com\\r\\n\\r\\n\", []authres.Result{\n\t\t&authres.DKIMResult{Value: authres.ResultPass, Domain: \"example.org\"},\n\t\t&authres.SPFResult{Value: authres.ResultNone, From: \"example.org\", Helo: \"mx.example.org\"},\n\t}, PolicyQuarantine, authres.ResultFail)\n}\n"
  },
  {
    "path": "internal/dsn/dsn.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\n// Package dsn contains the utilities used for dsn message (DSN) generation.\n//\n// It implements RFC 3464 and RFC 3462.\npackage dsn\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"strings\"\n\t\"text/template\"\n\t\"time\"\n\n\t\"github.com/emersion/go-message/textproto\"\n\t\"github.com/emersion/go-smtp\"\n\t\"github.com/foxcpp/maddy/framework/address\"\n\t\"github.com/foxcpp/maddy/framework/dns\"\n)\n\ntype ReportingMTAInfo struct {\n\tReportingMTA    string\n\tReceivedFromMTA string\n\n\t// Message sender address, included as 'X-Maddy-Sender: rfc822; ADDR' field.\n\tXSender string\n\n\t// Message identifier, included as 'X-Maddy-MsgId: MSGID' field.\n\tXMessageID string\n\n\t// Time when message was enqueued for delivery by Reporting MTA.\n\tArrivalDate time.Time\n\n\t// Time when message delivery was attempted last time.\n\tLastAttemptDate time.Time\n}\n\nfunc (info ReportingMTAInfo) WriteTo(utf8 bool, w io.Writer) error {\n\t// DSN format uses structure similar to MIME header, so we reuse\n\t// MIME generator here.\n\th := textproto.Header{}\n\n\tif info.ReportingMTA == \"\" {\n\t\treturn errors.New(\"dsn: Reporting-MTA field is mandatory\")\n\t}\n\n\treportingMTA, err := dns.SelectIDNA(utf8, info.ReportingMTA)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"dsn: cannot convert Reporting-MTA to a suitable representation: %w\", err)\n\t}\n\n\th.Add(\"Reporting-MTA\", \"dns; \"+reportingMTA)\n\n\tif info.ReceivedFromMTA != \"\" {\n\t\treceivedFromMTA, err := dns.SelectIDNA(utf8, info.ReceivedFromMTA)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"dsn: cannot convert Received-From-MTA to a suitable representation: %w\", err)\n\t\t}\n\n\t\th.Add(\"Received-From-MTA\", \"dns; \"+receivedFromMTA)\n\t}\n\n\tif info.XSender != \"\" {\n\t\tsender, err := address.SelectIDNA(utf8, info.XSender)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"dsn: cannot convert X-Maddy-Sender to a suitable representation: %w\", err)\n\t\t}\n\n\t\tif utf8 {\n\t\t\th.Add(\"X-Maddy-Sender\", \"utf8; \"+sender)\n\t\t} else {\n\t\t\th.Add(\"X-Maddy-Sender\", \"rfc822; \"+sender)\n\t\t}\n\t}\n\tif info.XMessageID != \"\" {\n\t\th.Add(\"X-Maddy-MsgID\", info.XMessageID)\n\t}\n\n\tif !info.ArrivalDate.IsZero() {\n\t\th.Add(\"Arrival-Date\", info.ArrivalDate.Format(\"Mon, 2 Jan 2006 15:04:05 -0700\"))\n\t}\n\tif !info.ArrivalDate.IsZero() {\n\t\th.Add(\"Last-Attempt-Date\", info.LastAttemptDate.Format(\"Mon, 2 Jan 2006 15:04:05 -0700\"))\n\t}\n\n\treturn textproto.WriteHeader(w, h)\n}\n\ntype Action string\n\nconst (\n\tActionFailed    Action = \"failed\"\n\tActionDelayed   Action = \"delayed\"\n\tActionDelivered Action = \"delivered\"\n\tActionRelayed   Action = \"relayed\"\n\tActionExpanded  Action = \"expanded\"\n)\n\ntype RecipientInfo struct {\n\tFinalRecipient string\n\tRemoteMTA      string\n\n\tAction Action\n\tStatus smtp.EnhancedCode\n\n\t// DiagnosticCode is the error that will be returned to the sender.\n\tDiagnosticCode error\n}\n\nfunc (info RecipientInfo) WriteTo(utf8 bool, w io.Writer) error {\n\t// DSN format uses structure similar to MIME header, so we reuse\n\t// MIME generator here.\n\th := textproto.Header{}\n\n\tif info.FinalRecipient == \"\" {\n\t\treturn errors.New(\"dsn: Final-Recipient is required\")\n\t}\n\tfinalRcpt, err := address.SelectIDNA(utf8, info.FinalRecipient)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"dsn: cannot convert Final-Recipient to a suitable representation: %w\", err)\n\t}\n\tif utf8 {\n\t\th.Add(\"Final-Recipient\", \"utf8; \"+finalRcpt)\n\t} else {\n\t\th.Add(\"Final-Recipient\", \"rfc822; \"+finalRcpt)\n\t}\n\n\tif info.Action == \"\" {\n\t\treturn errors.New(\"dsn: Action is required\")\n\t}\n\th.Add(\"Action\", string(info.Action))\n\tif info.Status[0] == 0 {\n\t\treturn errors.New(\"dsn: Status is required\")\n\t}\n\th.Add(\"Status\", fmt.Sprintf(\"%d.%d.%d\", info.Status[0], info.Status[1], info.Status[2]))\n\n\tif smtpErr, ok := info.DiagnosticCode.(*smtp.SMTPError); ok {\n\t\t// Error message may contain newlines if it is received from another SMTP server.\n\t\t// But we cannot directly insert CR/LF into Disagnostic-Code so rewrite it.\n\t\th.Add(\"Diagnostic-Code\", fmt.Sprintf(\"smtp; %d %d.%d.%d %s\",\n\t\t\tsmtpErr.Code, smtpErr.EnhancedCode[0], smtpErr.EnhancedCode[1], smtpErr.EnhancedCode[2],\n\t\t\tstrings.ReplaceAll(strings.ReplaceAll(smtpErr.Message, \"\\n\", \" \"), \"\\r\", \" \")))\n\t} else if utf8 {\n\t\t// It might contain Unicode, so don't include it if we are not allowed to.\n\t\t// ... I didn't bother implementing mangling logic to remove Unicode\n\t\t// characters.\n\t\terrorDesc := info.DiagnosticCode.Error()\n\t\terrorDesc = strings.ReplaceAll(strings.ReplaceAll(errorDesc, \"\\n\", \" \"), \"\\r\", \" \")\n\n\t\th.Add(\"Diagnostic-Code\", \"X-Maddy; \"+errorDesc)\n\t}\n\n\tif info.RemoteMTA != \"\" {\n\t\tremoteMTA, err := dns.SelectIDNA(utf8, info.RemoteMTA)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"dsn: cannot convert Remote-MTA to a suitable representation: %w\", err)\n\t\t}\n\n\t\th.Add(\"Remote-MTA\", \"dns; \"+remoteMTA)\n\t}\n\n\treturn textproto.WriteHeader(w, h)\n}\n\ntype Envelope struct {\n\tMsgID string\n\tFrom  string\n\tTo    string\n}\n\n// GenerateDSN is a top-level function that should be used for generation of the DSNs.\n//\n// DSN header will be returned, body itself will be written to outWriter.\nfunc GenerateDSN(utf8 bool, envelope Envelope, mtaInfo ReportingMTAInfo, rcptsInfo []RecipientInfo, failedHeader textproto.Header, outWriter io.Writer) (textproto.Header, error) {\n\tpartWriter := textproto.NewMultipartWriter(outWriter)\n\n\treportHeader := textproto.Header{}\n\treportHeader.Add(\"Date\", time.Now().Format(\"Mon, 2 Jan 2006 15:04:05 -0700\"))\n\treportHeader.Add(\"Message-Id\", envelope.MsgID)\n\treportHeader.Add(\"Content-Transfer-Encoding\", \"8bit\")\n\treportHeader.Add(\"Content-Type\", \"multipart/report; report-type=delivery-status; boundary=\"+partWriter.Boundary())\n\treportHeader.Add(\"MIME-Version\", \"1.0\")\n\treportHeader.Add(\"Auto-Submitted\", \"auto-replied\")\n\treportHeader.Add(\"To\", envelope.To)\n\treportHeader.Add(\"From\", envelope.From)\n\treportHeader.Add(\"Subject\", \"Undelivered Mail Returned to Sender\")\n\n\tif err := writeHumanReadablePart(partWriter, mtaInfo, rcptsInfo); err != nil {\n\t\treturn textproto.Header{}, err\n\t}\n\tif err := writeMachineReadablePart(utf8, partWriter, mtaInfo, rcptsInfo); err != nil {\n\t\treturn textproto.Header{}, err\n\t}\n\tif err := writeHeader(utf8, partWriter, failedHeader); err != nil {\n\t\treturn textproto.Header{}, err\n\t}\n\treturn reportHeader, partWriter.Close()\n}\n\nfunc writeHeader(utf8 bool, w *textproto.MultipartWriter, header textproto.Header) error {\n\tpartHeader := textproto.Header{}\n\tpartHeader.Add(\"Content-Description\", \"Undelivered message header\")\n\tif utf8 {\n\t\tpartHeader.Add(\"Content-Type\", \"message/global-headers\")\n\t} else {\n\t\tpartHeader.Add(\"Content-Type\", \"message/rfc822-headers\")\n\t}\n\tpartHeader.Add(\"Content-Transfer-Encoding\", \"8bit\")\n\theaderWriter, err := w.CreatePart(partHeader)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn textproto.WriteHeader(headerWriter, header)\n}\n\nfunc writeMachineReadablePart(utf8 bool, w *textproto.MultipartWriter, mtaInfo ReportingMTAInfo, rcptsInfo []RecipientInfo) error {\n\tmachineHeader := textproto.Header{}\n\tif utf8 {\n\t\tmachineHeader.Add(\"Content-Type\", \"message/global-delivery-status\")\n\t} else {\n\t\tmachineHeader.Add(\"Content-Type\", \"message/delivery-status\")\n\t}\n\tmachineHeader.Add(\"Content-Description\", \"Delivery report\")\n\tmachineWriter, err := w.CreatePart(machineHeader)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// WriteTo will add an empty line after output.\n\tif err := mtaInfo.WriteTo(utf8, machineWriter); err != nil {\n\t\treturn err\n\t}\n\n\tfor _, rcpt := range rcptsInfo {\n\t\tif err := rcpt.WriteTo(utf8, machineWriter); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\n// failedText is the text of the human-readable part of DSN.\nvar failedText = template.Must(template.New(\"dsn-text\").Parse(`\nThis is the mail delivery system at {{.ReportingMTA}}.\n\nUnfortunately, your message could not be delivered to one or more\nrecipients. The usual cause of this problem is invalid\nrecipient address or maintenance at the recipient side.\n\nContact the postmaster for further assistance, provide the Message ID (below):\n\nMessage ID: {{.XMessageID}}\nArrival: {{.ArrivalDate}}\nLast delivery attempt: {{.LastAttemptDate}}\n\n`))\n\nfunc writeHumanReadablePart(w *textproto.MultipartWriter, mtaInfo ReportingMTAInfo, rcptsInfo []RecipientInfo) error {\n\thumanHeader := textproto.Header{}\n\thumanHeader.Add(\"Content-Transfer-Encoding\", \"8bit\")\n\thumanHeader.Add(\"Content-Type\", `text/plain; charset=\"utf-8\"`)\n\thumanHeader.Add(\"Content-Description\", \"Notification\")\n\thumanWriter, err := w.CreatePart(humanHeader)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tmtaInfo.ArrivalDate = mtaInfo.ArrivalDate.Truncate(time.Second)\n\tmtaInfo.LastAttemptDate = mtaInfo.LastAttemptDate.Truncate(time.Second)\n\n\tif err := failedText.Execute(humanWriter, mtaInfo); err != nil {\n\t\treturn err\n\t}\n\n\tfor _, rcpt := range rcptsInfo {\n\t\tif _, err := fmt.Fprintf(humanWriter, \"Delivery to %s failed with error: %v\\n\", rcpt.FinalRecipient, rcpt.DiagnosticCode); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/endpoint/dovecot_sasld/dovecot_sasl.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage dovecotsasld\n\nimport (\n\t\"fmt\"\n\tstdlog \"log\"\n\t\"net\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/emersion/go-sasl\"\n\tdovecotsasl \"github.com/foxcpp/go-dovecot-sasl\"\n\t\"github.com/foxcpp/maddy/framework/config\"\n\tmodconfig \"github.com/foxcpp/maddy/framework/config/module\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/log\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n\t\"github.com/foxcpp/maddy/framework/resource/netresource\"\n\t\"github.com/foxcpp/maddy/internal/auth\"\n\t\"github.com/foxcpp/maddy/internal/authz\"\n)\n\nconst modName = \"dovecot_sasld\"\n\ntype Endpoint struct {\n\taddrs    []string\n\tlog      *log.Logger\n\tsaslAuth auth.SASLAuth\n\n\tendpoints   []config.Endpoint\n\tlistenersWg sync.WaitGroup\n\n\tsrv *dovecotsasl.Server\n}\n\nfunc New(c *container.C, _ string, addrs []string) (container.LifetimeModule, error) {\n\tlogger := c.DefaultLogger.Sublogger(modName)\n\treturn &Endpoint{\n\t\taddrs: addrs,\n\t\tsaslAuth: auth.SASLAuth{\n\t\t\tLog: logger.Sublogger(\"sasl\"),\n\t\t},\n\t\tlog: logger,\n\t}, nil\n}\n\nfunc (endp *Endpoint) Name() string {\n\treturn modName\n}\n\nfunc (endp *Endpoint) InstanceName() string {\n\treturn modName\n}\n\nfunc (endp *Endpoint) Configure(_ []string, cfg *config.Map) error {\n\tcfg.Callback(\"auth\", func(m *config.Map, node config.Node) error {\n\t\treturn endp.saslAuth.AddProvider(m, node)\n\t})\n\tcfg.Bool(\"sasl_login\", false, false, &endp.saslAuth.EnableLogin)\n\tconfig.EnumMapped(cfg, \"auth_map_normalize\", true, false, authz.NormalizeFuncs, authz.NormalizeAuto,\n\t\t&endp.saslAuth.AuthNormalize)\n\tmodconfig.Table(cfg, \"auth_map\", true, false, nil, &endp.saslAuth.AuthMap)\n\tif _, err := cfg.Process(); err != nil {\n\t\treturn err\n\t}\n\n\tendp.srv = dovecotsasl.NewServer()\n\tendp.saslAuth.Log.Debug = endp.log.Debug\n\tendp.srv.Log = stdlog.New(endp.log, \"\", 0)\n\n\tfor _, mech := range endp.saslAuth.SASLMechanisms() {\n\t\tendp.srv.AddMechanism(mech, mechInfo[mech], func(req *dovecotsasl.AuthReq) sasl.Server {\n\t\t\tvar remoteAddr net.Addr\n\t\t\tif req.RemoteIP != nil && req.RemotePort != 0 {\n\t\t\t\tremoteAddr = &net.TCPAddr{IP: req.RemoteIP, Port: int(req.RemotePort)}\n\t\t\t}\n\n\t\t\treturn endp.saslAuth.CreateSASL(mech, remoteAddr, func(_ string, _ auth.ContextData) error { return nil })\n\t\t})\n\t}\n\n\tfor _, addr := range endp.addrs {\n\t\tparsed, err := config.ParseEndpoint(addr)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"%s: %v\", modName, err)\n\t\t}\n\n\t\tendp.endpoints = append(endp.endpoints, parsed)\n\t}\n\n\treturn nil\n}\n\nfunc (endp *Endpoint) Start() error {\n\tfor _, addr := range endp.endpoints {\n\t\tl, err := netresource.Listen(addr.Network(), addr.Address())\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"%s: %v\", modName, err)\n\t\t}\n\n\t\tendp.log.Printf(\"listening on %v\", l.Addr())\n\t\tendp.listenersWg.Add(1)\n\t\tgo func() {\n\t\t\tdefer endp.listenersWg.Done()\n\t\t\tif err := endp.srv.Serve(l); err != nil {\n\t\t\t\tif !strings.HasSuffix(err.Error(), \"use of closed network connection\") {\n\t\t\t\t\tendp.log.Printf(\"failed to serve %v: %v\", l.Addr(), err)\n\t\t\t\t}\n\t\t\t}\n\t\t}()\n\t}\n\treturn nil\n}\n\nfunc (endp *Endpoint) Stop() error {\n\tdefer endp.listenersWg.Wait()\n\treturn endp.srv.Close()\n}\n\nfunc init() {\n\tmodules.RegisterEndpoint(modName, New)\n}\n"
  },
  {
    "path": "internal/endpoint/dovecot_sasld/mech_info.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage dovecotsasld\n\nimport (\n\t\"github.com/emersion/go-sasl\"\n\tdovecotsasl \"github.com/foxcpp/go-dovecot-sasl\"\n)\n\nvar mechInfo = map[string]dovecotsasl.Mechanism{\n\tsasl.Plain: {\n\t\tPlaintext: true,\n\t},\n\tsasl.Login: {\n\t\tPlaintext: true,\n\t},\n}\n"
  },
  {
    "path": "internal/endpoint/imap/imap.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage imap\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/emersion/go-imap\"\n\tcompress \"github.com/emersion/go-imap-compress\"\n\tsortthread \"github.com/emersion/go-imap-sortthread\"\n\timapbackend \"github.com/emersion/go-imap/backend\"\n\timapserver \"github.com/emersion/go-imap/server\"\n\t\"github.com/emersion/go-message\"\n\t_ \"github.com/emersion/go-message/charset\"\n\t\"github.com/emersion/go-sasl\"\n\ti18nlevel \"github.com/foxcpp/go-imap-i18nlevel\"\n\tnamespace \"github.com/foxcpp/go-imap-namespace\"\n\t\"github.com/foxcpp/maddy/framework/config\"\n\tmodconfig \"github.com/foxcpp/maddy/framework/config/module\"\n\ttls2 \"github.com/foxcpp/maddy/framework/config/tls\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/log\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n\t\"github.com/foxcpp/maddy/framework/resource/netresource\"\n\t\"github.com/foxcpp/maddy/internal/auth\"\n\t\"github.com/foxcpp/maddy/internal/authz\"\n\t\"github.com/foxcpp/maddy/internal/proxy_protocol\"\n\t\"github.com/foxcpp/maddy/internal/updatepipe\"\n)\n\ntype Endpoint struct {\n\taddrs         []string\n\tserv          *imapserver.Server\n\tproxyProtocol *proxy_protocol.ProxyProtocol\n\tStore         module.Storage\n\ttlsConfig     *tls.Config\n\n\tendpoints   []config.Endpoint\n\tlisteners   []net.Listener\n\tlistenersWg sync.WaitGroup\n\n\tsaslAuth auth.SASLAuth\n\n\tstorageNormalize authz.NormalizeFunc\n\tstorageMap       module.Table\n\n\tlog *log.Logger\n}\n\nfunc New(c *container.C, modName string, addrs []string) (container.LifetimeModule, error) {\n\tlogger := c.DefaultLogger.Sublogger(modName)\n\tendp := &Endpoint{\n\t\taddrs: addrs,\n\t\tlog:   logger,\n\t\tsaslAuth: auth.SASLAuth{\n\t\t\tLog: logger.Sublogger(\"sasl\"),\n\t\t},\n\t}\n\n\treturn endp, nil\n}\n\nfunc (endp *Endpoint) Configure(_ []string, cfg *config.Map) error {\n\tvar (\n\t\tinsecureAuth bool\n\t\tioDebug      bool\n\t\tioErrors     bool\n\t)\n\n\tcfg.Callback(\"auth\", func(m *config.Map, node config.Node) error {\n\t\treturn endp.saslAuth.AddProvider(m, node)\n\t})\n\tcfg.Bool(\"sasl_login\", false, false, &endp.saslAuth.EnableLogin)\n\tcfg.Custom(\"storage\", false, true, nil, modconfig.StorageDirective, &endp.Store)\n\tcfg.Custom(\"tls\", true, true, nil, tls2.TLSDirective, &endp.tlsConfig)\n\tcfg.Custom(\"proxy_protocol\", false, false, nil, proxy_protocol.ProxyProtocolDirective, &endp.proxyProtocol)\n\tcfg.Bool(\"insecure_auth\", false, false, &insecureAuth)\n\tcfg.Bool(\"io_debug\", false, false, &ioDebug)\n\tcfg.Bool(\"io_errors\", false, false, &ioErrors)\n\tcfg.Bool(\"debug\", true, false, &endp.log.Debug)\n\tconfig.EnumMapped(cfg, \"storage_map_normalize\", false, false, authz.NormalizeFuncs, authz.NormalizeAuto,\n\t\t&endp.storageNormalize)\n\tmodconfig.Table(cfg, \"storage_map\", false, false, nil, &endp.storageMap)\n\tconfig.EnumMapped(cfg, \"auth_map_normalize\", true, false, authz.NormalizeFuncs, authz.NormalizeAuto,\n\t\t&endp.saslAuth.AuthNormalize)\n\tmodconfig.Table(cfg, \"auth_map\", true, false, nil, &endp.saslAuth.AuthMap)\n\tif _, err := cfg.Process(); err != nil {\n\t\treturn err\n\t}\n\n\tendp.saslAuth.Log.Debug = endp.log.Debug\n\n\taddresses := make([]config.Endpoint, 0, len(endp.addrs))\n\tfor _, addr := range endp.addrs {\n\t\tsaddr, err := config.ParseEndpoint(addr)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"imap: invalid address: %s\", addr)\n\t\t}\n\t\tif saddr.IsTLS() && endp.tlsConfig == nil {\n\t\t\treturn errors.New(\"imap: can't bind on IMAPS endpoint without TLS configuration\")\n\t\t}\n\t\taddresses = append(addresses, saddr)\n\t}\n\tendp.endpoints = addresses\n\n\tendp.serv = imapserver.New(endp)\n\tendp.serv.AllowInsecureAuth = insecureAuth\n\tendp.serv.TLSConfig = endp.tlsConfig\n\tif ioErrors {\n\t\tendp.serv.ErrorLog = endp.log\n\t} else {\n\t\tendp.serv.ErrorLog = &log.NopLogger\n\t}\n\tif ioDebug {\n\t\tendp.serv.Debug = endp.log.DebugWriter()\n\t\tendp.log.Println(\"I/O debugging is on! It may leak passwords in logs, be careful!\")\n\t}\n\n\tif err := endp.enableExtensions(); err != nil {\n\t\treturn err\n\t}\n\n\tfor _, mech := range endp.saslAuth.SASLMechanisms() {\n\t\tendp.serv.EnableAuth(mech, func(c imapserver.Conn) sasl.Server {\n\t\t\treturn endp.saslAuth.CreateSASL(mech, c.Info().RemoteAddr, func(identity string, data auth.ContextData) error {\n\t\t\t\treturn endp.openAccount(c, identity)\n\t\t\t})\n\t\t})\n\t}\n\n\tif endp.serv.AllowInsecureAuth {\n\t\tendp.log.Println(\"authentication over unencrypted connections is allowed, this is insecure configuration and should be used only for testing!\")\n\t}\n\tif endp.serv.TLSConfig == nil {\n\t\tendp.log.Println(\"TLS is disabled, this is insecure configuration and should be used only for testing!\")\n\t\tendp.serv.AllowInsecureAuth = true\n\t}\n\n\treturn nil\n}\n\nfunc (endp *Endpoint) Start() error {\n\tif updBe, ok := endp.Store.(updatepipe.Backend); ok {\n\t\tif err := updBe.EnableUpdatePipe(updatepipe.ModeReplicate); err != nil {\n\t\t\tendp.log.Error(\"failed to initialize updates pipe\", err)\n\t\t}\n\t}\n\n\tif err := endp.setupListeners(endp.endpoints); err != nil {\n\t\tif err := endp.Stop(); err != nil {\n\t\t\tendp.log.Error(\"failed to stop after setupListeners error\", err)\n\t\t}\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (endp *Endpoint) setupListeners(addresses []config.Endpoint) error {\n\tfor _, addr := range addresses {\n\t\tvar l net.Listener\n\t\tvar err error\n\t\tl, err = netresource.Listen(addr.Network(), addr.Address())\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"imap: %v\", err)\n\t\t}\n\t\tendp.log.Printf(\"listening on %v\", addr)\n\n\t\tif addr.IsTLS() {\n\t\t\tif endp.tlsConfig == nil {\n\t\t\t\treturn errors.New(\"imap: can't bind on IMAPS endpoint without TLS configuration\")\n\t\t\t}\n\t\t\tl = tls.NewListener(l, endp.tlsConfig)\n\t\t}\n\n\t\tif endp.proxyProtocol != nil {\n\t\t\tl = proxy_protocol.NewListener(l, endp.proxyProtocol, endp.log)\n\t\t}\n\n\t\tendp.listeners = append(endp.listeners, l)\n\t\tendp.listenersWg.Add(1)\n\t\tgo func() {\n\t\t\tdefer endp.listenersWg.Done()\n\t\t\tif err := endp.serv.Serve(l); err != nil && !strings.HasSuffix(err.Error(), \"use of closed network connection\") {\n\t\t\t\tendp.log.Printf(\"imap: failed to serve %s: %s\", addr, err)\n\t\t\t}\n\t\t}()\n\t}\n\n\treturn nil\n}\n\nfunc (endp *Endpoint) Name() string {\n\treturn \"imap\"\n}\n\nfunc (endp *Endpoint) InstanceName() string {\n\treturn \"imap\"\n}\n\nfunc (endp *Endpoint) Stop() error {\n\tfor _, l := range endp.listeners {\n\t\tif err := l.Close(); err != nil {\n\t\t\tendp.log.Error(\"failed to close listener\", err)\n\t\t}\n\t}\n\tif err := endp.serv.Close(); err != nil {\n\t\treturn err\n\t}\n\tendp.listenersWg.Wait()\n\treturn nil\n}\n\nfunc (endp *Endpoint) usernameForStorage(ctx context.Context, saslUsername string) (string, error) {\n\tsaslUsername, err := endp.storageNormalize(saslUsername)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif endp.storageMap == nil {\n\t\treturn saslUsername, nil\n\t}\n\n\tmapped, ok, err := endp.storageMap.Lookup(ctx, saslUsername)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif !ok {\n\t\treturn \"\", imapbackend.ErrInvalidCredentials\n\t}\n\n\tif saslUsername != mapped {\n\t\tendp.log.DebugMsg(\"using mapped username for storage\", \"username\", saslUsername, \"mapped_username\", mapped)\n\t}\n\n\treturn mapped, nil\n}\n\nfunc (endp *Endpoint) openAccount(c imapserver.Conn, identity string) error {\n\tusername, err := endp.usernameForStorage(context.TODO(), identity)\n\tif err != nil {\n\t\tif errors.Is(err, imapbackend.ErrInvalidCredentials) {\n\t\t\treturn err\n\t\t}\n\t\tendp.log.Error(\"failed to determine storage account name\", err, \"username\", username)\n\t\treturn fmt.Errorf(\"internal server error\")\n\t}\n\n\tu, err := endp.Store.GetOrCreateIMAPAcct(username)\n\tif err != nil {\n\t\treturn err\n\t}\n\tctx := c.Context()\n\tctx.State = imap.AuthenticatedState\n\tctx.User = u\n\treturn nil\n}\n\nfunc (endp *Endpoint) Login(connInfo *imap.ConnInfo, username, password string) (imapbackend.User, error) {\n\t// saslAuth handles AuthMap calling.\n\terr := endp.saslAuth.AuthPlain(username, password)\n\tif err != nil {\n\t\tendp.log.Error(\"authentication failed\", err, \"username\", username, \"src_ip\", connInfo.RemoteAddr)\n\t\treturn nil, imapbackend.ErrInvalidCredentials\n\t}\n\n\tstorageUsername, err := endp.usernameForStorage(context.TODO(), username)\n\tif err != nil {\n\t\tif errors.Is(err, imapbackend.ErrInvalidCredentials) {\n\t\t\treturn nil, err\n\t\t}\n\t\tendp.log.Error(\"authentication failed due to an internal error\", err, \"username\", username, \"src_ip\", connInfo.RemoteAddr)\n\t\treturn nil, fmt.Errorf(\"internal server error\")\n\t}\n\n\treturn endp.Store.GetOrCreateIMAPAcct(storageUsername)\n}\n\nfunc (endp *Endpoint) I18NLevel() int {\n\tbe, ok := endp.Store.(i18nlevel.Backend)\n\tif !ok {\n\t\treturn 0\n\t}\n\treturn be.I18NLevel()\n}\n\nfunc (endp *Endpoint) enableExtensions() error {\n\texts := endp.Store.IMAPExtensions()\n\tfor _, ext := range exts {\n\t\tswitch ext {\n\t\tcase \"I18NLEVEL=1\", \"I18NLEVEL=2\":\n\t\t\tendp.serv.Enable(i18nlevel.NewExtension())\n\t\tcase \"SORT\":\n\t\t\tendp.serv.Enable(sortthread.NewSortExtension())\n\t\t}\n\t\tif strings.HasPrefix(ext, \"THREAD\") {\n\t\t\tendp.serv.Enable(sortthread.NewThreadExtension())\n\t\t}\n\t}\n\n\tendp.serv.Enable(compress.NewExtension())\n\tendp.serv.Enable(namespace.NewExtension())\n\n\treturn nil\n}\n\nfunc (endp *Endpoint) SupportedThreadAlgorithms() []sortthread.ThreadAlgorithm {\n\tbe, ok := endp.Store.(sortthread.ThreadBackend)\n\tif !ok {\n\t\treturn nil\n\t}\n\n\treturn be.SupportedThreadAlgorithms()\n}\n\nfunc init() {\n\tmodules.RegisterEndpoint(\"imap\", New)\n\n\timap.CharsetReader = message.CharsetReader\n}\n"
  },
  {
    "path": "internal/endpoint/openmetrics/om.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage openmetrics\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"sync\"\n\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/log\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n\t\"github.com/foxcpp/maddy/framework/resource/netresource\"\n\t\"github.com/prometheus/client_golang/prometheus/promhttp\"\n)\n\nconst modName = \"openmetrics\"\n\ntype Endpoint struct {\n\taddrs     []string\n\tendpoints []config.Endpoint\n\tlogger    *log.Logger\n\n\tlistenersWg sync.WaitGroup\n\tserv        http.Server\n\tmux         *http.ServeMux\n}\n\nfunc New(c *container.C, _ string, args []string) (container.LifetimeModule, error) {\n\treturn &Endpoint{\n\t\taddrs:  args,\n\t\tlogger: c.DefaultLogger.Sublogger(modName),\n\t}, nil\n}\n\nfunc (e *Endpoint) Configure(inlineArgs []string, cfg *config.Map) error {\n\tcfg.Bool(\"debug\", false, false, &e.logger.Debug)\n\tif _, err := cfg.Process(); err != nil {\n\t\treturn err\n\t}\n\n\te.mux = http.NewServeMux()\n\te.mux.Handle(\"/metrics\", promhttp.Handler())\n\te.serv.Handler = e.mux\n\n\tfor _, a := range e.addrs {\n\t\tendp, err := config.ParseEndpoint(a)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"%s: malformed endpoint: %v\", modName, err)\n\t\t}\n\t\tif endp.IsTLS() {\n\t\t\treturn fmt.Errorf(\"%s: TLS is not supported yet\", modName)\n\t\t}\n\t\te.endpoints = append(e.endpoints, endp)\n\t}\n\n\treturn nil\n}\n\nfunc (e *Endpoint) Name() string {\n\treturn modName\n}\n\nfunc (e *Endpoint) InstanceName() string {\n\treturn \"\"\n}\n\nfunc (e *Endpoint) Start() error {\n\tfor _, endp := range e.endpoints {\n\t\tl, err := netresource.Listen(endp.Network(), endp.Address())\n\t\tif err != nil {\n\t\t\tif err := e.Stop(); err != nil {\n\t\t\t\te.logger.Error(\"failed to stop after failed listen\", err)\n\t\t\t}\n\t\t\treturn fmt.Errorf(\"%s: %v\", modName, err)\n\t\t}\n\n\t\te.listenersWg.Add(1)\n\t\tgo func() {\n\t\t\te.logger.Println(\"listening on\", endp.String())\n\t\t\terr := e.serv.Serve(l)\n\t\t\tif err != nil && !errors.Is(err, http.ErrServerClosed) {\n\t\t\t\te.logger.Error(\"serve failed\", err, \"endpoint\", endp)\n\t\t\t}\n\t\t\te.listenersWg.Done()\n\t\t}()\n\t}\n\treturn nil\n}\n\nfunc (e *Endpoint) Stop() error {\n\tif err := e.serv.Close(); err != nil {\n\t\treturn err\n\t}\n\te.listenersWg.Wait()\n\treturn nil\n}\n\nfunc init() {\n\tmodules.RegisterEndpoint(modName, New)\n}\n"
  },
  {
    "path": "internal/endpoint/smtp/date.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage smtp\n\nimport (\n\t\"fmt\"\n\t\"regexp\"\n\t\"time\"\n)\n\n// Taken from https://github.com/emersion/go-imap/blob/09c1d69/date.go.\n\nvar dateTimeLayouts = [...]string{\n\t// Defined in RFC 5322 section 3.3, mentioned as env-date in RFC 3501 page 84.\n\t\"Mon, 02 Jan 2006 15:04:05 -0700\",\n\t\"_2 Jan 2006 15:04:05 -0700\",\n\t\"_2 Jan 2006 15:04:05 MST\",\n\t\"_2 Jan 2006 15:04 -0700\",\n\t\"_2 Jan 2006 15:04 MST\",\n\t\"_2 Jan 06 15:04:05 -0700\",\n\t\"_2 Jan 06 15:04:05 MST\",\n\t\"_2 Jan 06 15:04 -0700\",\n\t\"_2 Jan 06 15:04 MST\",\n\t\"Mon, _2 Jan 2006 15:04:05 -0700\",\n\t\"Mon, _2 Jan 2006 15:04:05 MST\",\n\t\"Mon, _2 Jan 2006 15:04 -0700\",\n\t\"Mon, _2 Jan 2006 15:04 MST\",\n\t\"Mon, _2 Jan 06 15:04:05 -0700\",\n\t\"Mon, _2 Jan 06 15:04:05 MST\",\n\t\"Mon, _2 Jan 06 15:04 -0700\",\n\t\"Mon, _2 Jan 06 15:04 MST\",\n}\n\n// TODO: this is a blunt way to strip any trailing CFWS (comment). A sharper\n// one would strip multiple CFWS, and only if really valid according to\n// RFC5322.\nvar commentRE = regexp.MustCompile(`[ \\t]+\\(.*\\)$`)\n\n// Try parsing the date based on the layouts defined in RFC 5322, section 3.3.\n// Inspired by https://github.com/golang/go/blob/master/src/net/mail/message.go\nfunc parseMessageDateTime(maybeDate string) (time.Time, error) {\n\tmaybeDate = commentRE.ReplaceAllString(maybeDate, \"\")\n\tfor _, layout := range dateTimeLayouts {\n\t\tparsed, err := time.Parse(layout, maybeDate)\n\t\tif err == nil {\n\t\t\treturn parsed, nil\n\t\t}\n\t}\n\treturn time.Time{}, fmt.Errorf(\"date %s could not be parsed\", maybeDate)\n}\n"
  },
  {
    "path": "internal/endpoint/smtp/metrics.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage smtp\n\nimport \"github.com/prometheus/client_golang/prometheus\"\n\nvar (\n\tstartedSMTPTransactions = prometheus.NewCounterVec(\n\t\tprometheus.CounterOpts{\n\t\t\tNamespace: \"maddy\",\n\t\t\tSubsystem: \"smtp\",\n\t\t\tName:      \"started_transactions\",\n\t\t\tHelp:      \"Amount of SMTP transactions started\",\n\t\t},\n\t\t[]string{\"module\"},\n\t)\n\tcompletedSMTPTransactions = prometheus.NewCounterVec(\n\t\tprometheus.CounterOpts{\n\t\t\tNamespace: \"maddy\",\n\t\t\tSubsystem: \"smtp\",\n\t\t\tName:      \"smtp_completed_transactions\",\n\t\t\tHelp:      \"Amount of SMTP transactions successfully completed\",\n\t\t},\n\t\t[]string{\"module\"},\n\t)\n\tabortedSMTPTransactions = prometheus.NewCounterVec(\n\t\tprometheus.CounterOpts{\n\t\t\tNamespace: \"maddy\",\n\t\t\tSubsystem: \"smtp\",\n\t\t\tName:      \"aborted_transactions\",\n\t\t\tHelp:      \"Amount of SMTP transactions aborted\",\n\t\t},\n\t\t[]string{\"module\"},\n\t)\n\n\tratelimitDefers = prometheus.NewCounterVec(\n\t\tprometheus.CounterOpts{\n\t\t\tNamespace: \"maddy\",\n\t\t\tSubsystem: \"smtp\",\n\t\t\tName:      \"ratelimit_deferred\",\n\t\t\tHelp:      \"Messages rejected with 4xx code due to ratelimiting\",\n\t\t},\n\t\t[]string{\"module\"},\n\t)\n\tfailedLogins = prometheus.NewCounterVec(\n\t\tprometheus.CounterOpts{\n\t\t\tNamespace: \"maddy\",\n\t\t\tSubsystem: \"smtp\",\n\t\t\tName:      \"failed_logins\",\n\t\t\tHelp:      \"AUTH command failures\",\n\t\t},\n\t\t[]string{\"module\"},\n\t)\n\tfailedCmds = prometheus.NewCounterVec(\n\t\tprometheus.CounterOpts{\n\t\t\tNamespace: \"maddy\",\n\t\t\tSubsystem: \"smtp\",\n\t\t\tName:      \"failed_commands\",\n\t\t\tHelp:      \"Failed transaction commands (MAIL, RCPT, DATA)\",\n\t\t},\n\t\t[]string{\"module\", \"command\", \"smtp_code\", \"smtp_enchcode\"},\n\t)\n)\n\nfunc init() {\n\tprometheus.MustRegister(startedSMTPTransactions)\n\tprometheus.MustRegister(completedSMTPTransactions)\n\tprometheus.MustRegister(abortedSMTPTransactions)\n\tprometheus.MustRegister(ratelimitDefers)\n\tprometheus.MustRegister(failedCmds)\n}\n"
  },
  {
    "path": "internal/endpoint/smtp/session.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage smtp\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"runtime/trace\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/emersion/go-message/textproto\"\n\t\"github.com/emersion/go-sasl\"\n\t\"github.com/emersion/go-smtp\"\n\t\"github.com/foxcpp/maddy/framework/address\"\n\t\"github.com/foxcpp/maddy/framework/buffer\"\n\t\"github.com/foxcpp/maddy/framework/dns\"\n\t\"github.com/foxcpp/maddy/framework/exterrors\"\n\t\"github.com/foxcpp/maddy/framework/log\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/internal/auth\"\n)\n\nfunc limitReader(r io.Reader, n int64, err error) *limitedReader {\n\treturn &limitedReader{R: r, N: n, E: err, Enabled: true}\n}\n\ntype limitedReader struct {\n\tR       io.Reader\n\tN       int64\n\tE       error\n\tEnabled bool\n}\n\n// same as io.LimitedReader.Read except returning the custom error and the option\n// to be disabled\nfunc (l *limitedReader) Read(p []byte) (n int, err error) {\n\tif !l.Enabled {\n\t\treturn l.R.Read(p)\n\t}\n\tif l.N <= 0 {\n\t\treturn 0, l.E\n\t}\n\tif int64(len(p)) > l.N {\n\t\tp = p[0:l.N]\n\t}\n\tn, err = l.R.Read(p)\n\tl.N -= int64(n)\n\treturn\n}\n\ntype Session struct {\n\tendp *Endpoint\n\n\t// Specific for this session.\n\t// sessionCtx is not used for cancellation or timeouts, only for tracing.\n\tsessionCtx       context.Context\n\tcancelRDNS       func()\n\tconnState        module.ConnState\n\trepeatedMailErrs int\n\tloggedRcptErrors int\n\n\t// Specific for the currently handled message.\n\t// msgCtx is not used for cancellation or timeouts, only for tracing.\n\t// It is the subcontext of sessionCtx.\n\t// Mutex is used to prevent Close from accessing inconsistent state when it\n\t// is called asynchronously to any SMTP command.\n\tmsgLock     sync.Mutex\n\tmsgCtx      context.Context\n\tmsgTask     *trace.Task\n\tmailFrom    string\n\topts        smtp.MailOptions\n\tmsgMeta     *module.MsgMetadata\n\tdelivery    module.Delivery\n\tdeliveryErr error\n\n\tlog *log.Logger\n}\n\nfunc (s *Session) AuthMechanisms() []string {\n\treturn s.endp.saslAuth.SASLMechanisms()\n}\n\nfunc (s *Session) Auth(mech string) (sasl.Server, error) {\n\treturn s.endp.saslAuth.CreateSASL(mech, s.connState.RemoteAddr, func(identity string, data auth.ContextData) error {\n\t\ts.connState.AuthUser = identity\n\t\ts.connState.AuthPassword = data.Password\n\t\treturn nil\n\t}), nil\n}\n\nfunc (s *Session) Reset() {\n\ts.msgLock.Lock()\n\tdefer s.msgLock.Unlock()\n\n\tif s.delivery != nil {\n\t\ts.abort(s.msgCtx)\n\t}\n\ts.endp.log.DebugMsg(\"reset\")\n}\n\nfunc (s *Session) releaseLimits() {\n\tdomain := \"\"\n\tif s.mailFrom != \"\" {\n\t\tvar err error\n\t\t_, domain, err = address.Split(s.mailFrom)\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\t}\n\n\taddr, ok := s.msgMeta.Conn.RemoteAddr.(*net.TCPAddr)\n\tif !ok {\n\t\taddr = &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1)}\n\t}\n\ts.endp.limits.ReleaseMsg(addr.IP, domain)\n}\n\nfunc (s *Session) abort(ctx context.Context) {\n\tif err := s.delivery.Abort(ctx); err != nil {\n\t\ts.endp.log.Error(\"delivery abort failed\", err)\n\t}\n\ts.log.Msg(\"aborted\", \"msg_id\", s.msgMeta.ID)\n\tabortedSMTPTransactions.WithLabelValues(s.endp.name).Inc()\n\ts.cleanSession()\n}\n\nfunc (s *Session) cleanSession() {\n\ts.releaseLimits()\n\n\ts.mailFrom = \"\"\n\ts.opts = smtp.MailOptions{}\n\ts.msgMeta = nil\n\ts.delivery = nil\n\ts.deliveryErr = nil\n\ts.msgCtx = nil\n\ts.msgTask.End()\n}\n\nfunc (s *Session) AuthPlain(username, password string) error {\n\t// Executed before authentication and session initialization.\n\tif err := s.endp.pipeline.RunEarlyChecks(context.TODO(), &s.connState); err != nil {\n\t\treturn s.endp.wrapErr(\"\", true, \"AUTH\", err)\n\t}\n\n\t// saslAuth will handle AuthMap and AuthNormalize.\n\terr := s.endp.saslAuth.AuthPlain(username, password)\n\tif err != nil {\n\t\ts.endp.log.Error(\"authentication failed\", err, \"username\", username, \"src_ip\", s.connState.RemoteAddr)\n\n\t\tfailedLogins.WithLabelValues(s.endp.name).Inc()\n\n\t\treturn s.endp.authErrorMap(err)\n\t}\n\n\ts.connState.AuthUser = username\n\ts.connState.AuthPassword = password\n\n\treturn nil\n}\n\nfunc (s *Session) startDelivery(ctx context.Context, from string, opts smtp.MailOptions) (string, error) {\n\tvar err error\n\tmsgMeta := &module.MsgMetadata{\n\t\tConn:     &s.connState,\n\t\tSMTPOpts: opts,\n\t}\n\tmsgMeta.ID, err = module.GenerateMsgID()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif s.connState.AuthUser != \"\" {\n\t\ts.log.Msg(\"incoming message\",\n\t\t\t\"src_host\", msgMeta.Conn.Hostname,\n\t\t\t\"src_ip\", msgMeta.Conn.RemoteAddr.String(),\n\t\t\t\"sender\", from,\n\t\t\t\"msg_id\", msgMeta.ID,\n\t\t\t\"username\", s.connState.AuthUser,\n\t\t)\n\t} else {\n\t\ts.log.Msg(\"incoming message\",\n\t\t\t\"src_host\", msgMeta.Conn.Hostname,\n\t\t\t\"src_ip\", msgMeta.Conn.RemoteAddr.String(),\n\t\t\t\"sender\", from,\n\t\t\t\"msg_id\", msgMeta.ID,\n\t\t)\n\t}\n\n\t// INTERNATIONALIZATION: Do not permit non-ASCII addresses unless SMTPUTF8 is\n\t// used.\n\tif !opts.UTF8 {\n\t\tfor _, ch := range from {\n\t\t\tif ch > 128 {\n\t\t\t\treturn \"\", &exterrors.SMTPError{\n\t\t\t\t\tCode:         550,\n\t\t\t\t\tEnhancedCode: exterrors.EnhancedCode{5, 6, 7},\n\t\t\t\t\tMessage:      \"SMTPUTF8 is required for non-ASCII senders\",\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Decode punycode, normalize to NFC and case-fold address.\n\tcleanFrom := from\n\tif from != \"\" {\n\t\tcleanFrom, err = address.CleanDomain(from)\n\t\tif err != nil {\n\t\t\treturn \"\", &exterrors.SMTPError{\n\t\t\t\tCode:         553,\n\t\t\t\tEnhancedCode: exterrors.EnhancedCode{5, 1, 7},\n\t\t\t\tMessage:      \"Unable to normalize the sender address\",\n\t\t\t}\n\t\t}\n\t}\n\n\tmsgMeta.OriginalFrom = from\n\n\tdomain := \"\"\n\tif cleanFrom != \"\" {\n\t\t_, domain, err = address.Split(cleanFrom)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\tremoteIP, ok := msgMeta.Conn.RemoteAddr.(*net.TCPAddr)\n\tif !ok {\n\t\tremoteIP = &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1)}\n\t}\n\tif err := s.endp.limits.TakeMsg(context.Background(), remoteIP.IP, domain); err != nil {\n\t\treturn \"\", err\n\t}\n\n\ts.msgCtx, s.msgTask = trace.NewTask(ctx, \"Incoming Message\")\n\n\tmailCtx, mailTask := trace.NewTask(s.msgCtx, \"MAIL FROM\")\n\tdefer mailTask.End()\n\n\tdelivery, err := s.endp.pipeline.StartDelivery(mailCtx, msgMeta, cleanFrom)\n\tif err != nil {\n\t\ts.msgCtx = nil\n\t\ts.msgTask.End()\n\t\ts.endp.limits.ReleaseMsg(remoteIP.IP, domain)\n\t\treturn msgMeta.ID, err\n\t}\n\n\tstartedSMTPTransactions.WithLabelValues(s.endp.name).Inc()\n\n\ts.msgMeta = msgMeta\n\ts.mailFrom = cleanFrom\n\ts.delivery = delivery\n\n\treturn msgMeta.ID, nil\n}\n\nfunc (s *Session) Mail(from string, opts *smtp.MailOptions) error {\n\tif s.endp.authAlwaysRequired && s.connState.AuthUser == \"\" {\n\t\treturn smtp.ErrAuthRequired\n\t}\n\n\ts.msgLock.Lock()\n\tdefer s.msgLock.Unlock()\n\n\tif !s.endp.deferServerReject {\n\t\t// Will initialize s.msgCtx.\n\t\tmsgID, err := s.startDelivery(s.sessionCtx, from, *opts)\n\t\tif err != nil {\n\t\t\tif !errors.Is(err, context.DeadlineExceeded) {\n\t\t\t\ts.log.Error(\"MAIL FROM error\", err, \"msg_id\", msgID)\n\t\t\t}\n\t\t\treturn s.endp.wrapErr(msgID, !opts.UTF8, \"MAIL\", err)\n\t\t}\n\t}\n\n\t// Keep the MAIL FROM argument for deferred startDelivery.\n\ts.mailFrom = from\n\ts.opts = *opts\n\n\treturn nil\n}\n\nfunc (s *Session) fetchRDNSName(ctx context.Context) {\n\tdefer trace.StartRegion(ctx, \"rDNS fetch\").End()\n\n\ttcpAddr, ok := s.connState.RemoteAddr.(*net.TCPAddr)\n\tif !ok {\n\t\ts.connState.RDNSName.Set(nil, nil)\n\t\treturn\n\t}\n\n\tname, err := dns.LookupAddr(ctx, s.endp.resolver, tcpAddr.IP)\n\tif err != nil {\n\t\tdnsErr, ok := err.(*net.DNSError)\n\t\tif ok && dnsErr.IsNotFound {\n\t\t\ts.connState.RDNSName.Set(nil, nil)\n\t\t\treturn\n\t\t}\n\n\t\tif !errors.Is(err, context.Canceled) {\n\t\t\t// Often occurs when transaction completes before rDNS lookup and\n\t\t\t// rDNS name was not actually needed. So do not log cancelation\n\t\t\t// error if that's the case.\n\n\t\t\treason, misc := exterrors.UnwrapDNSErr(err)\n\t\t\tmisc[\"reason\"] = reason\n\t\t\ts.log.Error(\"rDNS error\", exterrors.WithFields(err, misc), \"src_ip\", s.connState.RemoteAddr)\n\t\t}\n\t\ts.connState.RDNSName.Set(nil, err)\n\t\treturn\n\t}\n\n\ts.connState.RDNSName.Set(name, nil)\n}\n\nfunc (s *Session) Rcpt(to string, opts *smtp.RcptOptions) error {\n\ts.msgLock.Lock()\n\tdefer s.msgLock.Unlock()\n\n\t// deferServerReject = true and this is the first RCPT TO command.\n\tif s.delivery == nil {\n\t\t// If we already attempted to initialize the delivery -\n\t\t// fail again.\n\t\tif s.deliveryErr != nil {\n\t\t\ts.repeatedMailErrs++\n\t\t\t// The deliveryErr is already wrapped.\n\t\t\treturn s.deliveryErr\n\t\t}\n\n\t\t// It will initialize s.msgCtx.\n\t\tmsgID, err := s.startDelivery(s.sessionCtx, s.mailFrom, s.opts)\n\t\tif err != nil {\n\t\t\tif !errors.Is(err, context.DeadlineExceeded) {\n\t\t\t\ts.log.Error(\"MAIL FROM error (deferred)\", err, \"rcpt\", to, \"msg_id\", msgID)\n\t\t\t}\n\t\t\ts.deliveryErr = s.endp.wrapErr(msgID, !s.opts.UTF8, \"RCPT\", err)\n\t\t\treturn s.deliveryErr\n\t\t}\n\t}\n\n\trcptCtx, rcptTask := trace.NewTask(s.msgCtx, \"RCPT TO\")\n\tdefer rcptTask.End()\n\n\tif err := s.rcpt(rcptCtx, to, opts); err != nil {\n\t\tif s.loggedRcptErrors < s.endp.maxLoggedRcptErrors {\n\t\t\ts.log.Error(\"RCPT error\", err, \"rcpt\", to, \"msg_id\", s.msgMeta.ID)\n\t\t\ts.loggedRcptErrors++\n\t\t\tif s.loggedRcptErrors == s.endp.maxLoggedRcptErrors {\n\t\t\t\ts.log.Msg(\"too many RCPT errors, possible dictonary attack\", \"src_ip\", s.connState.RemoteAddr, \"msg_id\", s.msgMeta.ID)\n\t\t\t}\n\t\t}\n\t\treturn s.endp.wrapErr(s.msgMeta.ID, !s.opts.UTF8, \"RCPT\", err)\n\t}\n\ts.endp.log.Msg(\"RCPT ok\", \"rcpt\", to, \"msg_id\", s.msgMeta.ID)\n\treturn nil\n}\n\nfunc (s *Session) rcpt(ctx context.Context, to string, opts *smtp.RcptOptions) error {\n\t// INTERNATIONALIZATION: Do not permit non-ASCII addresses unless SMTPUTF8 is\n\t// used.\n\tif !address.IsASCII(to) && !s.opts.UTF8 {\n\t\treturn &exterrors.SMTPError{\n\t\t\tCode:         553,\n\t\t\tEnhancedCode: exterrors.EnhancedCode{5, 6, 7},\n\t\t\tMessage:      \"SMTPUTF8 is required for non-ASCII recipients\",\n\t\t}\n\t}\n\tcleanTo, err := address.CleanDomain(to)\n\tif err != nil {\n\t\treturn &exterrors.SMTPError{\n\t\t\tCode:         501,\n\t\t\tEnhancedCode: exterrors.EnhancedCode{5, 1, 2},\n\t\t\tMessage:      \"Unable to normalize the recipient address\",\n\t\t}\n\t}\n\n\treturn s.delivery.AddRcpt(ctx, cleanTo, *opts)\n}\n\nfunc (s *Session) Logout() error {\n\ts.msgLock.Lock()\n\tdefer s.msgLock.Unlock()\n\n\tif s.delivery != nil {\n\t\ts.abort(s.msgCtx)\n\n\t\tif s.repeatedMailErrs > s.endp.maxLoggedRcptErrors {\n\t\t\ts.log.Msg(\"MAIL FROM repeated error a lot of times, possible dictonary attack\", \"count\", s.repeatedMailErrs, \"src_ip\", s.connState.RemoteAddr)\n\t\t}\n\t}\n\tif s.cancelRDNS != nil {\n\t\ts.cancelRDNS()\n\t}\n\n\ts.endp.sessionCnt.Add(-1)\n\n\treturn nil\n}\n\nfunc (s *Session) prepareBody(r io.Reader) (textproto.Header, buffer.Buffer, error) {\n\tlimitr := limitReader(r, s.endp.maxHeaderBytes, &exterrors.SMTPError{\n\t\tCode:         552,\n\t\tEnhancedCode: exterrors.EnhancedCode{5, 3, 4},\n\t\tMessage:      \"Message header size exceeds limit\",\n\t})\n\n\tbufr := bufio.NewReader(limitr)\n\theader, err := textproto.ReadHeader(bufr)\n\tif err != nil {\n\t\treturn textproto.Header{}, nil, fmt.Errorf(\"I/O error while parsing header: %w\", err)\n\t}\n\n\tif s.endp.submission {\n\t\t// The MsgMetadata is passed by pointer all the way down.\n\t\tif err := s.submissionPrepare(s.msgMeta, &header); err != nil {\n\t\t\treturn textproto.Header{}, nil, err\n\t\t}\n\t}\n\n\t// the header size check is done. The message size will be checked by go-smtp\n\tlimitr.Enabled = false\n\n\tbuf, err := s.endp.buffer(bufr)\n\tif err != nil {\n\t\treturn textproto.Header{}, nil, fmt.Errorf(\"I/O error while writing buffer: %w\", err)\n\t}\n\n\treturn header, buf, nil\n}\n\nfunc (s *Session) Data(r io.Reader) error {\n\ts.msgLock.Lock()\n\tdefer s.msgLock.Unlock()\n\n\tbodyCtx, bodyTask := trace.NewTask(s.msgCtx, \"DATA\")\n\tdefer bodyTask.End()\n\n\twrapErr := func(err error) error {\n\t\ts.log.Error(\"DATA error\", err, \"msg_id\", s.msgMeta.ID)\n\t\treturn s.endp.wrapErr(s.msgMeta.ID, !s.opts.UTF8, \"DATA\", err)\n\t}\n\n\theader, buf, err := s.prepareBody(r)\n\tif err != nil {\n\t\treturn wrapErr(err)\n\t}\n\tdefer func() {\n\t\tif err := buf.Remove(); err != nil {\n\t\t\ts.log.Error(\"failed to remove buffered body\", err)\n\t\t}\n\n\t\t// go-smtp will call Reset, but it will call Abort if delivery is non-nil.\n\t\ts.cleanSession()\n\t}()\n\n\tif err := s.checkRoutingLoops(header); err != nil {\n\t\treturn wrapErr(err)\n\t}\n\n\tif strings.EqualFold(header.Get(\"TLS-Required\"), \"No\") {\n\t\ts.msgMeta.TLSRequireOverride = true\n\t}\n\n\tif err := s.delivery.Body(bodyCtx, header, buf); err != nil {\n\t\treturn wrapErr(err)\n\t}\n\n\tif err := s.delivery.Commit(bodyCtx); err != nil {\n\t\treturn wrapErr(err)\n\t}\n\n\ts.log.Msg(\"accepted\", \"msg_id\", s.msgMeta.ID)\n\n\treturn nil\n}\n\ntype statusWrapper struct {\n\tsc smtp.StatusCollector\n\ts  *Session\n}\n\nfunc (sw statusWrapper) SetStatus(rcpt string, err error) {\n\tsw.sc.SetStatus(rcpt, sw.s.endp.wrapErr(sw.s.msgMeta.ID, !sw.s.opts.UTF8, \"DATA\", err))\n}\n\nfunc (s *Session) LMTPData(r io.Reader, sc smtp.StatusCollector) error {\n\ts.msgLock.Lock()\n\tdefer s.msgLock.Unlock()\n\n\tbodyCtx, bodyTask := trace.NewTask(s.msgCtx, \"DATA\")\n\tdefer bodyTask.End()\n\n\twrapErr := func(err error) error {\n\t\ts.log.Error(\"DATA error\", err, \"msg_id\", s.msgMeta.ID)\n\t\treturn s.endp.wrapErr(s.msgMeta.ID, !s.opts.UTF8, \"DATA\", err)\n\t}\n\n\theader, buf, err := s.prepareBody(r)\n\tif err != nil {\n\t\treturn wrapErr(err)\n\t}\n\tdefer func() {\n\t\tif err := buf.Remove(); err != nil {\n\t\t\ts.log.Error(\"failed to remove buffered body\", err)\n\t\t}\n\n\t\t// go-smtp will call Reset, but it will call Abort if delivery is non-nil.\n\t\ts.cleanSession()\n\t}()\n\n\tif strings.EqualFold(header.Get(\"TLS-Required\"), \"No\") {\n\t\ts.msgMeta.TLSRequireOverride = true\n\t}\n\n\tif err := s.checkRoutingLoops(header); err != nil {\n\t\treturn wrapErr(err)\n\t}\n\n\ts.delivery.(module.PartialDelivery).BodyNonAtomic(bodyCtx, statusWrapper{sc, s}, header, buf)\n\n\t// We can't really tell whether it is failed completely or succeeded\n\t// so always commit. Should be harmless, anyway.\n\tif err := s.delivery.Commit(bodyCtx); err != nil {\n\t\treturn wrapErr(err)\n\t}\n\n\ts.log.Msg(\"accepted\", \"msg_id\", s.msgMeta.ID)\n\n\treturn nil\n}\n\nfunc (s *Session) checkRoutingLoops(header textproto.Header) error {\n\t// RFC 5321 Section 6.3:\n\t// >Simple counting of the number of \"Received:\" header fields in a\n\t// >message has proven to be an effective, although rarely optimal,\n\t// >method of detecting loops in mail systems.\n\treceivedCount := 0\n\tfor f := header.FieldsByKey(\"Received\"); f.Next(); {\n\t\treceivedCount++\n\t}\n\tif receivedCount > s.endp.maxReceived {\n\t\treturn &exterrors.SMTPError{\n\t\t\tCode:         554,\n\t\t\tEnhancedCode: exterrors.EnhancedCode{5, 4, 6},\n\t\t\tMessage:      fmt.Sprintf(\"Too many Received header fields (%d), possible forwarding loop\", receivedCount),\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (endp *Endpoint) wrapErr(msgId string, mangleUTF8 bool, command string, err error) error {\n\tif err == nil {\n\t\treturn nil\n\t}\n\n\tif errors.Is(err, context.DeadlineExceeded) {\n\t\treturn &smtp.SMTPError{\n\t\t\tCode:         451,\n\t\t\tEnhancedCode: smtp.EnhancedCode{4, 4, 5},\n\t\t\tMessage:      \"High load, try again later\",\n\t\t}\n\t}\n\n\tres := &smtp.SMTPError{\n\t\tCode:         554,\n\t\tEnhancedCode: smtp.EnhancedCodeNotSet,\n\t\t// Err on the side of caution if the error lacks SMTP annotations. If\n\t\t// we just pass the error text through, we might accidenetally disclose\n\t\t// details of server configuration.\n\t\tMessage: \"Internal server error\",\n\t}\n\n\tif exterrors.IsTemporary(err) {\n\t\tres.Code = 451\n\t}\n\n\tctxInfo := exterrors.Fields(err)\n\tctxCode, ok := ctxInfo[\"smtp_code\"].(int)\n\tif ok {\n\t\tres.Code = ctxCode\n\t}\n\tctxEnchCode, ok := ctxInfo[\"smtp_enchcode\"].(exterrors.EnhancedCode)\n\tif ok {\n\t\tres.EnhancedCode = smtp.EnhancedCode(ctxEnchCode)\n\t}\n\tctxMsg, ok := ctxInfo[\"smtp_msg\"].(string)\n\tif ok {\n\t\tres.Message = ctxMsg\n\t}\n\n\tif smtpErr, ok := err.(*smtp.SMTPError); ok {\n\t\tendp.log.Printf(\"plain SMTP error returned, this is deprecated\")\n\t\tres.Code = smtpErr.Code\n\t\tres.EnhancedCode = smtpErr.EnhancedCode\n\t\tres.Message = smtpErr.Message\n\t}\n\n\tif msgId != \"\" {\n\t\tres.Message += \" (msg ID = \" + msgId + \")\"\n\t}\n\n\tfailedCmds.WithLabelValues(endp.name, command, strconv.Itoa(res.Code),\n\t\tfmt.Sprintf(\"%d.%d.%d\",\n\t\t\tres.EnhancedCode[0],\n\t\t\tres.EnhancedCode[1],\n\t\t\tres.EnhancedCode[2])).Inc()\n\n\t// INTERNATIONALIZATION: See RFC 6531 Section 3.7.4.1.\n\tif mangleUTF8 {\n\t\tb := strings.Builder{}\n\t\tb.Grow(len(res.Message))\n\t\tfor _, ch := range res.Message {\n\t\t\tif ch > 128 {\n\t\t\t\tb.WriteRune('?')\n\t\t\t} else {\n\t\t\t\tb.WriteRune(ch)\n\t\t\t}\n\t\t}\n\t\tres.Message = b.String()\n\t}\n\n\treturn res\n}\n"
  },
  {
    "path": "internal/endpoint/smtp/smtp.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage smtp\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/emersion/go-smtp\"\n\t\"github.com/foxcpp/maddy/framework/buffer\"\n\t\"github.com/foxcpp/maddy/framework/config\"\n\tmodconfig \"github.com/foxcpp/maddy/framework/config/module\"\n\ttls2 \"github.com/foxcpp/maddy/framework/config/tls\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/dns\"\n\t\"github.com/foxcpp/maddy/framework/exterrors\"\n\t\"github.com/foxcpp/maddy/framework/future\"\n\t\"github.com/foxcpp/maddy/framework/log\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n\t\"github.com/foxcpp/maddy/framework/resource/netresource\"\n\t\"github.com/foxcpp/maddy/internal/auth\"\n\t\"github.com/foxcpp/maddy/internal/authz\"\n\t\"github.com/foxcpp/maddy/internal/limits\"\n\t\"github.com/foxcpp/maddy/internal/msgpipeline\"\n\t\"github.com/foxcpp/maddy/internal/proxy_protocol\"\n\t\"golang.org/x/net/idna\"\n)\n\ntype Endpoint struct {\n\tsaslAuth      auth.SASLAuth\n\tserv          *smtp.Server\n\tname          string\n\taddrs         []string\n\tendpoints     []config.Endpoint\n\tlisteners     []net.Listener\n\tproxyProtocol *proxy_protocol.ProxyProtocol\n\tpipeline      *msgpipeline.MsgPipeline\n\tresolver      dns.Resolver\n\tlimits        *limits.Group\n\n\tbuffer func(r io.Reader) (buffer.Buffer, error)\n\n\tauthAlwaysRequired  bool\n\tsubmission          bool\n\tlmtp                bool\n\tdeferServerReject   bool\n\tmaxLoggedRcptErrors int\n\tmaxReceived         int\n\tmaxHeaderBytes      int64\n\n\tsessionCnt      atomic.Int32\n\tshutdownTimeout time.Duration\n\n\tlistenersWg sync.WaitGroup\n\n\tlog *log.Logger\n}\n\nfunc (endp *Endpoint) Name() string {\n\treturn endp.name\n}\n\nfunc (endp *Endpoint) InstanceName() string {\n\treturn endp.name\n}\n\nfunc New(c *container.C, modName string, addrs []string) (container.LifetimeModule, error) {\n\tlogger := c.DefaultLogger.Sublogger(modName)\n\tendp := &Endpoint{\n\t\tname:       modName,\n\t\taddrs:      addrs,\n\t\tsubmission: modName == \"submission\",\n\t\tlmtp:       modName == \"lmtp\",\n\t\tresolver:   dns.DefaultResolver(),\n\t\tbuffer:     buffer.BufferInMemory,\n\t\tlog:        logger,\n\t\tsaslAuth: auth.SASLAuth{\n\t\t\tLog: logger.Sublogger(\"sasl\"),\n\t\t},\n\t}\n\treturn endp, nil\n}\n\nfunc (endp *Endpoint) Configure(_ []string, cfg *config.Map) error {\n\tendp.serv = smtp.NewServer(endp)\n\tendp.serv.ErrorLog = endp.log\n\tendp.serv.LMTP = endp.lmtp\n\tendp.serv.EnableSMTPUTF8 = true\n\tendp.serv.EnableREQUIRETLS = true\n\tif err := endp.setConfig(cfg); err != nil {\n\t\treturn err\n\t}\n\n\taddresses := make([]config.Endpoint, 0, len(endp.addrs))\n\tfor _, addr := range endp.addrs {\n\t\tsaddr, err := config.ParseEndpoint(addr)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"%s: invalid address: %s\", addr, endp.name)\n\t\t}\n\n\t\taddresses = append(addresses, saddr)\n\t}\n\tendp.endpoints = addresses\n\n\tallLocal := true\n\tfor _, addr := range addresses {\n\t\tif addr.Scheme != \"unix\" && !strings.HasPrefix(addr.Host, \"127.0.0.\") {\n\t\t\tallLocal = false\n\t\t}\n\t}\n\n\tif endp.serv.AllowInsecureAuth && !allLocal {\n\t\tendp.log.Println(\"authentication over unencrypted connections is allowed, this is insecure configuration and should be used only for testing!\")\n\t}\n\tif endp.serv.TLSConfig == nil {\n\t\tif !allLocal {\n\t\t\tendp.log.Println(\"TLS is disabled, this is insecure configuration and should be used only for testing!\")\n\t\t}\n\n\t\tendp.serv.AllowInsecureAuth = true\n\t}\n\n\treturn nil\n}\n\nfunc autoBufferMode(maxSize int, dir string) func(io.Reader) (buffer.Buffer, error) {\n\treturn func(r io.Reader) (buffer.Buffer, error) {\n\t\t// First try to read up to N bytes.\n\t\tinitial := make([]byte, maxSize)\n\t\tactualSize, err := io.ReadFull(r, initial)\n\t\tif err != nil {\n\t\t\tif err == io.ErrUnexpectedEOF {\n\t\t\t\tlog.Debugln(\"autobuffer: keeping the message in RAM (read\", actualSize, \"bytes, got EOF)\")\n\t\t\t\treturn buffer.MemoryBuffer{Slice: initial[:actualSize]}, nil\n\t\t\t}\n\t\t\tif err == io.EOF {\n\t\t\t\t// Special case: message with empty body.\n\t\t\t\treturn buffer.MemoryBuffer{}, nil\n\t\t\t}\n\t\t\t// Some I/O error happened, bail out.\n\t\t\treturn nil, err\n\t\t}\n\t\tif actualSize < maxSize {\n\t\t\t// Ok, the message is smaller than N. Make a MemoryBuffer and\n\t\t\t// handle it in RAM.\n\t\t\tlog.Debugln(\"autobuffer: keeping the message in RAM (read\", actualSize, \"bytes, got short read)\")\n\t\t\treturn buffer.MemoryBuffer{Slice: initial[:actualSize]}, nil\n\t\t}\n\n\t\tlog.Debugln(\"autobuffer: spilling the message to the FS\")\n\t\t// The message is big. Dump what we got to the disk and continue writing it there.\n\t\treturn buffer.BufferInFile(\n\t\t\tio.MultiReader(bytes.NewReader(initial[:actualSize]), r),\n\t\t\tdir)\n\t}\n}\n\nfunc bufferModeDirective(_ *config.Map, node config.Node) (interface{}, error) {\n\tif len(node.Args) < 1 {\n\t\treturn nil, config.NodeErr(node, \"at least one argument required\")\n\t}\n\tswitch node.Args[0] {\n\tcase \"ram\":\n\t\tif len(node.Args) > 1 {\n\t\t\treturn nil, config.NodeErr(node, \"no additional arguments for 'ram' mode\")\n\t\t}\n\t\treturn buffer.BufferInMemory, nil\n\tcase \"fs\":\n\t\tpath := filepath.Join(config.StateDirectory, \"buffer\")\n\t\tif err := os.MkdirAll(path, 0o700); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tswitch len(node.Args) {\n\t\tcase 2:\n\t\t\tpath = node.Args[1]\n\t\t\tfallthrough\n\t\tcase 1:\n\t\t\treturn func(r io.Reader) (buffer.Buffer, error) {\n\t\t\t\treturn buffer.BufferInFile(r, path)\n\t\t\t}, nil\n\t\tdefault:\n\t\t\treturn nil, config.NodeErr(node, \"too many arguments for 'fs' mode\")\n\t\t}\n\tcase \"auto\":\n\t\tpath := filepath.Join(config.StateDirectory, \"buffer\")\n\t\tif err := os.MkdirAll(path, 0o700); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tmaxSize := 1 * 1024 * 1024 // 1 MiB\n\t\tswitch len(node.Args) {\n\t\tcase 3:\n\t\t\tpath = node.Args[2]\n\t\t\tfallthrough\n\t\tcase 2:\n\t\t\tvar err error\n\t\t\tmaxSize, err = config.ParseDataSize(node.Args[1])\n\t\t\tif err != nil {\n\t\t\t\treturn nil, config.NodeErr(node, \"%v\", err)\n\t\t\t}\n\t\t\tfallthrough\n\t\tcase 1:\n\t\t\treturn autoBufferMode(maxSize, path), nil\n\t\tdefault:\n\t\t\treturn nil, config.NodeErr(node, \"too many arguments for 'auto' mode\")\n\t\t}\n\tdefault:\n\t\treturn nil, config.NodeErr(node, \"unknown buffer mode: %v\", node.Args[0])\n\t}\n}\n\nfunc (endp *Endpoint) setConfig(cfg *config.Map) error {\n\tvar (\n\t\thostname string\n\t\terr      error\n\t\tioDebug  bool\n\t)\n\n\tcfg.Callback(\"auth\", func(m *config.Map, node config.Node) error {\n\t\treturn endp.saslAuth.AddProvider(m, node)\n\t})\n\tcfg.Bool(\"sasl_login\", false, false, &endp.saslAuth.EnableLogin)\n\tcfg.String(\"hostname\", true, true, \"\", &hostname)\n\tconfig.EnumMapped(cfg, \"auth_map_normalize\", true, false, authz.NormalizeFuncs, authz.NormalizeAuto,\n\t\t&endp.saslAuth.AuthNormalize)\n\tmodconfig.Table(cfg, \"auth_map\", true, false, nil, &endp.saslAuth.AuthMap)\n\tcfg.Duration(\"write_timeout\", false, false, 1*time.Minute, &endp.serv.WriteTimeout)\n\tcfg.Duration(\"read_timeout\", false, false, 10*time.Minute, &endp.serv.ReadTimeout)\n\tcfg.Duration(\"shutdown_timeout\", false, false, 3*time.Minute, &endp.shutdownTimeout)\n\tcfg.DataSize(\"max_message_size\", false, false, 32*1024*1024, &endp.serv.MaxMessageBytes)\n\tcfg.DataSize(\"max_header_size\", false, false, 1*1024*1024, &endp.maxHeaderBytes)\n\tcfg.Int(\"max_recipients\", false, false, 20000, &endp.serv.MaxRecipients)\n\tcfg.Int(\"max_received\", false, false, 50, &endp.maxReceived)\n\tcfg.Custom(\"buffer\", false, false, func() (interface{}, error) {\n\t\tpath := filepath.Join(config.StateDirectory, \"buffer\")\n\t\tif err := os.MkdirAll(path, 0o700); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn autoBufferMode(1*1024*1024 /* 1 MiB */, path), nil\n\t}, bufferModeDirective, &endp.buffer)\n\tcfg.Custom(\"tls\", true, endp.name != \"lmtp\", nil, tls2.TLSDirective, &endp.serv.TLSConfig)\n\tcfg.Custom(\"proxy_protocol\", false, false, nil, proxy_protocol.ProxyProtocolDirective, &endp.proxyProtocol)\n\tcfg.Bool(\"insecure_auth\", endp.name == \"lmtp\", false, &endp.serv.AllowInsecureAuth)\n\tcfg.Int(\"smtp_max_line_length\", false, false, 4000, &endp.serv.MaxLineLength)\n\tcfg.Bool(\"io_debug\", false, false, &ioDebug)\n\tcfg.Bool(\"debug\", true, false, &endp.log.Debug)\n\tcfg.Bool(\"defer_sender_reject\", false, true, &endp.deferServerReject)\n\tcfg.Int(\"max_logged_rcpt_errors\", false, false, 5, &endp.maxLoggedRcptErrors)\n\tcfg.Custom(\"limits\", false, false, func() (interface{}, error) {\n\t\treturn &limits.Group{}, nil\n\t}, func(cfg *config.Map, n config.Node) (interface{}, error) {\n\t\tvar g *limits.Group\n\t\tif err := modconfig.GroupFromNode(\"limits\", n.Args, n, cfg.Globals, &g); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn g, nil\n\t}, &endp.limits)\n\tcfg.AllowUnknown()\n\tunknown, err := cfg.Process()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tendp.saslAuth.Log.Debug = endp.log.Debug\n\tendp.saslAuth.ErrorMap = endp.authErrorMap\n\n\t// INTERNATIONALIZATION: See RFC 6531 Section 3.3.\n\tendp.serv.Domain, err = idna.ToASCII(hostname)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"%s: cannot represent the hostname as an A-label name: %w\", endp.name, err)\n\t}\n\n\tendp.pipeline, err = msgpipeline.New(cfg.Globals, unknown)\n\tif err != nil {\n\t\treturn err\n\t}\n\tendp.pipeline.Hostname = endp.serv.Domain\n\tendp.pipeline.Resolver = endp.resolver\n\tendp.pipeline.Log = endp.log.Sublogger(\"pipeline\")\n\tendp.pipeline.FirstPipeline = true\n\n\tif endp.submission {\n\t\tendp.authAlwaysRequired = true\n\t\tif len(endp.saslAuth.SASLMechanisms()) == 0 {\n\t\t\treturn fmt.Errorf(\"%s: auth. provider must be set for submission endpoint\", endp.name)\n\t\t}\n\t}\n\n\tif ioDebug {\n\t\tendp.serv.Debug = endp.log.DebugWriter()\n\t\tendp.log.Println(\"I/O debugging is on! It may leak passwords in logs, be careful!\")\n\t}\n\n\treturn nil\n}\n\nfunc (endp *Endpoint) Start() error {\n\tif err := endp.setupListeners(endp.endpoints); err != nil {\n\t\tif err := endp.Stop(); err != nil {\n\t\t\tendp.log.Error(\"failed to Stop after setupListeners fail\", err)\n\t\t}\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (endp *Endpoint) authErrorMap(err error) error {\n\tif exterrors.IsTemporary(err) {\n\t\treturn &smtp.SMTPError{\n\t\t\tCode:         454,\n\t\t\tEnhancedCode: smtp.EnhancedCode{4, 7, 0},\n\t\t\tMessage:      \"Temporary authentication failure\",\n\t\t}\n\t}\n\n\treturn &smtp.SMTPError{\n\t\tCode:         535,\n\t\tEnhancedCode: smtp.EnhancedCode{5, 7, 8},\n\t\tMessage:      \"Invalid credentials\",\n\t}\n}\n\nfunc (endp *Endpoint) setupListeners(addresses []config.Endpoint) error {\n\tfor _, addr := range addresses {\n\t\tvar l net.Listener\n\t\tvar err error\n\t\tl, err = netresource.Listen(addr.Network(), addr.Address())\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"%s: %w\", endp.name, err)\n\t\t}\n\t\tendp.log.Printf(\"listening on %v\", addr)\n\n\t\tif addr.IsTLS() {\n\t\t\tif endp.serv.TLSConfig == nil {\n\t\t\t\treturn fmt.Errorf(\"%s: can't bind on SMTPS endpoint without TLS configuration\", endp.name)\n\t\t\t}\n\t\t\tl = tls.NewListener(l, endp.serv.TLSConfig)\n\t\t}\n\n\t\tif endp.proxyProtocol != nil {\n\t\t\tl = proxy_protocol.NewListener(l, endp.proxyProtocol, endp.log.Sublogger(\"proxy\"))\n\t\t}\n\n\t\tendp.listeners = append(endp.listeners, l)\n\n\t\tendp.listenersWg.Add(1)\n\t\tgo func() {\n\t\t\tif err := endp.serv.Serve(l); err != nil {\n\t\t\t\tendp.log.Printf(\"failed to serve %s: %s\", addr, err)\n\t\t\t}\n\t\t\tendp.listenersWg.Done()\n\t\t}()\n\t}\n\n\treturn nil\n}\n\nfunc (endp *Endpoint) NewSession(conn *smtp.Conn) (smtp.Session, error) {\n\tsess := endp.newSession(conn)\n\n\t// Executed before authentication and session initialization.\n\tif err := endp.pipeline.RunEarlyChecks(context.TODO(), &sess.connState); err != nil {\n\t\tif err := sess.Logout(); err != nil {\n\t\t\tendp.log.Error(\"early checks logout failed\", err)\n\t\t}\n\t\treturn nil, endp.wrapErr(\"\", true, \"EHLO\", err)\n\t}\n\n\tendp.sessionCnt.Add(1)\n\n\treturn sess, nil\n}\n\nfunc (endp *Endpoint) newSession(conn *smtp.Conn) *Session {\n\ts := &Session{\n\t\tendp:       endp,\n\t\tlog:        endp.log,\n\t\tsessionCtx: context.Background(),\n\t}\n\n\t// Used in tests.\n\tif conn == nil {\n\t\treturn s\n\t}\n\n\ts.connState = module.ConnState{\n\t\tHostname:   conn.Hostname(),\n\t\tLocalAddr:  conn.Conn().LocalAddr(),\n\t\tRemoteAddr: conn.Conn().RemoteAddr(),\n\t}\n\tif tlsState, ok := conn.TLSConnectionState(); ok {\n\t\ts.connState.TLS = tlsState\n\t}\n\n\tif endp.serv.LMTP {\n\t\ts.connState.Proto = \"LMTP\"\n\t} else {\n\t\t// Check if TLS connection conn struct is poplated.\n\t\t// If it is - we are ssing TLS.\n\t\tif s.connState.TLS.HandshakeComplete {\n\t\t\ts.connState.Proto = \"ESMTPS\"\n\t\t} else {\n\t\t\ts.connState.Proto = \"ESMTP\"\n\t\t}\n\t}\n\n\tif endp.resolver != nil {\n\t\trdnsCtx, cancelRDNS := context.WithCancel(s.sessionCtx)\n\t\ts.connState.RDNSName = future.New()\n\t\ts.cancelRDNS = cancelRDNS\n\t\tgo s.fetchRDNSName(rdnsCtx)\n\t}\n\n\treturn s\n}\n\nfunc (endp *Endpoint) ConnectionCount() int {\n\treturn int(endp.sessionCnt.Load())\n}\n\nfunc (endp *Endpoint) Stop() error {\n\tctx, cancel := context.WithTimeout(context.Background(), endp.shutdownTimeout)\n\tdefer cancel()\n\n\tif err := endp.serv.Shutdown(ctx); err != nil {\n\t\treturn err\n\t}\n\n\tendp.listenersWg.Wait()\n\n\treturn nil\n}\n\nfunc init() {\n\tmodules.RegisterEndpoint(\"smtp\", New)\n\tmodules.RegisterEndpoint(\"submission\", New)\n\tmodules.RegisterEndpoint(\"lmtp\", New)\n}\n"
  },
  {
    "path": "internal/endpoint/smtp/smtp_test.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage smtp\n\nimport (\n\t\"flag\"\n\t\"math/rand\"\n\t\"net\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/emersion/go-sasl\"\n\t\"github.com/emersion/go-smtp\"\n\t\"github.com/foxcpp/go-mockdns\"\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/exterrors\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n\t\"github.com/foxcpp/maddy/internal/auth\"\n\t\"github.com/foxcpp/maddy/internal/msgpipeline\"\n\t\"github.com/foxcpp/maddy/internal/testutils\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nvar testPort string\n\nconst testMsg = \"From: <sender@example.org>\\r\\n\" +\n\t\"Subject: Hello there!\\r\\n\" +\n\t\"\\r\\n\" +\n\t\"foobar\\r\\n\"\n\nfunc testEndpoint(t *testing.T, modName string, authMod module.PlainAuth, tgt module.DeliveryTarget, checks []module.Check, cfg []config.Node) *Endpoint {\n\tt.Helper()\n\n\tmod, err := New(container.New(), modName, []string{\"tcp://127.0.0.1:\" + testPort})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tendp := mod.(*Endpoint)\n\n\tendp.resolver = &mockdns.Resolver{\n\t\tZones: map[string]mockdns.Zone{\n\t\t\t\"mx.example.org.\": {\n\t\t\t\tA: []string{\"127.0.0.1\"},\n\t\t\t},\n\t\t\t\"1.0.0.127.in-addr.arpa.\": {\n\t\t\t\tPTR: []string{\"mx.example.org\"},\n\t\t\t},\n\t\t},\n\t}\n\tendp.log = testutils.Logger(t, \"smtp\")\n\n\tcfg = append(cfg,\n\t\tconfig.Node{\n\t\t\tName: \"hostname\",\n\t\t\tArgs: []string{\"mx.example.com\"},\n\t\t},\n\t\tconfig.Node{\n\t\t\tName: \"tls\",\n\t\t\tArgs: []string{\"off\"},\n\t\t},\n\t\tconfig.Node{ // To make it succeed, pipeline is actually replaced below.\n\t\t\tName: \"deliver_to\",\n\t\t\tArgs: []string{\"dummy\"},\n\t\t},\n\t)\n\n\tif authMod != nil {\n\t\tcfg = append(cfg, config.Node{\n\t\t\tName: \"auth\",\n\t\t\tArgs: []string{\"dummy\"},\n\t\t})\n\t}\n\n\terr = endp.Configure(nil, config.NewMap(nil, config.Node{\n\t\tChildren: cfg,\n\t}))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tendp.saslAuth = auth.SASLAuth{\n\t\tLog:   testutils.Logger(t, \"smtp/saslauth\"),\n\t\tPlain: []module.PlainAuth{authMod},\n\t}\n\n\tendp.pipeline = msgpipeline.Mock(tgt, checks)\n\tendp.pipeline.Hostname = \"mx.example.com\"\n\tendp.pipeline.Resolver = endp.resolver\n\tendp.pipeline.FirstPipeline = true\n\tendp.pipeline.Log = testutils.Logger(t, \"smtp/pipeline\")\n\n\tif err := endp.Start(); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\treturn endp\n}\n\nfunc submitMsg(t *testing.T, cl *smtp.Client, from string, rcpts []string, msg string) error {\n\treturn submitMsgOpts(t, cl, from, rcpts, nil, msg)\n}\n\nfunc submitMsgOpts(t *testing.T, cl *smtp.Client, from string, rcpts []string, opts *smtp.MailOptions, msg string) error {\n\tt.Helper()\n\n\t// Error for this one is ignored because it fails if EHLO was already sent\n\t// and submitMsg can happen multiple times.\n\t_ = cl.Hello(\"mx.example.org\")\n\tif err := cl.Mail(from, opts); err != nil {\n\t\treturn err\n\t}\n\tfor _, rcpt := range rcpts {\n\t\tif err := cl.Rcpt(rcpt, &smtp.RcptOptions{}); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tdata, err := cl.Data()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif _, err := data.Write([]byte(msg)); err != nil {\n\t\treturn err\n\t}\n\n\treturn data.Close()\n}\n\nfunc TestSMTPDelivery(t *testing.T) {\n\ttgt := testutils.Target{}\n\tendp := testEndpoint(t, \"smtp\", nil, &tgt, nil, nil)\n\tdefer func() {\n\t\tassert.NoError(t, endp.Stop())\n\t}()\n\n\tcl, err := smtp.Dial(\"127.0.0.1:\" + testPort)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer func() {\n\t\tassert.NoError(t, cl.Close())\n\t}()\n\n\terr = submitMsg(t, cl, \"sender@example.org\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"}, testMsg)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tif len(tgt.Messages) != 1 {\n\t\tt.Fatal(\"Expected a message, got\", len(tgt.Messages))\n\t}\n\tmsg := tgt.Messages[0]\n\tmsgID := testutils.CheckMsgID(t, &msg, \"sender@example.org\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"}, \"\")\n\n\treceivedPrefix := `from mx.example.org (mx.example.org [127.0.0.1]) by mx.example.com (envelope-sender <sender@example.org>) with ESMTP id ` + msgID\n\n\tif !strings.HasPrefix(msg.Header.Get(\"Received\"), receivedPrefix) {\n\t\tt.Error(\"Wrong Received contents:\", msg.Header.Get(\"Received\"))\n\t}\n\n\tif msg.MsgMeta.Conn.Proto != \"ESMTP\" {\n\t\tt.Error(\"Wrong SrcProto:\", msg.MsgMeta.Conn.Proto)\n\t}\n\n\trdnsName, _ := msg.MsgMeta.Conn.RDNSName.Get()\n\tif rdnsName, _ := rdnsName.(string); rdnsName != \"mx.example.org\" {\n\t\tt.Error(\"Wrong rDNS name:\", rdnsName)\n\t}\n}\n\nfunc TestSMTPDelivery_rDNSError(t *testing.T) {\n\ttgt := testutils.Target{}\n\tendp := testEndpoint(t, \"smtp\", nil, &tgt, nil, nil)\n\tdefer func() {\n\t\tassert.NoError(t, endp.Stop())\n\t}()\n\n\tendp.resolver.(*mockdns.Resolver).Zones[\"1.0.0.127.in-addr.arpa.\"] = mockdns.Zone{\n\t\tErr: &net.DNSError{\n\t\t\tName:       \"1.0.0.127.in-addr.arpa.\",\n\t\t\tServer:     \"127.0.0.1:53\",\n\t\t\tErr:        \"bad\",\n\t\t\tIsNotFound: false,\n\t\t},\n\t}\n\n\tcl, err := smtp.Dial(\"127.0.0.1:\" + testPort)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer func() {\n\t\tassert.NoError(t, cl.Close())\n\t}()\n\n\terr = submitMsg(t, cl, \"sender@example.org\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"}, testMsg)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tif len(tgt.Messages) != 1 {\n\t\tt.Fatal(\"Expected a message, got\", len(tgt.Messages))\n\t}\n\tmsg := tgt.Messages[0]\n\ttestutils.CheckMsgID(t, &msg, \"sender@example.org\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"}, \"\")\n\n\trdnsName, err := msg.MsgMeta.Conn.RDNSName.Get()\n\tif rdnsName != nil || err == nil {\n\t\tt.Errorf(\"Wrong rDNS result: %#+v (%v)\", rdnsName, err)\n\t}\n}\n\nfunc TestSMTPDelivery_EarlyCheck_Fail(t *testing.T) {\n\ttgt := testutils.Target{}\n\tendp := testEndpoint(t, \"smtp\", nil, &tgt, []module.Check{\n\t\t&testutils.Check{\n\t\t\tEarlyErr: &exterrors.SMTPError{\n\t\t\t\tCode:    523,\n\t\t\t\tMessage: \"Hey\",\n\t\t\t},\n\t\t},\n\t}, nil)\n\tdefer func() {\n\t\tassert.NoError(t, endp.Stop())\n\t}()\n\n\tcl, err := smtp.Dial(\"127.0.0.1:\" + testPort)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer func() {\n\t\tassert.NoError(t, cl.Close())\n\t}()\n\n\terr = cl.Mail(\"sender@example.org\", nil)\n\tif err == nil {\n\t\tt.Fatal(\"Expected an error, got none\")\n\t}\n\n\tsmtpErr, ok := err.(*smtp.SMTPError)\n\tif !ok {\n\t\tt.Fatal(\"Non-SMTPError returned\")\n\t}\n\n\tif smtpErr.Code != 523 {\n\t\tt.Fatal(\"Wrong SMTP code:\", smtpErr.Code)\n\t}\n\tif smtpErr.Message != \"Hey\" {\n\t\tt.Fatal(\"Wrong SMTP message:\", smtpErr.Message)\n\t}\n}\n\nfunc TestSMTPDeliver_CheckError(t *testing.T) {\n\ttgt := testutils.Target{}\n\tendp := testEndpoint(t, \"smtp\", nil, &tgt, []module.Check{\n\t\t&testutils.Check{\n\t\t\tConnRes: module.CheckResult{\n\t\t\t\tReason: &exterrors.SMTPError{\n\t\t\t\t\tCode:    523,\n\t\t\t\t\tMessage: \"Hey\",\n\t\t\t\t},\n\t\t\t\tReject: true,\n\t\t\t},\n\t\t},\n\t}, nil)\n\tendp.deferServerReject = false\n\tdefer func() {\n\t\tassert.NoError(t, endp.Stop())\n\t}()\n\n\tcl, err := smtp.Dial(\"127.0.0.1:\" + testPort)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer func() {\n\t\tassert.NoError(t, cl.Close())\n\t}()\n\n\terr = cl.Mail(\"sender@example.org\", nil)\n\tif err == nil {\n\t\tt.Fatal(\"Expected an error, got none\")\n\t}\n\tsmtpErr, ok := err.(*smtp.SMTPError)\n\tif !ok {\n\t\tt.Fatal(\"Non-SMTPError returned\")\n\t}\n\n\tif smtpErr.Code != 523 {\n\t\tt.Fatal(\"Wrong SMTP code:\", smtpErr.Code)\n\t}\n\tif !strings.HasPrefix(smtpErr.Message, \"Hey\") {\n\t\tt.Fatal(\"Wrong SMTP message:\", smtpErr.Message)\n\t}\n}\n\nfunc TestSMTPDeliver_CheckError_Deferred(t *testing.T) {\n\ttgt := testutils.Target{}\n\tendp := testEndpoint(t, \"smtp\", nil, &tgt, []module.Check{\n\t\t&testutils.Check{\n\t\t\tConnRes: module.CheckResult{\n\t\t\t\tReason: &exterrors.SMTPError{\n\t\t\t\t\tCode:    523,\n\t\t\t\t\tMessage: \"Hey\",\n\t\t\t\t},\n\t\t\t\tReject: true,\n\t\t\t},\n\t\t},\n\t}, nil)\n\tendp.deferServerReject = true\n\tdefer func() {\n\t\tassert.NoError(t, endp.Stop())\n\t}()\n\n\tcl, err := smtp.Dial(\"127.0.0.1:\" + testPort)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer func() {\n\t\tassert.NoError(t, cl.Close())\n\t}()\n\n\terr = cl.Mail(\"sender@example.org\", nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tcheckErr := func(err error) {\n\t\tif err == nil {\n\t\t\tt.Fatal(\"Expected an error, got none\")\n\t\t}\n\t\tsmtpErr, ok := err.(*smtp.SMTPError)\n\t\tif !ok {\n\t\t\tt.Error(\"Non-SMTPError returned\")\n\t\t\treturn\n\t\t}\n\n\t\tif smtpErr.Code != 523 {\n\t\t\tt.Error(\"Wrong SMTP code:\", smtpErr.Code)\n\t\t}\n\t\tif !strings.HasPrefix(smtpErr.Message, \"Hey\") {\n\t\t\tt.Error(\"Wrong SMTP message:\", smtpErr.Message)\n\t\t}\n\t}\n\n\tcheckErr(cl.Rcpt(\"test1@example.org\", &smtp.RcptOptions{}))\n\tcheckErr(cl.Rcpt(\"test1@example.org\", &smtp.RcptOptions{}))\n\tcheckErr(cl.Rcpt(\"test2@example.org\", &smtp.RcptOptions{}))\n}\n\nfunc TestSMTPDelivery_Multi(t *testing.T) {\n\ttgt := testutils.Target{}\n\tendp := testEndpoint(t, \"smtp\", nil, &tgt, nil, nil)\n\tdefer func() {\n\t\tassert.NoError(t, endp.Stop())\n\t}()\n\n\tcl, err := smtp.Dial(\"127.0.0.1:\" + testPort)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer func() {\n\t\tassert.NoError(t, cl.Close())\n\t}()\n\n\terr = submitMsg(t, cl, \"sender1@example.org\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"}, testMsg)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\terr = submitMsg(t, cl, \"sender2@example.org\", []string{\"rcpt3@example.com\", \"rcpt4@example.com\"}, testMsg)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tif len(tgt.Messages) != 2 {\n\t\tt.Fatal(\"Expected two messages, got\", len(tgt.Messages))\n\t}\n\tmsg := tgt.Messages[0]\n\tmsgID := testutils.CheckMsgID(t, &msg, \"sender1@example.org\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"}, \"\")\n\treceivedPrefix := `from mx.example.org (mx.example.org [127.0.0.1]) by mx.example.com (envelope-sender <sender1@example.org>) with ESMTP id ` + msgID\n\tif !strings.HasPrefix(msg.Header.Get(\"Received\"), receivedPrefix) {\n\t\tt.Error(\"Wrong Received contents:\", msg.Header.Get(\"Received\"))\n\t}\n\n\tmsg = tgt.Messages[1]\n\tmsgID = testutils.CheckMsgID(t, &msg, \"sender2@example.org\", []string{\"rcpt3@example.com\", \"rcpt4@example.com\"}, \"\")\n\treceivedPrefix = `from mx.example.org (mx.example.org [127.0.0.1]) by mx.example.com (envelope-sender <sender2@example.org>) with ESMTP id ` + msgID\n\tif !strings.HasPrefix(msg.Header.Get(\"Received\"), receivedPrefix) {\n\t\tt.Error(\"Wrong Received contents:\", msg.Header.Get(\"Received\"))\n\t}\n}\n\nfunc TestSMTPDelivery_AbortData(t *testing.T) {\n\ttgt := testutils.Target{}\n\tendp := testEndpoint(t, \"smtp\", nil, &tgt, nil, nil)\n\tdefer func() {\n\t\tassert.NoError(t, endp.Stop())\n\t}()\n\n\tcl, err := smtp.Dial(\"127.0.0.1:\" + testPort)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer func() {\n\t\t_ = cl.Close()\n\t}()\n\n\tif err := cl.Hello(\"mx.example.org\"); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := cl.Mail(\"sender@example.org\", nil); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := cl.Rcpt(\"test@example.com\", &smtp.RcptOptions{}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdata, err := cl.Data()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif _, err := data.Write([]byte(testMsg)); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// Then.. Suddenly, close the connection without sending the final dot.\n\trequire.NoError(t, cl.Close())\n\n\ttime.Sleep(250 * time.Millisecond)\n\n\tif len(tgt.Messages) != 0 {\n\t\tt.Fatal(\"Expected no messages, got\", len(tgt.Messages))\n\t}\n}\n\nfunc TestSMTPDelivery_EmptyMessage(t *testing.T) {\n\ttgt := testutils.Target{}\n\tendp := testEndpoint(t, \"smtp\", nil, &tgt, nil, nil)\n\tdefer func() {\n\t\tassert.NoError(t, endp.Stop())\n\t}()\n\n\tcl, err := smtp.Dial(\"127.0.0.1:\" + testPort)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer func() {\n\t\tassert.NoError(t, cl.Close())\n\t}()\n\n\tif err := cl.Hello(\"mx.example.org\"); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := cl.Mail(\"sender@example.org\", nil); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := cl.Rcpt(\"test@example.com\", &smtp.RcptOptions{}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdata, err := cl.Data()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := data.Close(); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\ttime.Sleep(250 * time.Millisecond)\n\n\tif len(tgt.Messages) != 1 {\n\t\tt.Fatal(\"Expected 1 message, got\", len(tgt.Messages))\n\t}\n\tmsg := tgt.Messages[0]\n\tif len(msg.Body) != 0 {\n\t\tt.Fatal(\"Expected an empty body, got\", len(msg.Body))\n\t}\n}\n\nfunc TestSMTPDelivery_AbortLogout(t *testing.T) {\n\ttgt := testutils.Target{}\n\tendp := testEndpoint(t, \"smtp\", nil, &tgt, nil, nil)\n\tdefer func() {\n\t\tassert.NoError(t, endp.Stop())\n\t}()\n\n\tcl, err := smtp.Dial(\"127.0.0.1:\" + testPort)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer func() {\n\t\t_ = cl.Close()\n\t}()\n\n\tif err := cl.Hello(\"mx.example.org\"); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := cl.Mail(\"sender@example.org\", nil); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := cl.Rcpt(\"test@example.com\", &smtp.RcptOptions{}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// Then.. Suddenly, close the connection.\n\trequire.NoError(t, cl.Close())\n\n\ttime.Sleep(250 * time.Millisecond)\n\n\tif len(tgt.Messages) != 0 {\n\t\tt.Fatal(\"Expected no messages, got\", len(tgt.Messages))\n\t}\n}\n\nfunc TestSMTPDelivery_Reset(t *testing.T) {\n\ttgt := testutils.Target{}\n\tendp := testEndpoint(t, \"smtp\", nil, &tgt, nil, nil)\n\tdefer func() {\n\t\tassert.NoError(t, endp.Stop())\n\t}()\n\n\tcl, err := smtp.Dial(\"127.0.0.1:\" + testPort)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer func() {\n\t\tassert.NoError(t, cl.Close())\n\t}()\n\n\tif err := cl.Mail(\"from-garbage@example.org\", nil); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := cl.Rcpt(\"to-garbage@example.org\", &smtp.RcptOptions{}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := cl.Reset(); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// then submit the message as if nothing happened.\n\n\terr = submitMsg(t, cl, \"sender@example.org\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"}, testMsg)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tif len(tgt.Messages) != 1 {\n\t\tt.Fatal(\"Expected a message, got\", len(tgt.Messages))\n\t}\n\tmsg := tgt.Messages[0]\n\ttestutils.CheckMsgID(t, &msg, \"sender@example.org\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"}, \"\")\n}\n\nfunc TestSMTPDelivery_SubmissionAuthRequire(t *testing.T) {\n\ttgt := testutils.Target{}\n\tendp := testEndpoint(t, \"submission\", &modules.Dummy{}, &tgt, nil, nil)\n\tdefer func() {\n\t\tassert.NoError(t, endp.Stop())\n\t}()\n\n\tcl, err := smtp.Dial(\"127.0.0.1:\" + testPort)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer func() {\n\t\tassert.NoError(t, cl.Close())\n\t}()\n\n\tif err := cl.Mail(\"from-garbage@example.org\", nil); err == nil {\n\t\tt.Fatal(\"Expected an error, got none\")\n\t}\n}\n\nfunc TestSMTPDelivery_SubmissionAuthOK(t *testing.T) {\n\ttgt := testutils.Target{}\n\tendp := testEndpoint(t, \"submission\", &modules.Dummy{}, &tgt, nil, nil)\n\tdefer func() {\n\t\tassert.NoError(t, endp.Stop())\n\t}()\n\n\tcl, err := smtp.Dial(\"127.0.0.1:\" + testPort)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer func() {\n\t\tassert.NoError(t, cl.Close())\n\t}()\n\n\tif err := cl.Auth(sasl.NewPlainClient(\"\", \"user\", \"password\")); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tif err := submitMsg(t, cl, \"sender@example.org\", []string{\"rcpt@example.org\"}, testMsg); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tif len(tgt.Messages) != 1 {\n\t\tt.Fatal(\"Expected a message, got\", len(tgt.Messages))\n\t}\n\tmsg := tgt.Messages[0]\n\tmsgID := testutils.CheckMsgID(t, &msg, \"sender@example.org\", []string{\"rcpt@example.org\"}, \"\")\n\n\tif msg.MsgMeta.Conn.AuthUser != \"user\" {\n\t\tt.Error(\"Wrong AuthUser:\", msg.MsgMeta.Conn.AuthUser)\n\t}\n\tif msg.MsgMeta.Conn.AuthPassword != \"password\" {\n\t\tt.Error(\"Wrong AuthPassword:\", msg.MsgMeta.Conn.AuthPassword)\n\t}\n\n\treceivedPrefix := `by mx.example.com (envelope-sender <sender@example.org>) with ESMTP id ` + msgID\n\tif !strings.HasPrefix(msg.Header.Get(\"Received\"), receivedPrefix) {\n\t\tt.Error(\"Wrong Received contents:\", msg.Header.Get(\"Received\"))\n\t}\n\n\tif msg.Header.Get(\"Message-ID\") == \"\" {\n\t\tt.Error(\"No submissionPrepare run\")\n\t}\n}\n\nfunc TestMain(m *testing.M) {\n\tremoteSmtpPort := flag.String(\"test.smtpport\", \"random\", \"(maddy) SMTP port to use for connections in tests\")\n\tflag.Parse()\n\n\tif *remoteSmtpPort == \"random\" {\n\t\t*remoteSmtpPort = strconv.Itoa(rand.Intn(65536-10000) + 10000)\n\t}\n\n\ttestPort = *remoteSmtpPort\n\tos.Exit(m.Run())\n}\n"
  },
  {
    "path": "internal/endpoint/smtp/smtputf8_test.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage smtp\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/emersion/go-smtp\"\n\t\"github.com/foxcpp/go-mockdns\"\n\t\"github.com/foxcpp/maddy/framework/exterrors\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/internal/testutils\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestSMTPUTF8_MangleStatusMessage(t *testing.T) {\n\ttgt := testutils.Target{}\n\tendp := testEndpoint(t, \"smtp\", nil, &tgt, []module.Check{\n\t\t&testutils.Check{\n\t\t\tConnRes: module.CheckResult{\n\t\t\t\tReason: &exterrors.SMTPError{\n\t\t\t\t\tCode:    523,\n\t\t\t\t\tMessage: \"Hey 凱凱\",\n\t\t\t\t},\n\t\t\t\tReject: true,\n\t\t\t},\n\t\t},\n\t}, nil)\n\tendp.deferServerReject = false\n\tdefer func() {\n\t\tassert.NoError(t, endp.Stop())\n\t}()\n\tdefer testutils.WaitForConnsClose(t, endp.serv)\n\n\tcl, err := smtp.Dial(\"127.0.0.1:\" + testPort)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer func() {\n\t\tassert.NoError(t, cl.Close())\n\t}()\n\n\terr = cl.Mail(\"sender@example.org\", nil)\n\tif err == nil {\n\t\tt.Fatal(\"Expected an error, got none\")\n\t}\n\tsmtpErr, ok := err.(*smtp.SMTPError)\n\tif !ok {\n\t\tt.Fatal(\"Non-SMTPError returned\")\n\t}\n\n\tif smtpErr.Code != 523 {\n\t\tt.Fatal(\"Wrong SMTP code:\", smtpErr.Code)\n\t}\n\tif !strings.HasPrefix(smtpErr.Message, \"Hey ??\") {\n\t\tt.Fatal(\"Wrong SMTP message:\", smtpErr.Message)\n\t}\n}\n\nfunc TestSMTP_RejectNonASCIIFrom(t *testing.T) {\n\ttgt := testutils.Target{}\n\tendp := testEndpoint(t, \"smtp\", nil, &tgt, nil, nil)\n\tendp.deferServerReject = false\n\tdefer func() {\n\t\tassert.NoError(t, endp.Stop())\n\t}()\n\tdefer testutils.WaitForConnsClose(t, endp.serv)\n\n\tcl, err := smtp.Dial(\"127.0.0.1:\" + testPort)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer func() {\n\t\trequire.NoError(t, cl.Close())\n\t}()\n\n\terr = submitMsg(t, cl, \"ѣ@example.org\", []string{\"rcpt@example.com\"}, testMsg)\n\n\tsmtpErr, ok := err.(*smtp.SMTPError)\n\tif !ok {\n\t\tt.Fatal(\"Non-SMTPError returned\")\n\t}\n\tif smtpErr.Code != 550 {\n\t\tt.Fatal(\"Wrong SMTP code:\", smtpErr.Code)\n\t}\n\tif smtpErr.EnhancedCode != (smtp.EnhancedCode{5, 6, 7}) {\n\t\tt.Fatal(\"Wrong SMTP ench. code:\", smtpErr.EnhancedCode)\n\t}\n}\n\nfunc TestSMTPUTF8_NormalizeCaseFoldFrom(t *testing.T) {\n\ttgt := testutils.Target{}\n\tendp := testEndpoint(t, \"smtp\", nil, &tgt, nil, nil)\n\tendp.deferServerReject = false\n\tdefer func() {\n\t\tassert.NoError(t, endp.Stop())\n\t}()\n\tdefer testutils.WaitForConnsClose(t, endp.serv)\n\n\tcl, err := smtp.Dial(\"127.0.0.1:\" + testPort)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer func() {\n\t\tassert.NoError(t, cl.Close())\n\t}()\n\n\terr = submitMsgOpts(t, cl, \"foo@E\\u0301.example.org\", []string{\"rcpt@example.com\"}, &smtp.MailOptions{\n\t\tUTF8: true,\n\t}, testMsg)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tif len(tgt.Messages) != 1 {\n\t\tt.Fatal(\"Expected a message, got\", len(tgt.Messages))\n\t}\n\tmsg := tgt.Messages[0]\n\ttestutils.CheckMsgID(t, &msg, \"foo@é.example.org\", []string{\"rcpt@example.com\"}, \"\")\n}\n\nfunc TestSMTP_RejectNonASCIIRcpt(t *testing.T) {\n\ttgt := testutils.Target{}\n\tendp := testEndpoint(t, \"smtp\", nil, &tgt, nil, nil)\n\tendp.deferServerReject = false\n\tdefer func() {\n\t\tassert.NoError(t, endp.Stop())\n\t}()\n\tdefer testutils.WaitForConnsClose(t, endp.serv)\n\n\tcl, err := smtp.Dial(\"127.0.0.1:\" + testPort)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer func() {\n\t\tassert.NoError(t, cl.Close())\n\t}()\n\n\terr = submitMsg(t, cl, \"x@example.org\", []string{\"ѣ@example.org\"}, testMsg)\n\n\tsmtpErr, ok := err.(*smtp.SMTPError)\n\tif !ok {\n\t\tt.Fatal(\"Non-SMTPError returned\")\n\t}\n\tif smtpErr.Code != 553 {\n\t\tt.Fatal(\"Wrong SMTP code:\", smtpErr.Code)\n\t}\n\tif smtpErr.EnhancedCode != (smtp.EnhancedCode{5, 6, 7}) {\n\t\tt.Fatal(\"Wrong SMTP ench. code:\", smtpErr.EnhancedCode)\n\t}\n}\n\nfunc TestSMTPUTF8_NormalizeCaseFoldRcpt(t *testing.T) {\n\ttgt := testutils.Target{}\n\tendp := testEndpoint(t, \"smtp\", nil, &tgt, nil, nil)\n\tendp.deferServerReject = false\n\tdefer func() {\n\t\tassert.NoError(t, endp.Stop())\n\t}()\n\tdefer testutils.WaitForConnsClose(t, endp.serv)\n\n\tcl, err := smtp.Dial(\"127.0.0.1:\" + testPort)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer func() {\n\t\tassert.NoError(t, cl.Close())\n\t}()\n\n\terr = submitMsgOpts(t, cl, \"x@example.org\", []string{\"foo@E\\u0301.example.org\"}, &smtp.MailOptions{\n\t\tUTF8: true,\n\t}, testMsg)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tif len(tgt.Messages) != 1 {\n\t\tt.Fatal(\"Expected a message, got\", len(tgt.Messages))\n\t}\n\tmsg := tgt.Messages[0]\n\ttestutils.CheckMsgID(t, &msg, \"x@example.org\", []string{\"foo@é.example.org\"}, \"\")\n}\n\nfunc TestSMTPUTF8_NoMangleStatusMessage(t *testing.T) {\n\ttgt := testutils.Target{}\n\tendp := testEndpoint(t, \"smtp\", nil, &tgt, []module.Check{\n\t\t&testutils.Check{\n\t\t\tConnRes: module.CheckResult{\n\t\t\t\tReason: &exterrors.SMTPError{\n\t\t\t\t\tCode:    523,\n\t\t\t\t\tMessage: \"Hey 凱凱\",\n\t\t\t\t},\n\t\t\t\tReject: true,\n\t\t\t},\n\t\t},\n\t}, nil)\n\tendp.deferServerReject = false\n\tdefer func() {\n\t\tassert.NoError(t, endp.Stop())\n\t}()\n\tdefer testutils.WaitForConnsClose(t, endp.serv)\n\n\tcl, err := smtp.Dial(\"127.0.0.1:\" + testPort)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer func() {\n\t\tassert.NoError(t, cl.Close())\n\t}()\n\n\terr = cl.Mail(\"sender@example.org\", &smtp.MailOptions{\n\t\tUTF8: true,\n\t})\n\tif err == nil {\n\t\tt.Fatal(\"Expected an error, got none\")\n\t}\n\tsmtpErr, ok := err.(*smtp.SMTPError)\n\tif !ok {\n\t\tt.Fatal(\"Non-SMTPError returned\")\n\t}\n\n\tif smtpErr.Code != 523 {\n\t\tt.Fatal(\"Wrong SMTP code:\", smtpErr.Code)\n\t}\n\tif !strings.HasPrefix(smtpErr.Message, \"Hey 凱凱\") {\n\t\tt.Fatal(\"Wrong SMTP message:\", smtpErr.Message)\n\t}\n}\n\nfunc TestSMTPUTF8_Received_EHLO_ALabel(t *testing.T) {\n\ttgt := testutils.Target{}\n\tendp := testEndpoint(t, \"smtp\", nil, &tgt, nil, nil)\n\tdefer func() {\n\t\tassert.NoError(t, endp.Stop())\n\t}()\n\tdefer testutils.WaitForConnsClose(t, endp.serv)\n\n\tcl, err := smtp.Dial(\"127.0.0.1:\" + testPort)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer func() {\n\t\tassert.NoError(t, cl.Close())\n\t}()\n\n\tif err := cl.Hello(\"凱凱.invalid\"); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\terr = submitMsg(t, cl, \"sender@example.org\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"}, testMsg)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tif len(tgt.Messages) != 1 {\n\t\tt.Fatal(\"Expected a message, got\", len(tgt.Messages))\n\t}\n\tmsg := tgt.Messages[0]\n\tmsgID := testutils.CheckMsgID(t, &msg, \"sender@example.org\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"}, \"\")\n\n\treceivedPrefix := `from xn--y9qa.invalid (mx.example.org [127.0.0.1]) by mx.example.com (envelope-sender <sender@example.org>) with ESMTP id ` + msgID\n\n\tif !strings.HasPrefix(msg.Header.Get(\"Received\"), receivedPrefix) {\n\t\tt.Error(\"Wrong Received contents:\", msg.Header.Get(\"Received\"))\n\t}\n}\n\nfunc TestSMTPUTF8_Received_rDNS_ALabel(t *testing.T) {\n\ttgt := testutils.Target{}\n\tendp := testEndpoint(t, \"smtp\", nil, &tgt, nil, nil)\n\tdefer func() {\n\t\tassert.NoError(t, endp.Stop())\n\t}()\n\tdefer testutils.WaitForConnsClose(t, endp.serv)\n\n\tendp.resolver.(*mockdns.Resolver).Zones[\"1.0.0.127.in-addr.arpa.\"] = mockdns.Zone{\n\t\tPTR: []string{\"凱凱.invalid.\"},\n\t}\n\n\tcl, err := smtp.Dial(\"127.0.0.1:\" + testPort)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer func() {\n\t\tassert.NoError(t, cl.Close())\n\t}()\n\n\terr = submitMsg(t, cl, \"sender@example.org\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"}, testMsg)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tif len(tgt.Messages) != 1 {\n\t\tt.Fatal(\"Expected a message, got\", len(tgt.Messages))\n\t}\n\tmsg := tgt.Messages[0]\n\tmsgID := testutils.CheckMsgID(t, &msg, \"sender@example.org\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"}, \"\")\n\n\treceivedPrefix := `from mx.example.org (xn--y9qa.invalid [127.0.0.1]) by mx.example.com (envelope-sender <sender@example.org>) with ESMTP id ` + msgID\n\n\tif !strings.HasPrefix(msg.Header.Get(\"Received\"), receivedPrefix) {\n\t\tt.Error(\"Wrong Received contents:\", msg.Header.Get(\"Received\"))\n\t}\n}\n\nfunc TestSMTPUTF8_Received_rDNS_ULabel(t *testing.T) {\n\ttgt := testutils.Target{}\n\tendp := testEndpoint(t, \"smtp\", nil, &tgt, nil, nil)\n\tdefer func() {\n\t\tassert.NoError(t, endp.Stop())\n\t}()\n\tdefer testutils.WaitForConnsClose(t, endp.serv)\n\n\tendp.resolver.(*mockdns.Resolver).Zones[\"1.0.0.127.in-addr.arpa.\"] = mockdns.Zone{\n\t\tPTR: []string{\"凱凱.invalid.\"},\n\t}\n\n\tcl, err := smtp.Dial(\"127.0.0.1:\" + testPort)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer func() {\n\t\tassert.NoError(t, cl.Close())\n\t}()\n\n\terr = submitMsgOpts(t, cl, \"sender@example.org\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"}, &smtp.MailOptions{\n\t\tUTF8: true,\n\t}, testMsg)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tif len(tgt.Messages) != 1 {\n\t\tt.Fatal(\"Expected a message, got\", len(tgt.Messages))\n\t}\n\tmsg := tgt.Messages[0]\n\tmsgID := testutils.CheckMsgID(t, &msg, \"sender@example.org\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"}, \"\")\n\n\treceivedPrefix := `from mx.example.org (凱凱.invalid [127.0.0.1]) by mx.example.com (envelope-sender <sender@example.org>) with UTF8ESMTP id ` + msgID\n\n\tif !strings.HasPrefix(msg.Header.Get(\"Received\"), receivedPrefix) {\n\t\tt.Error(\"Wrong Received contents:\", msg.Header.Get(\"Received\"))\n\t}\n}\n\nfunc TestSMTPUTF8_Received_EHLO_ULabel(t *testing.T) {\n\ttgt := testutils.Target{}\n\tendp := testEndpoint(t, \"smtp\", nil, &tgt, nil, nil)\n\tdefer func() {\n\t\tassert.NoError(t, endp.Stop())\n\t}()\n\tdefer testutils.WaitForConnsClose(t, endp.serv)\n\n\tcl, err := smtp.Dial(\"127.0.0.1:\" + testPort)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer func() {\n\t\tassert.NoError(t, cl.Close())\n\t}()\n\n\tif err := cl.Hello(\"凱凱.invalid\"); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\terr = submitMsgOpts(t, cl, \"sender@example.org\", []string{\"rcpt@example.com\"}, &smtp.MailOptions{\n\t\tUTF8: true,\n\t}, testMsg)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tif len(tgt.Messages) != 1 {\n\t\tt.Fatal(\"Expected a message, got\", len(tgt.Messages))\n\t}\n\tmsg := tgt.Messages[0]\n\tmsgID := testutils.CheckMsgID(t, &msg, \"sender@example.org\", []string{\"rcpt@example.com\"}, \"\")\n\n\t// Also, 'with UTF8ESMTP'.\n\treceivedPrefix := `from 凱凱.invalid (mx.example.org [127.0.0.1]) by mx.example.com (envelope-sender <sender@example.org>) with UTF8ESMTP id ` + msgID\n\n\tif !strings.HasPrefix(msg.Header.Get(\"Received\"), receivedPrefix) {\n\t\tt.Error(\"Wrong Received contents:\", msg.Header.Get(\"Received\"))\n\t}\n}\n"
  },
  {
    "path": "internal/endpoint/smtp/submission.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage smtp\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/mail\"\n\t\"time\"\n\n\t\"github.com/emersion/go-message/textproto\"\n\t\"github.com/foxcpp/maddy/framework/exterrors\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/google/uuid\"\n)\n\nvar (\n\tmsgIDField = func() (string, error) {\n\t\tid, err := uuid.NewRandom()\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\treturn id.String(), nil\n\t}\n\n\tnow = time.Now\n)\n\nfunc (s *Session) submissionPrepare(msgMeta *module.MsgMetadata, header *textproto.Header) error {\n\tmsgMeta.DontTraceSender = true\n\n\tif header.Get(\"Message-ID\") == \"\" {\n\t\tmsgId, err := msgIDField()\n\t\tif err != nil {\n\t\t\treturn errors.New(\"Message-ID generation failed\")\n\t\t}\n\t\ts.log.Msg(\"adding missing Message-ID\")\n\t\theader.Set(\"Message-ID\", \"<\"+msgId+\"@\"+s.endp.serv.Domain+\">\")\n\t}\n\n\tif header.Get(\"From\") == \"\" {\n\t\treturn &exterrors.SMTPError{\n\t\t\tCode:         554,\n\t\t\tEnhancedCode: exterrors.EnhancedCode{5, 6, 0},\n\t\t\tMessage:      \"Message does not contains a From header field\",\n\t\t\tMisc: map[string]interface{}{\n\t\t\t\t\"modifier\": \"submission_prepare\",\n\t\t\t},\n\t\t}\n\t}\n\n\tfor _, hdr := range [...]string{\"Sender\"} {\n\t\tif value := header.Get(hdr); value != \"\" {\n\t\t\tif _, err := mail.ParseAddress(value); err != nil {\n\t\t\t\treturn &exterrors.SMTPError{\n\t\t\t\t\tCode:         554,\n\t\t\t\t\tEnhancedCode: exterrors.EnhancedCode{5, 6, 0},\n\t\t\t\t\tMessage:      fmt.Sprintf(\"Invalid address in %s\", hdr),\n\t\t\t\t\tMisc: map[string]interface{}{\n\t\t\t\t\t\t\"modifier\": \"submission_prepare\",\n\t\t\t\t\t\t\"addr\":     value,\n\t\t\t\t\t},\n\t\t\t\t\tErr: err,\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tfor _, hdr := range [...]string{\"To\", \"Cc\", \"Bcc\", \"Reply-To\"} {\n\t\tif value := header.Get(hdr); value != \"\" {\n\t\t\tif _, err := mail.ParseAddressList(value); err != nil {\n\t\t\t\treturn &exterrors.SMTPError{\n\t\t\t\t\tCode:         554,\n\t\t\t\t\tEnhancedCode: exterrors.EnhancedCode{5, 6, 0},\n\t\t\t\t\tMessage:      fmt.Sprintf(\"Invalid address in %s\", hdr),\n\t\t\t\t\tMisc: map[string]interface{}{\n\t\t\t\t\t\t\"modifier\": \"submission_prepare\",\n\t\t\t\t\t\t\"addr\":     value,\n\t\t\t\t\t},\n\t\t\t\t\tErr: err,\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\taddrs, err := mail.ParseAddressList(header.Get(\"From\"))\n\tif err != nil {\n\t\treturn &exterrors.SMTPError{\n\t\t\tCode:         554,\n\t\t\tEnhancedCode: exterrors.EnhancedCode{5, 6, 0},\n\t\t\tMessage:      \"Invalid address in From\",\n\t\t\tMisc: map[string]interface{}{\n\t\t\t\t\"modifier\": \"submission_prepare\",\n\t\t\t\t\"addr\":     header.Get(\"From\"),\n\t\t\t},\n\t\t\tErr: err,\n\t\t}\n\t}\n\n\t// https://tools.ietf.org/html/rfc5322#section-3.6.2\n\t// If From contains multiple addresses, Sender field must be present.\n\tif len(addrs) > 1 && header.Get(\"Sender\") == \"\" {\n\t\treturn &exterrors.SMTPError{\n\t\t\tCode:         554,\n\t\t\tEnhancedCode: exterrors.EnhancedCode{5, 6, 0},\n\t\t\tMessage:      \"Missing Sender header field\",\n\t\t\tMisc: map[string]interface{}{\n\t\t\t\t\"modifier\": \"submission_prepare\",\n\t\t\t\t\"from\":     header.Get(\"From\"),\n\t\t\t},\n\t\t}\n\t}\n\n\tif dateHdr := header.Get(\"Date\"); dateHdr != \"\" {\n\t\t_, err := parseMessageDateTime(dateHdr)\n\t\tif err != nil {\n\t\t\treturn &exterrors.SMTPError{\n\t\t\t\tCode:    554,\n\t\t\t\tMessage: \"Malformed Date header\",\n\t\t\t\tMisc: map[string]interface{}{\n\t\t\t\t\t\"modifier\": \"submission_prepare\",\n\t\t\t\t\t\"date\":     dateHdr,\n\t\t\t\t},\n\t\t\t\tErr: err,\n\t\t\t}\n\t\t}\n\t} else {\n\t\ts.log.Msg(\"adding missing Date header\")\n\t\theader.Set(\"Date\", now().UTC().Format(\"Mon, 2 Jan 2006 15:04:05 -0700\"))\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/endpoint/smtp/submission_test.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage smtp\n\nimport (\n\t\"reflect\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/emersion/go-message/textproto\"\n\t\"github.com/emersion/go-smtp\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc init() {\n\tmsgIDField = func() (string, error) {\n\t\treturn \"A\", nil\n\t}\n\n\tnow = func() time.Time {\n\t\treturn time.Unix(0, 0)\n\t}\n}\n\nfunc TestSubmissionPrepare(t *testing.T) {\n\ttest := func(hdrMap, expectedMap map[string][]string) {\n\t\tt.Helper()\n\n\t\thdr := textproto.Header{}\n\t\tfor k, v := range hdrMap {\n\t\t\tfor _, field := range v {\n\t\t\t\thdr.Add(k, field)\n\t\t\t}\n\t\t}\n\n\t\tendp := testEndpoint(t, \"submission\", &modules.Dummy{}, &modules.Dummy{}, nil, nil)\n\t\tdefer func() {\n\t\t\t// Synchronize the endpoint initialization.\n\t\t\t// Otherwise Close will race with Serve called by setupListeners.\n\t\t\tcl, _ := smtp.Dial(\"127.0.0.1:\" + testPort)\n\t\t\tassert.NoError(t, cl.Close())\n\n\t\t\tassert.NoError(t, endp.Stop())\n\t\t}()\n\n\t\tsession, err := endp.NewSession(nil)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\terr = session.(*Session).submissionPrepare(&module.MsgMetadata{}, &hdr)\n\t\tif expectedMap == nil {\n\t\t\tif err == nil {\n\t\t\t\tt.Error(\"Expected an error, got none\")\n\t\t\t}\n\t\t\tt.Log(err)\n\t\t\treturn\n\t\t}\n\t\tif expectedMap != nil && err != nil {\n\t\t\tt.Error(\"Unexpected error:\", err)\n\t\t\treturn\n\t\t}\n\n\t\tresMap := make(map[string][]string)\n\t\tfor field := hdr.Fields(); field.Next(); {\n\t\t\tresMap[field.Key()] = append(resMap[field.Key()], field.Value())\n\t\t}\n\n\t\tif !reflect.DeepEqual(expectedMap, resMap) {\n\t\t\tt.Errorf(\"wrong header result\\nwant %#+v\\ngot  %#+v\", expectedMap, resMap)\n\t\t}\n\t}\n\n\t// No From field.\n\ttest(map[string][]string{}, nil)\n\n\t// Malformed From field.\n\ttest(map[string][]string{\n\t\t\"From\": {\"<hello@example.org>, \\\"\\\"\"},\n\t}, nil)\n\ttest(map[string][]string{\n\t\t\"From\": {\" adasda\"},\n\t}, nil)\n\n\t// Malformed Reply-To.\n\ttest(map[string][]string{\n\t\t\"From\":     {\"<hello@example.org>\"},\n\t\t\"Reply-To\": {\"<hello@example.org>, \\\"\\\"\"},\n\t}, nil)\n\n\t// Malformed CC.\n\ttest(map[string][]string{\n\t\t\"From\":     {\"<hello@example.org>\"},\n\t\t\"Reply-To\": {\"<hello@example.org>\"},\n\t\t\"Cc\":       {\"<hello@example.org>, \\\"\\\"\"},\n\t}, nil)\n\n\t// Malformed Sender.\n\ttest(map[string][]string{\n\t\t\"From\":     {\"<hello@example.org>\"},\n\t\t\"Reply-To\": {\"<hello@example.org>\"},\n\t\t\"Cc\":       {\"<hello@example.org>\"},\n\t\t\"Sender\":   {\"<hello@example.org> asd\"},\n\t}, nil)\n\n\t// Multiple From + no Sender.\n\ttest(map[string][]string{\n\t\t\"From\": {\"<hello@example.org>, <hello2@example.org>\"},\n\t}, nil)\n\n\t// Multiple From + valid Sender.\n\ttest(map[string][]string{\n\t\t\"From\":       {\"<hello@example.org>, <hello2@example.org>\"},\n\t\t\"Sender\":     {\"<hello@example.org>\"},\n\t\t\"Date\":       {\"Fri, 22 Nov 2019 20:51:31 +0800\"},\n\t\t\"Message-Id\": {\"<foobar@example.org>\"},\n\t}, map[string][]string{\n\t\t\"From\":       {\"<hello@example.org>, <hello2@example.org>\"},\n\t\t\"Sender\":     {\"<hello@example.org>\"},\n\t\t\"Date\":       {\"Fri, 22 Nov 2019 20:51:31 +0800\"},\n\t\t\"Message-Id\": {\"<foobar@example.org>\"},\n\t})\n\n\t// Add missing Message-Id.\n\ttest(map[string][]string{\n\t\t\"From\": {\"<hello@example.org>\"},\n\t\t\"Date\": {\"Fri, 22 Nov 2019 20:51:31 +0800\"},\n\t}, map[string][]string{\n\t\t\"From\":       {\"<hello@example.org>\"},\n\t\t\"Date\":       {\"Fri, 22 Nov 2019 20:51:31 +0800\"},\n\t\t\"Message-Id\": {\"<A@mx.example.com>\"},\n\t})\n\n\t// Malformed Date.\n\ttest(map[string][]string{\n\t\t\"From\": {\"<hello@example.org>\"},\n\t\t\"Date\": {\"not a date\"},\n\t}, nil)\n\n\t// Add missing Date.\n\ttest(map[string][]string{\n\t\t\"From\":       {\"<hello@example.org>\"},\n\t\t\"Message-Id\": {\"<A@mx.example.org>\"},\n\t}, map[string][]string{\n\t\t\"From\":       {\"<hello@example.org>\"},\n\t\t\"Message-Id\": {\"<A@mx.example.org>\"},\n\t\t\"Date\":       {\"Thu, 1 Jan 1970 00:00:00 +0000\"},\n\t})\n}\n"
  },
  {
    "path": "internal/imap_filter/command/command.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage command\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"os\"\n\t\"os/exec\"\n\t\"regexp\"\n\n\t\"github.com/emersion/go-message/textproto\"\n\t\"github.com/foxcpp/maddy/framework/buffer\"\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/log\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n)\n\nconst modName = \"imap.filter.command\"\n\nvar placeholderRe = regexp.MustCompile(`{[a-zA-Z0-9_]+?}`)\n\ntype Check struct {\n\tinstName string\n\tlog      *log.Logger\n\n\tcmd     string\n\tcmdArgs []string\n}\n\nfunc (c *Check) IMAPFilter(accountName string, rcptTo string, msgMeta *module.MsgMetadata, hdr textproto.Header, body buffer.Buffer) (folder string, flags []string, err error) {\n\tcmd, args := c.expandCommand(msgMeta, accountName, rcptTo, hdr)\n\n\tvar buf bytes.Buffer\n\t_ = textproto.WriteHeader(&buf, hdr)\n\tbR, err := body.Open()\n\tif err != nil {\n\t\treturn \"\", nil, err\n\t}\n\n\treturn c.run(cmd, args, io.MultiReader(bytes.NewReader(buf.Bytes()), bR))\n}\n\nfunc New(c *container.C, _, instName string) (module.Module, error) {\n\tchk := &Check{\n\t\tinstName: instName,\n\t\tlog:      c.DefaultLogger.Sublogger(modName),\n\t}\n\n\treturn chk, nil\n}\n\nfunc (c *Check) Name() string {\n\treturn modName\n}\n\nfunc (c *Check) InstanceName() string {\n\treturn c.instName\n}\n\nfunc (c *Check) Configure(inlineArgs []string, cfg *config.Map) error {\n\tif len(inlineArgs) == 0 {\n\t\treturn errors.New(\"command: at least one argument is required (command name)\")\n\t}\n\n\tc.cmd = inlineArgs[0]\n\tc.cmdArgs = inlineArgs[1:]\n\n\t// Check whether the inline argument command is usable.\n\tif _, err := exec.LookPath(c.cmd); err != nil {\n\t\treturn fmt.Errorf(\"command: %w\", err)\n\t}\n\n\t_, err := cfg.Process()\n\treturn err\n}\n\nfunc (c *Check) expandCommand(msgMeta *module.MsgMetadata, accountName string, rcptTo string, hdr textproto.Header) (string, []string) {\n\texpArgs := make([]string, len(c.cmdArgs))\n\n\tfor i, arg := range c.cmdArgs {\n\t\texpArgs[i] = placeholderRe.ReplaceAllStringFunc(arg, func(placeholder string) string {\n\t\t\tswitch placeholder {\n\t\t\tcase \"{auth_user}\":\n\t\t\t\tif msgMeta.Conn == nil {\n\t\t\t\t\treturn \"\"\n\t\t\t\t}\n\t\t\t\treturn msgMeta.Conn.AuthUser\n\t\t\tcase \"{source_ip}\":\n\t\t\t\tif msgMeta.Conn == nil {\n\t\t\t\t\treturn \"\"\n\t\t\t\t}\n\t\t\t\ttcpAddr, _ := msgMeta.Conn.RemoteAddr.(*net.TCPAddr)\n\t\t\t\tif tcpAddr == nil {\n\t\t\t\t\treturn \"\"\n\t\t\t\t}\n\t\t\t\treturn tcpAddr.IP.String()\n\t\t\tcase \"{source_host}\":\n\t\t\t\tif msgMeta.Conn == nil {\n\t\t\t\t\treturn \"\"\n\t\t\t\t}\n\t\t\t\treturn msgMeta.Conn.Hostname\n\t\t\tcase \"{source_rdns}\":\n\t\t\t\tif msgMeta.Conn == nil {\n\t\t\t\t\treturn \"\"\n\t\t\t\t}\n\t\t\t\tvalI, err := msgMeta.Conn.RDNSName.Get()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn \"\"\n\t\t\t\t}\n\t\t\t\tif valI == nil {\n\t\t\t\t\treturn \"\"\n\t\t\t\t}\n\t\t\t\treturn valI.(string)\n\t\t\tcase \"{msg_id}\":\n\t\t\t\treturn msgMeta.ID\n\t\t\tcase \"{sender}\":\n\t\t\t\treturn msgMeta.OriginalFrom\n\t\t\tcase \"{rcpt_to}\":\n\t\t\t\treturn rcptTo\n\t\t\tcase \"{original_rcpt_to}\":\n\t\t\t\toldestOriginalRcpt := rcptTo\n\t\t\t\tfor originalRcpt, ok := rcptTo, true; ok; originalRcpt, ok = msgMeta.OriginalRcpts[originalRcpt] {\n\t\t\t\t\toldestOriginalRcpt = originalRcpt\n\t\t\t\t}\n\t\t\t\treturn oldestOriginalRcpt\n\t\t\tcase \"{subject}\":\n\t\t\t\treturn hdr.Get(\"Subject\")\n\t\t\tcase \"{account_name}\":\n\t\t\t\treturn accountName\n\t\t\t}\n\t\t\treturn placeholder\n\t\t})\n\t}\n\n\treturn c.cmd, expArgs\n}\n\nfunc (c *Check) run(cmdName string, args []string, stdin io.Reader) (string, []string, error) {\n\tc.log.Debugln(\"running\", cmdName, args)\n\n\tcmd := exec.Command(cmdName, args...)\n\tcmd.Stdin = stdin\n\tstdout, err := cmd.StdoutPipe()\n\tif err != nil {\n\t\treturn \"\", nil, err\n\t}\n\n\tif err := cmd.Start(); err != nil {\n\t\treturn \"\", nil, err\n\t}\n\n\tscnr := bufio.NewScanner(stdout)\n\tvar (\n\t\tfolder string\n\t\tflags  []string\n\t)\n\tif scnr.Scan() {\n\t\tfolder = scnr.Text()\n\t}\n\tfor scnr.Scan() {\n\t\tflags = append(flags, scnr.Text())\n\t}\n\tif err := scnr.Err(); err != nil {\n\t\treturn \"\", nil, err\n\t}\n\n\terr = cmd.Wait()\n\tif err != nil {\n\t\tif _, ok := err.(*exec.ExitError); !ok {\n\t\t\t// If that's not ExitError, the process may still be running. We do\n\t\t\t// not want this.\n\t\t\tif err := cmd.Process.Signal(os.Interrupt); err != nil {\n\t\t\t\tc.log.Error(\"failed to kill process\", err)\n\t\t\t}\n\t\t}\n\t\treturn \"\", nil, err\n\t}\n\n\tc.log.Debugf(\"folder: %s, extra flags: %v\", folder, flags)\n\n\treturn folder, flags, nil\n}\n\nfunc init() {\n\tmodules.Register(modName, New)\n}\n"
  },
  {
    "path": "internal/imap_filter/group.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage imap_filter\n\nimport (\n\t\"github.com/emersion/go-message/textproto\"\n\t\"github.com/foxcpp/maddy/framework/buffer\"\n\t\"github.com/foxcpp/maddy/framework/config\"\n\tmodconfig \"github.com/foxcpp/maddy/framework/config/module\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/log\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n)\n\n// Group wraps multiple modifiers and runs them serially.\n//\n// It is also registered as a module under 'modifiers' name and acts as a\n// module group.\ntype Group struct {\n\tinstName string\n\tFilters  []module.IMAPFilter\n\tlog      *log.Logger\n}\n\nfunc NewGroup(c *container.C, modName, instName string) (module.Module, error) {\n\treturn &Group{\n\t\tinstName: instName,\n\t\tlog:      c.DefaultLogger.Sublogger(modName),\n\t}, nil\n}\n\nfunc (g *Group) IMAPFilter(accountName string, rcptTo string, meta *module.MsgMetadata, hdr textproto.Header, body buffer.Buffer) (folder string, flags []string, err error) {\n\tif g == nil {\n\t\treturn \"\", nil, nil\n\t}\n\tvar (\n\t\tfinalFolder string\n\t\tfinalFlags  = make([]string, 0, len(g.Filters))\n\t)\n\tfor _, f := range g.Filters {\n\t\tfolder, flags, err := f.IMAPFilter(accountName, rcptTo, meta, hdr, body)\n\t\tif err != nil {\n\t\t\tg.log.Error(\"IMAP filter failed\", err)\n\t\t\tcontinue\n\t\t}\n\t\tif folder != \"\" && finalFolder == \"\" {\n\t\t\tfinalFolder = folder\n\t\t}\n\t\tfinalFlags = append(finalFlags, flags...)\n\t}\n\treturn finalFolder, finalFlags, nil\n}\n\nfunc (g *Group) Configure(inlineArgs []string, cfg *config.Map) error {\n\tfor _, node := range cfg.Block.Children {\n\t\tmod, err := modconfig.IMAPFilter(cfg.Globals, append([]string{node.Name}, node.Args...), node)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tg.Filters = append(g.Filters, mod)\n\t}\n\n\treturn nil\n}\n\nfunc (g *Group) Name() string {\n\treturn \"modifiers\"\n}\n\nfunc (g *Group) InstanceName() string {\n\treturn g.instName\n}\n\nfunc init() {\n\tmodules.Register(\"imap_filters\", NewGroup)\n}\n"
  },
  {
    "path": "internal/libdns/acmedns.go",
    "content": "//go:build libdns_acmedns || libdns_all\n// +build libdns_acmedns libdns_all\n\npackage libdns\n\nimport (\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n\t\"github.com/libdns/acmedns\"\n)\n\nfunc init() {\n\tmodules.Register(\"libdns.acmedns\", func(c *container.C, modName, instName string) (module.Module, error) {\n\t\tp := acmedns.Provider{}\n\t\treturn &ProviderModule{\n\t\t\tRecordDeleter:  &p,\n\t\t\tRecordAppender: &p,\n\t\t\tsetConfig: func(c *config.Map) {\n\t\t\t\tc.String(\"username\", false, true, \"\", &p.Username)\n\t\t\t\tc.String(\"password\", false, true, \"\", &p.Password)\n\t\t\t\tc.String(\"subdomain\", false, true, \"\", &p.Subdomain)\n\t\t\t\tc.String(\"server_url\", false, true, \"\", &p.ServerURL)\n\t\t\t},\n\t\t\tinstName: instName,\n\t\t\tmodName:  modName,\n\t\t}, nil\n\t})\n}\n"
  },
  {
    "path": "internal/libdns/alidns.go",
    "content": "//go:build libdns_alidns || libdns_all\n// +build libdns_alidns libdns_all\n\npackage libdns\n\nimport (\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n\t\"github.com/libdns/alidns\"\n)\n\nfunc init() {\n\tmodules.Register(\"libdns.alidns\", func(c *container.C, modName, instName string) (module.Module, error) {\n\t\tp := alidns.Provider{}\n\t\treturn &ProviderModule{\n\t\t\tRecordDeleter:  &p,\n\t\t\tRecordAppender: &p,\n\t\t\tsetConfig: func(c *config.Map) {\n\t\t\t\tc.String(\"key_id\", false, false, \"\", &p.AccKeyID)\n\t\t\t\tc.String(\"key_secret\", false, false, \"\", &p.AccKeySecret)\n\t\t\t},\n\t\t\tinstName: instName,\n\t\t\tmodName:  modName,\n\t\t}, nil\n\t})\n}\n"
  },
  {
    "path": "internal/libdns/cloudflare.go",
    "content": "//go:build libdns_cloudflare || !libdns_separate\n// +build libdns_cloudflare !libdns_separate\n\npackage libdns\n\nimport (\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n\t\"github.com/libdns/cloudflare\"\n)\n\nfunc init() {\n\tmodules.Register(\"libdns.cloudflare\", func(c *container.C, modName, instName string) (module.Module, error) {\n\t\tp := cloudflare.Provider{}\n\t\treturn &ProviderModule{\n\t\t\tRecordDeleter:  &p,\n\t\t\tRecordAppender: &p,\n\t\t\tsetConfig: func(c *config.Map) {\n\t\t\t\tc.String(\"api_token\", false, false, \"\", &p.APIToken)\n\t\t\t},\n\t\t\tinstName: instName,\n\t\t\tmodName:  modName,\n\t\t}, nil\n\t})\n}\n"
  },
  {
    "path": "internal/libdns/digitalocean.go",
    "content": "//go:build libdns_digitalocean || !libdns_separate\n// +build libdns_digitalocean !libdns_separate\n\npackage libdns\n\nimport (\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n\t\"github.com/libdns/digitalocean\"\n)\n\nfunc init() {\n\tmodules.Register(\"libdns.digitalocean\", func(c *container.C, modName, instName string) (module.Module, error) {\n\t\tp := digitalocean.Provider{}\n\t\treturn &ProviderModule{\n\t\t\tRecordDeleter:  &p,\n\t\t\tRecordAppender: &p,\n\t\t\tsetConfig: func(c *config.Map) {\n\t\t\t\tc.String(\"api_token\", false, false, \"\", &p.APIToken)\n\t\t\t},\n\t\t\tinstName: instName,\n\t\t\tmodName:  modName,\n\t\t}, nil\n\t})\n}\n"
  },
  {
    "path": "internal/libdns/gandi.go",
    "content": "//go:build libdns_gandi || !libdns_separate\n// +build libdns_gandi !libdns_separate\n\npackage libdns\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/log\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n\t\"github.com/libdns/gandi\"\n)\n\nfunc init() {\n\tmodules.Register(\"libdns.gandi\", func(c *container.C, modName, instName string) (module.Module, error) {\n\t\tp := gandi.Provider{}\n\t\treturn &ProviderModule{\n\t\t\tRecordDeleter:  &p,\n\t\t\tRecordAppender: &p,\n\t\t\tsetConfig: func(c *config.Map) {\n\t\t\t\tc.String(\"api_token\", false, false, \"\", &p.APIToken)\n\t\t\t\tc.String(\"personal_token\", false, false, \"\", &p.BearerToken)\n\t\t\t},\n\t\t\tafterConfig: func() error {\n\t\t\t\tif p.APIToken != \"\" {\n\t\t\t\t\tlog.Println(\"libdns.gandi: api_token is deprecated, use personal_token instead (https://api.gandi.net/docs/authentication/)\")\n\t\t\t\t}\n\t\t\t\tif p.APIToken == \"\" && p.BearerToken == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"libdns.gandi: either api_token or personal_token should be specified\")\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t},\n\t\t\tinstName: instName,\n\t\t\tmodName:  modName,\n\t\t}, nil\n\t})\n}\n"
  },
  {
    "path": "internal/libdns/gcore.go",
    "content": "//go:build libdns_gcore || !libdns_separate\n\npackage libdns\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n\t\"github.com/libdns/gcore\"\n)\n\nfunc init() {\n\tmodules.Register(\"libdns.gcore\", func(c *container.C, modName, instName string) (module.Module, error) {\n\t\tp := gcore.Provider{}\n\t\treturn &ProviderModule{\n\t\t\tRecordDeleter:  &p,\n\t\t\tRecordAppender: &p,\n\t\t\tsetConfig: func(c *config.Map) {\n\t\t\t\tc.String(\"api_key\", false, false, \"\", &p.APIKey)\n\t\t\t},\n\t\t\tafterConfig: func() error {\n\t\t\t\tif p.APIKey == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"libdns.gcore: api_key should be specified\")\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t},\n\n\t\t\tinstName: instName,\n\t\t\tmodName:  modName,\n\t\t}, nil\n\t})\n}\n"
  },
  {
    "path": "internal/libdns/googleclouddns.go",
    "content": "//go:build libdns_googleclouddns || libdns_all\n// +build libdns_googleclouddns libdns_all\n\npackage libdns\n\nimport (\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n\t\"github.com/libdns/googleclouddns\"\n)\n\nfunc init() {\n\tmodules.Register(\"libdns.googleclouddns\", func(c *container.C, modName, instName string) (module.Module, error) {\n\t\tp := googleclouddns.Provider{}\n\t\treturn &ProviderModule{\n\t\t\tRecordDeleter:  &p,\n\t\t\tRecordAppender: &p,\n\t\t\tsetConfig: func(c *config.Map) {\n\t\t\t\tc.String(\"project\", false, true, \"\", &p.Project)\n\t\t\t\tc.String(\"service_account_json\", false, false, \"\", &p.ServiceAccountJSON)\n\t\t\t},\n\t\t\tinstName: instName,\n\t\t\tmodName:  modName,\n\t\t}, nil\n\t})\n}\n"
  },
  {
    "path": "internal/libdns/hetzner.go",
    "content": "//go:build libdns_hetzner || !libdns_separate\n// +build libdns_hetzner !libdns_separate\n\npackage libdns\n\nimport (\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/log\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n\t\"github.com/libdns/hetzner\"\n)\n\nfunc init() {\n\tmodules.Register(\"libdns.hetzner\", func(c *container.C, modName, instName string) (module.Module, error) {\n\t\tp := hetzner.Provider{}\n\t\treturn &ProviderModule{\n\t\t\tRecordDeleter:  &p,\n\t\t\tRecordAppender: &p,\n\t\t\tsetConfig: func(c *config.Map) {\n\t\t\t\tlog.DefaultLogger.Println(\"WARNING: maddy 0.10.0 will require new DNS API, see https://github.com/foxcpp/maddy/issues/807 for details\")\n\t\t\t\tc.String(\"api_token\", false, false, \"\", &p.AuthAPIToken)\n\t\t\t},\n\t\t\tinstName: instName,\n\t\t\tmodName:  modName,\n\t\t}, nil\n\t})\n}\n"
  },
  {
    "path": "internal/libdns/leaseweb.go",
    "content": "//go:build libdns_leaseweb || libdns_all\n// +build libdns_leaseweb libdns_all\n\npackage libdns\n\nimport (\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/log\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n\t\"github.com/libdns/leaseweb\"\n)\n\nfunc init() {\n\tmodules.Register(\"libdns.leaseweb\", func(c *container.C, modName, instName string) (module.Module, error) {\n\t\tp := leaseweb.Provider{}\n\t\treturn &ProviderModule{\n\t\t\tRecordDeleter:  &p,\n\t\t\tRecordAppender: &p,\n\t\t\tsetConfig: func(c *config.Map) {\n\t\t\t\tlog.DefaultLogger.Println(\"WARNING: maddy 0.10.0 will drop libdns.leaseweb, see https://github.com/foxcpp/maddy/issues/807 for details\")\n\t\t\t\tc.String(\"api_key\", false, false, \"\", &p.APIKey)\n\t\t\t},\n\t\t\tinstName: instName,\n\t\t\tmodName:  modName,\n\t\t}, nil\n\t})\n}\n"
  },
  {
    "path": "internal/libdns/metaname.go",
    "content": "//go:build libdns_metaname || libdns_all\n// +build libdns_metaname libdns_all\n\npackage libdns\n\nimport (\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n\t\"github.com/libdns/metaname\"\n)\n\nfunc init() {\n\tmodules.Register(\"libdns.metaname\", func(c *container.C, modName, instName string) (module.Module, error) {\n\t\tp := metaname.Provider{\n\t\t\tEndpoint: \"https://metaname.net/api/1.1\",\n\t\t}\n\t\treturn &ProviderModule{\n\t\t\tRecordDeleter:  &p,\n\t\t\tRecordAppender: &p,\n\t\t\tsetConfig: func(c *config.Map) {\n\t\t\t\tc.String(\"api_key\", false, false, \"\", &p.APIKey)\n\t\t\t\tc.String(\"account_ref\", false, false, \"\", &p.AccountReference)\n\t\t\t},\n\t\t\tinstName: instName,\n\t\t\tmodName:  modName,\n\t\t}, nil\n\t})\n}\n"
  },
  {
    "path": "internal/libdns/namecheap.go",
    "content": "//go:build go1.16\n// +build go1.16\n\npackage libdns\n\nimport (\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n\t\"github.com/libdns/namecheap\"\n)\n\nfunc init() {\n\tmodules.Register(\"libdns.namecheap\", func(c *container.C, modName, instName string) (module.Module, error) {\n\t\tp := namecheap.Provider{}\n\t\treturn &ProviderModule{\n\t\t\tRecordDeleter:  &p,\n\t\t\tRecordAppender: &p,\n\t\t\tsetConfig: func(c *config.Map) {\n\t\t\t\tc.String(\"api_key\", false, true, \"\", &p.APIKey)\n\t\t\t\tc.String(\"api_username\", false, true, \"\", &p.User)\n\t\t\t\tc.String(\"endpoint\", false, false, \"\", &p.APIEndpoint)\n\t\t\t\tc.String(\"client_ip\", false, false, \"\", &p.ClientIP)\n\t\t\t},\n\t\t\tinstName: instName,\n\t\t\tmodName:  modName,\n\t\t}, nil\n\t})\n}\n"
  },
  {
    "path": "internal/libdns/namedotcom.go",
    "content": "//go:build libdns_namedotdom || libdns_all\n// +build libdns_namedotdom libdns_all\n\npackage libdns\n\nimport (\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/log\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n\t\"github.com/libdns/namedotcom\"\n)\n\nfunc init() {\n\tmodules.Register(\"libdns.namedotcom\", func(c *container.C, modName, instName string) (module.Module, error) {\n\t\tp := namedotcom.Provider{\n\t\t\tServer: \"https://api.name.com\",\n\t\t}\n\t\treturn &ProviderModule{\n\t\t\tRecordDeleter:  &p,\n\t\t\tRecordAppender: &p,\n\t\t\tsetConfig: func(c *config.Map) {\n\t\t\t\tlog.DefaultLogger.Println(\"WARNING: maddy 0.10.0 will drop libdns.namedotcom, see https://github.com/foxcpp/maddy/issues/807 for details\")\n\t\t\t\tc.String(\"user\", false, false, \"\", &p.User)\n\t\t\t\tc.String(\"token\", false, false, \"\", &p.Token)\n\t\t\t},\n\t\t\tinstName: instName,\n\t\t\tmodName:  modName,\n\t\t}, nil\n\t})\n}\n"
  },
  {
    "path": "internal/libdns/provider_module.go",
    "content": "package libdns\n\nimport (\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/libdns/libdns\"\n)\n\ntype ProviderModule struct {\n\tlibdns.RecordDeleter\n\tlibdns.RecordAppender\n\tsetConfig   func(c *config.Map)\n\tafterConfig func() error\n\n\tinstName string\n\tmodName  string\n}\n\nfunc (p *ProviderModule) Configure(inlineArgs []string, cfg *config.Map) error {\n\tp.setConfig(cfg)\n\t_, err := cfg.Process()\n\tif p.afterConfig != nil {\n\t\tif err := p.afterConfig(); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn err\n}\n\nfunc (p *ProviderModule) Name() string {\n\treturn p.modName\n}\n\nfunc (p *ProviderModule) InstanceName() string {\n\treturn p.instName\n}\n"
  },
  {
    "path": "internal/libdns/rfc2136.go",
    "content": "//go:build libdns_rfc2136 || libdns_all\n// +build libdns_rfc2136 libdns_all\n\npackage libdns\n\nimport (\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n\t\"github.com/libdns/rfc2136\"\n)\n\nfunc init() {\n\tmodules.Register(\"libdns.rfc2136\", func(c *container.C, modName, instName string) (module.Module, error) {\n\t\tp := rfc2136.Provider{}\n\t\treturn &ProviderModule{\n\t\t\tRecordDeleter:  &p,\n\t\t\tRecordAppender: &p,\n\t\t\tsetConfig: func(c *config.Map) {\n\t\t\t\tc.String(\"key_name\", false, true, \"\", &p.KeyName)\n\t\t\t\tc.String(\"key\", false, true, \"\", &p.Key)\n\t\t\t\tc.String(\"key_alg\", false, true, \"\", &p.KeyAlg)\n\t\t\t\tc.String(\"server\", false, true, \"\", &p.Server)\n\t\t\t},\n\t\t\tinstName: instName,\n\t\t\tmodName:  modName,\n\t\t}, nil\n\t})\n}\n"
  },
  {
    "path": "internal/libdns/route53.go",
    "content": "//go:build libdns_route53 || libdns_all\n// +build libdns_route53 libdns_all\n\npackage libdns\n\nimport (\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n\t\"github.com/libdns/route53\"\n)\n\nfunc init() {\n\tmodules.Register(\"libdns.route53\", func(c *container.C, modName, instName string) (module.Module, error) {\n\t\tp := route53.Provider{}\n\t\treturn &ProviderModule{\n\t\t\tRecordDeleter:  &p,\n\t\t\tRecordAppender: &p,\n\t\t\tsetConfig: func(c *config.Map) {\n\t\t\t\tc.String(\"secret_access_key\", false, false, \"\", &p.SecretAccessKey)\n\t\t\t\tc.String(\"access_key_id\", false, false, \"\", &p.AccessKeyId)\n\t\t\t},\n\t\t\tinstName: instName,\n\t\t\tmodName:  modName,\n\t\t}, nil\n\t})\n}\n"
  },
  {
    "path": "internal/libdns/vultr.go",
    "content": "//go:build libdns_vultr || !libdns_separate\n// +build libdns_vultr !libdns_separate\n\npackage libdns\n\nimport (\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/log\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n\t\"github.com/libdns/vultr\"\n)\n\nfunc init() {\n\tmodules.Register(\"libdns.vultr\", func(c *container.C, modName, instName string) (module.Module, error) {\n\t\tp := vultr.Provider{}\n\t\treturn &ProviderModule{\n\t\t\tRecordDeleter:  &p,\n\t\t\tRecordAppender: &p,\n\t\t\tsetConfig: func(c *config.Map) {\n\t\t\t\tlog.DefaultLogger.Println(\"WARNING: maddy 0.10.0 will drop libdns.vultr, see https://github.com/foxcpp/maddy/issues/807 for details\")\n\t\t\t\tc.String(\"api_token\", false, false, \"\", &p.APIToken)\n\t\t\t},\n\t\t\tinstName: instName,\n\t\t\tmodName:  modName,\n\t\t}, nil\n\t})\n}\n"
  },
  {
    "path": "internal/limits/limiters/bucket.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage limiters\n\nimport (\n\t\"context\"\n\t\"sync\"\n\t\"time\"\n)\n\n// BucketSet combines a group of Ls into a single key-indexed structure.\n// Basically, each unique key gets its own counter. The main use case for\n// BucketSet is to apply per-resource rate limiting.\n//\n// Amount of buckets is limited to a certain value. When the size of internal\n// map is around or equal to that value, next Take call will attempt to remove\n// any stale buckets from the group. If it is not possible to do so (all\n// buckets are in active use), Take will return false. Alternatively, in some\n// rare cases, some other (undefined) waiting Take can return false.\n//\n// A BucksetSet without a New function assigned is no-op: Take and TakeContext\n// always succeed and Release does nothing.\ntype BucketSet struct {\n\t// New function is used to construct underlying L instances.\n\t//\n\t// It is safe to change it only when BucketSet is not used by any\n\t// goroutine.\n\tNew func() L\n\n\t// Time after which bucket is considered stale and can be removed from the\n\t// set. For safe use with Rate limiter, it should be at least as twice as\n\t// big as Rate refill interval.\n\tReapInterval time.Duration\n\n\tMaxBuckets int\n\n\tmLck sync.Mutex\n\tm    map[string]*struct {\n\t\tr       L\n\t\tlastUse time.Time\n\t}\n}\n\nfunc NewBucketSet(new_ func() L, reapInterval time.Duration, maxBuckets int) *BucketSet {\n\treturn &BucketSet{\n\t\tNew:          new_,\n\t\tReapInterval: reapInterval,\n\t\tMaxBuckets:   maxBuckets,\n\t\tm: map[string]*struct {\n\t\t\tr       L\n\t\t\tlastUse time.Time\n\t\t}{},\n\t}\n}\n\nfunc (r *BucketSet) Close() {\n\tr.mLck.Lock()\n\tdefer r.mLck.Unlock()\n\n\tfor _, v := range r.m {\n\t\tv.r.Close()\n\t}\n}\n\nfunc (r *BucketSet) take(key string) L {\n\tr.mLck.Lock()\n\tdefer r.mLck.Unlock()\n\n\tif len(r.m) > r.MaxBuckets {\n\t\tnow := time.Now()\n\t\t// Attempt to get rid of stale buckets.\n\t\tfor k, v := range r.m {\n\t\t\tif v.lastUse.Sub(now) > r.ReapInterval {\n\t\t\t\t// Drop the bucket, if there happen to be any waiting Take for it.\n\t\t\t\t// It will return 'false', but this is fine for us since this\n\t\t\t\t// whole 'reaping' process will run only when we are under a\n\t\t\t\t// high load and dropping random requests in this case is a\n\t\t\t\t// more or less reasonable thing to do.\n\t\t\t\tv.r.Close()\n\t\t\t\tdelete(r.m, k)\n\t\t\t}\n\t\t}\n\n\t\t// Still full? E.g. all buckets are in use.\n\t\tif len(r.m) > r.MaxBuckets {\n\t\t\treturn nil\n\t\t}\n\t}\n\n\tbucket, ok := r.m[key]\n\tif !ok {\n\t\tr.m[key] = &struct {\n\t\t\tr       L\n\t\t\tlastUse time.Time\n\t\t}{\n\t\t\tr:       r.New(),\n\t\t\tlastUse: time.Now(),\n\t\t}\n\t\tbucket = r.m[key]\n\t}\n\tr.m[key].lastUse = time.Now()\n\n\treturn bucket.r\n}\n\nfunc (r *BucketSet) Take(key string) bool {\n\tif r.New == nil {\n\t\treturn true\n\t}\n\n\tbucket := r.take(key)\n\treturn bucket.Take()\n}\n\nfunc (r *BucketSet) Release(key string) {\n\tif r.New == nil {\n\t\treturn\n\t}\n\n\tr.mLck.Lock()\n\tdefer r.mLck.Unlock()\n\n\tbucket, ok := r.m[key]\n\tif !ok {\n\t\treturn\n\t}\n\tbucket.r.Release()\n}\n\nfunc (r *BucketSet) TakeContext(ctx context.Context, key string) error {\n\tif r.New == nil {\n\t\treturn nil\n\t}\n\n\tbucket := r.take(key)\n\treturn bucket.TakeContext(ctx)\n}\n"
  },
  {
    "path": "internal/limits/limiters/concurrency.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage limiters\n\nimport \"context\"\n\n// Semaphore is a convenience wrapper for a channel that implements\n// semaphore-kind synchronization.\n//\n// If the argument given to the NewSemaphore is negative or zero,\n// all methods are no-op.\ntype Semaphore struct {\n\tc chan struct{}\n}\n\nfunc NewSemaphore(max int) Semaphore {\n\treturn Semaphore{c: make(chan struct{}, max)}\n}\n\nfunc (s Semaphore) Take() bool {\n\tif cap(s.c) <= 0 {\n\t\treturn true\n\t}\n\ts.c <- struct{}{}\n\treturn true\n}\n\nfunc (s Semaphore) TakeContext(ctx context.Context) error {\n\tif cap(s.c) <= 0 {\n\t\treturn nil\n\t}\n\tselect {\n\tcase s.c <- struct{}{}:\n\t\treturn nil\n\tcase <-ctx.Done():\n\t\treturn ctx.Err()\n\t}\n}\n\nfunc (s Semaphore) Release() {\n\tif cap(s.c) <= 0 {\n\t\treturn\n\t}\n\tselect {\n\tcase <-s.c:\n\tdefault:\n\t\tpanic(\"limiters: mismatched Release call\")\n\t}\n}\n\nfunc (s Semaphore) Close() {\n}\n"
  },
  {
    "path": "internal/limits/limiters/limiters.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\n// Package limiters provides a set of wrappers intended to restrict the amount\n// of resources consumed by the server.\npackage limiters\n\nimport \"context\"\n\n// The L interface represents a blocking limiter that has some upper bound of\n// resource use and blocks when it is exceeded until enough resources are\n// freed.\ntype L interface {\n\tTake() bool\n\tTakeContext(context.Context) error\n\tRelease()\n\n\t// Close frees any resources used internally by Limiter for book-keeping.\n\tClose()\n}\n"
  },
  {
    "path": "internal/limits/limiters/multilimit.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage limiters\n\nimport \"context\"\n\n// MultiLimit wraps multiple L implementations into a single one, locking them\n// in the specified order.\n//\n// It does not implement any deadlock detection or avoidance algorithms.\ntype MultiLimit struct {\n\tWrapped []L\n}\n\nfunc (ml *MultiLimit) Take() bool {\n\tfor i := 0; i < len(ml.Wrapped); i++ {\n\t\tif !ml.Wrapped[i].Take() {\n\t\t\t// Acquire failed, undo acquire for all other resources we already\n\t\t\t// got.\n\t\t\tfor _, l := range ml.Wrapped[:i] {\n\t\t\t\tl.Release()\n\t\t\t}\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc (ml *MultiLimit) TakeContext(ctx context.Context) error {\n\tfor i := 0; i < len(ml.Wrapped); i++ {\n\t\tif err := ml.Wrapped[i].TakeContext(ctx); err != nil {\n\t\t\t// Acquire failed, undo acquire for all other resources we already\n\t\t\t// got.\n\t\t\tfor _, l := range ml.Wrapped[:i] {\n\t\t\t\tl.Release()\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (ml *MultiLimit) Release() {\n\tfor _, l := range ml.Wrapped {\n\t\tl.Release()\n\t}\n}\n\nfunc (ml *MultiLimit) Close() {\n\tfor _, l := range ml.Wrapped {\n\t\tl.Close()\n\t}\n}\n"
  },
  {
    "path": "internal/limits/limiters/rate.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage limiters\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"time\"\n)\n\nvar ErrClosed = errors.New(\"limiters: Rate bucket is closed\")\n\n// Rate structure implements a basic rate-limiter for requests using the token\n// bucket approach.\n//\n// Take() is expected to be called before each request. Excessive calls will\n// block. Timeouts can be implemented using the TakeContext method.\n//\n// Rate.Close causes all waiting Take to return false. TakeContext returns\n// ErrClosed in this case.\n//\n// If burstSize = 0, all methods are no-op and always succeed.\ntype Rate struct {\n\tbucket chan struct{}\n\tstop   chan struct{}\n}\n\nfunc NewRate(burstSize int, interval time.Duration) Rate {\n\tr := Rate{\n\t\tbucket: make(chan struct{}, burstSize),\n\t\tstop:   make(chan struct{}),\n\t}\n\n\tif burstSize == 0 {\n\t\treturn r\n\t}\n\n\tfor i := 0; i < burstSize; i++ {\n\t\tr.bucket <- struct{}{}\n\t}\n\n\tgo r.fill(burstSize, interval)\n\treturn r\n}\n\nfunc (r Rate) fill(burstSize int, interval time.Duration) {\n\tt := time.NewTimer(interval)\n\tdefer t.Stop()\n\tfor {\n\t\tt.Reset(interval)\n\t\tselect {\n\t\tcase <-t.C:\n\t\tcase <-r.stop:\n\t\t\tclose(r.bucket)\n\t\t\treturn\n\t\t}\n\n\tfill:\n\t\tfor i := 0; i < burstSize; i++ {\n\t\t\tselect {\n\t\t\tcase r.bucket <- struct{}{}:\n\t\t\tdefault:\n\t\t\t\t// If there are no Take pending and the bucket is already\n\t\t\t\t// full - don't block.\n\t\t\t\tbreak fill\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (r Rate) Take() bool {\n\tif cap(r.bucket) == 0 {\n\t\treturn true\n\t}\n\n\t_, ok := <-r.bucket\n\treturn ok\n}\n\nfunc (r Rate) TakeContext(ctx context.Context) error {\n\tif cap(r.bucket) == 0 {\n\t\treturn nil\n\t}\n\n\tselect {\n\tcase _, ok := <-r.bucket:\n\t\tif !ok {\n\t\t\treturn ErrClosed\n\t\t}\n\t\treturn nil\n\tcase <-ctx.Done():\n\t\treturn ctx.Err()\n\t}\n}\n\nfunc (r Rate) Release() {\n}\n\nfunc (r Rate) Close() {\n\tclose(r.stop)\n}\n"
  },
  {
    "path": "internal/limits/limits.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\n// Package limit provides a module object that can be used to restrict the\n// concurrency and rate of the messages flow globally or on per-source,\n// per-destination basis.\n//\n// Note, all domain inputs are interpreted with the assumption they are already\n// normalized.\n//\n// Low-level components are available in the limiters/ subpackage.\npackage limits\n\nimport (\n\t\"context\"\n\t\"net\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n\t\"github.com/foxcpp/maddy/internal/limits/limiters\"\n)\n\ntype Group struct {\n\tinstName string\n\n\tglobal limiters.MultiLimit\n\tip     *limiters.BucketSet // BucketSet of MultiLimit\n\tsource *limiters.BucketSet // BucketSet of MultiLimit\n\tdest   *limiters.BucketSet // BucketSet of MultiLimit\n}\n\nfunc New(c *container.C, _, instName string) (module.Module, error) {\n\treturn &Group{\n\t\tinstName: instName,\n\t}, nil\n}\n\nfunc (g *Group) Configure(inlineArgs []string, cfg *config.Map) error {\n\tvar (\n\t\tglobalL []limiters.L\n\t\tipL     []func() limiters.L\n\t\tsourceL []func() limiters.L\n\t\tdestL   []func() limiters.L\n\t)\n\n\tfor _, child := range cfg.Block.Children {\n\t\tif len(child.Args) < 1 {\n\t\t\treturn config.NodeErr(child, \"at least two arguments are required\")\n\t\t}\n\n\t\tvar (\n\t\t\tctor func() limiters.L\n\t\t\terr  error\n\t\t)\n\t\tswitch kind := child.Args[0]; kind {\n\t\tcase \"rate\":\n\t\t\tctor, err = rateCtor(child, child.Args[1:])\n\t\tcase \"concurrency\":\n\t\t\tctor, err = concurrencyCtor(child, child.Args[1:])\n\t\tdefault:\n\t\t\treturn config.NodeErr(child, \"unknown limit kind: %v\", kind)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tswitch scope := child.Name; scope {\n\t\tcase \"all\":\n\t\t\tglobalL = append(globalL, ctor())\n\t\tcase \"ip\":\n\t\t\tipL = append(ipL, ctor)\n\t\tcase \"source\":\n\t\t\tsourceL = append(sourceL, ctor)\n\t\tcase \"destination\":\n\t\t\tdestL = append(destL, ctor)\n\t\tdefault:\n\t\t\treturn config.NodeErr(child, \"unknown limit scope: %v\", scope)\n\t\t}\n\t}\n\n\t// 20010 is slightly higher than the default max. recipients count in\n\t// endpoint/smtp.\n\tg.global = limiters.MultiLimit{Wrapped: globalL}\n\tif len(ipL) != 0 {\n\t\tg.ip = limiters.NewBucketSet(func() limiters.L {\n\t\t\tl := make([]limiters.L, 0, len(ipL))\n\t\t\tfor _, ctor := range ipL {\n\t\t\t\tl = append(l, ctor())\n\t\t\t}\n\t\t\treturn &limiters.MultiLimit{Wrapped: l}\n\t\t}, 1*time.Minute, 20010)\n\t}\n\tif len(sourceL) != 0 {\n\t\tg.source = limiters.NewBucketSet(func() limiters.L {\n\t\t\tl := make([]limiters.L, 0, len(sourceL))\n\t\t\tfor _, ctor := range sourceL {\n\t\t\t\tl = append(l, ctor())\n\t\t\t}\n\t\t\treturn &limiters.MultiLimit{Wrapped: l}\n\t\t}, 1*time.Minute, 20010)\n\t}\n\tif len(destL) != 0 {\n\t\tg.dest = limiters.NewBucketSet(func() limiters.L {\n\t\t\tl := make([]limiters.L, 0, len(destL))\n\t\t\tfor _, ctor := range destL {\n\t\t\t\tl = append(l, ctor())\n\t\t\t}\n\t\t\treturn &limiters.MultiLimit{Wrapped: l}\n\t\t}, 1*time.Minute, 20010)\n\t}\n\n\treturn nil\n}\n\nfunc rateCtor(node config.Node, args []string) (func() limiters.L, error) {\n\tperiod := 1 * time.Second\n\tburst := 0\n\n\tswitch len(args) {\n\tcase 2:\n\t\tvar err error\n\t\tperiod, err = time.ParseDuration(args[1])\n\t\tif err != nil {\n\t\t\treturn nil, config.NodeErr(node, \"%v\", err)\n\t\t}\n\t\tfallthrough\n\tcase 1:\n\t\tvar err error\n\t\tburst, err = strconv.Atoi(args[0])\n\t\tif err != nil {\n\t\t\treturn nil, config.NodeErr(node, \"%v\", err)\n\t\t}\n\tcase 0:\n\t\treturn nil, config.NodeErr(node, \"at least burst size is needed\")\n\tdefault:\n\t\treturn nil, config.NodeErr(node, \"too many arguments\")\n\t}\n\n\treturn func() limiters.L {\n\t\treturn limiters.NewRate(burst, period)\n\t}, nil\n}\n\nfunc concurrencyCtor(node config.Node, args []string) (func() limiters.L, error) {\n\tif len(args) != 1 {\n\t\treturn nil, config.NodeErr(node, \"max concurrency value is needed\")\n\t}\n\tmax, err := strconv.Atoi(args[0])\n\tif err != nil {\n\t\treturn nil, config.NodeErr(node, \"%v\", err)\n\t}\n\treturn func() limiters.L {\n\t\treturn limiters.NewSemaphore(max)\n\t}, nil\n}\n\nfunc (g *Group) TakeMsg(ctx context.Context, addr net.IP, sourceDomain string) error {\n\tctx, cancel := context.WithTimeout(ctx, 5*time.Second)\n\tdefer cancel()\n\n\tif err := g.global.TakeContext(ctx); err != nil {\n\t\treturn err\n\t}\n\n\tif g.ip != nil {\n\t\tif err := g.ip.TakeContext(ctx, addr.String()); err != nil {\n\t\t\tg.global.Release()\n\t\t\treturn err\n\t\t}\n\t}\n\tif g.source != nil {\n\t\tif err := g.source.TakeContext(ctx, sourceDomain); err != nil {\n\t\t\tg.global.Release()\n\t\t\tg.ip.Release(addr.String())\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (g *Group) TakeDest(ctx context.Context, domain string) error {\n\tif g.dest == nil {\n\t\treturn nil\n\t}\n\tctx, cancel := context.WithTimeout(ctx, 5*time.Second)\n\tdefer cancel()\n\treturn g.dest.TakeContext(ctx, domain)\n}\n\nfunc (g *Group) ReleaseMsg(addr net.IP, sourceDomain string) {\n\tg.global.Release()\n\tif g.ip != nil {\n\t\tg.ip.Release(addr.String())\n\t}\n\tif g.source != nil {\n\t\tg.source.Release(sourceDomain)\n\t}\n}\n\nfunc (g *Group) ReleaseDest(domain string) {\n\tif g.dest == nil {\n\t\treturn\n\t}\n\tg.dest.Release(domain)\n}\n\nfunc (g *Group) Name() string {\n\treturn \"limits\"\n}\n\nfunc (g *Group) InstanceName() string {\n\treturn g.instName\n}\n\nfunc init() {\n\tmodules.Register(\"limits\", New)\n}\n"
  },
  {
    "path": "internal/modify/dkim/dkim.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage dkim\n\nimport (\n\t\"context\"\n\t\"crypto\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"path/filepath\"\n\t\"runtime/trace\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/emersion/go-message/textproto\"\n\t\"github.com/emersion/go-msgauth/dkim\"\n\t\"github.com/foxcpp/maddy/framework/address\"\n\t\"github.com/foxcpp/maddy/framework/buffer\"\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/dns\"\n\t\"github.com/foxcpp/maddy/framework/exterrors\"\n\t\"github.com/foxcpp/maddy/framework/log\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n\t\"github.com/foxcpp/maddy/internal/target\"\n\t\"golang.org/x/net/idna\"\n)\n\nconst Day = 86400 * time.Second\n\nvar (\n\toversignDefault = []string{\n\t\t// Directly visible to the user.\n\t\t\"Subject\",\n\t\t\"Sender\",\n\t\t\"To\",\n\t\t\"Cc\",\n\t\t\"From\",\n\t\t\"Date\",\n\n\t\t// Affects body processing.\n\t\t\"MIME-Version\",\n\t\t\"Content-Type\",\n\t\t\"Content-Transfer-Encoding\",\n\n\t\t// Affects user interaction.\n\t\t\"Reply-To\",\n\t\t\"In-Reply-To\",\n\t\t\"Message-Id\",\n\t\t\"References\",\n\n\t\t// Provide additional security benefit for OpenPGP.\n\t\t\"Autocrypt\",\n\t\t\"Openpgp\",\n\t}\n\tsignDefault = []string{\n\t\t// Mailing list information. Not oversigned to prevent signature\n\t\t// breakage by aliasing MLMs.\n\t\t\"List-Id\",\n\t\t\"List-Help\",\n\t\t\"List-Unsubscribe\",\n\t\t\"List-Post\",\n\t\t\"List-Owner\",\n\t\t\"List-Archive\",\n\n\t\t// Not oversigned since it can be prepended by intermediate relays.\n\t\t\"Resent-To\",\n\t\t\"Resent-Sender\",\n\t\t\"Resent-Message-Id\",\n\t\t\"Resent-Date\",\n\t\t\"Resent-From\",\n\t\t\"Resent-Cc\",\n\t}\n\n\thashFuncs = map[string]crypto.Hash{\n\t\t\"sha256\": crypto.SHA256,\n\t}\n)\n\ntype Modifier struct {\n\tinstName string\n\n\tdomains        []string\n\tselector       string\n\tsigners        map[string]crypto.Signer\n\toversignHeader []string\n\tsignHeader     []string\n\theaderCanon    dkim.Canonicalization\n\tbodyCanon      dkim.Canonicalization\n\tsigExpiry      time.Duration\n\thash           crypto.Hash\n\tmultipleFromOk bool\n\tsignSubdomains bool\n\n\tlog *log.Logger\n}\n\nfunc New(c *container.C, modName, instName string) (module.Module, error) {\n\tm := &Modifier{\n\t\tinstName: instName,\n\t\tsigners:  map[string]crypto.Signer{},\n\t\tlog:      c.DefaultLogger.Sublogger(modName),\n\t}\n\n\treturn m, nil\n}\n\nfunc (m *Modifier) Name() string {\n\treturn \"modify.dkim\"\n}\n\nfunc (m *Modifier) InstanceName() string {\n\treturn m.instName\n}\n\nfunc (m *Modifier) Configure(inlineArgs []string, cfg *config.Map) error {\n\tif len(inlineArgs) != 0 {\n\t\tif len(inlineArgs) == 1 {\n\t\t\treturn errors.New(\"modify.dkim: at least two arguments required\")\n\t\t}\n\n\t\tm.domains = inlineArgs[0 : len(inlineArgs)-1]\n\t\tm.selector = inlineArgs[len(inlineArgs)-1]\n\t}\n\n\tvar (\n\t\thashName        string\n\t\tkeyPathTemplate string\n\t\tnewKeyAlgo      string\n\t)\n\n\tcfg.Bool(\"debug\", true, false, &m.log.Debug)\n\tcfg.StringList(\"domains\", false, false, m.domains, &m.domains)\n\tcfg.String(\"selector\", false, false, m.selector, &m.selector)\n\tcfg.String(\"key_path\", false, false, \"dkim_keys/{domain}_{selector}.key\", &keyPathTemplate)\n\tcfg.StringList(\"oversign_fields\", false, false, oversignDefault, &m.oversignHeader)\n\tcfg.StringList(\"sign_fields\", false, false, signDefault, &m.signHeader)\n\tcfg.Enum(\"header_canon\", false, false,\n\t\t[]string{string(dkim.CanonicalizationRelaxed), string(dkim.CanonicalizationSimple)},\n\t\tdkim.CanonicalizationRelaxed, (*string)(&m.headerCanon))\n\tcfg.Enum(\"body_canon\", false, false,\n\t\t[]string{string(dkim.CanonicalizationRelaxed), string(dkim.CanonicalizationSimple)},\n\t\tdkim.CanonicalizationRelaxed, (*string)(&m.bodyCanon))\n\tcfg.Duration(\"sig_expiry\", false, false, 5*Day, &m.sigExpiry)\n\tcfg.Enum(\"hash\", false, false,\n\t\t[]string{\"sha256\"}, \"sha256\", &hashName)\n\tcfg.Enum(\"newkey_algo\", false, false,\n\t\t[]string{\"rsa4096\", \"rsa2048\", \"ed25519\"}, \"rsa2048\", &newKeyAlgo)\n\tcfg.Bool(\"allow_multiple_from\", false, false, &m.multipleFromOk)\n\tcfg.Bool(\"sign_subdomains\", false, false, &m.signSubdomains)\n\n\tif _, err := cfg.Process(); err != nil {\n\t\treturn err\n\t}\n\n\tif len(m.domains) == 0 {\n\t\treturn errors.New(\"sign_domain: at least one domain is needed\")\n\t}\n\tif m.selector == \"\" {\n\t\treturn errors.New(\"sign_domain: selector is not specified\")\n\t}\n\tif m.signSubdomains && len(m.domains) > 1 {\n\t\treturn errors.New(\"sign_domain: only one domain is supported when sign_subdomains is enabled\")\n\t}\n\n\tm.hash = hashFuncs[hashName]\n\tif m.hash == 0 {\n\t\tpanic(\"modify.dkim.Init: Hash function allowed by config matcher but not present in hashFuncs\")\n\t}\n\n\tfor _, domain := range m.domains {\n\t\tif _, err := idna.ToASCII(domain); err != nil {\n\t\t\tm.log.Printf(\"warning: unable to convert domain %s to A-labels form, non-EAI messages will not be signed: %v\", domain, err)\n\t\t}\n\n\t\tkeyValues := strings.NewReplacer(\"{domain}\", domain, \"{selector}\", m.selector)\n\t\tkeyPath := keyValues.Replace(keyPathTemplate)\n\n\t\tsigner, newKey, err := m.loadOrGenerateKey(keyPath, newKeyAlgo)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif newKey {\n\t\t\tdnsPath := keyPath + \".dns\"\n\t\t\tif filepath.Ext(keyPath) == \".key\" {\n\t\t\t\tdnsPath = keyPath[:len(keyPath)-4] + \".dns\"\n\t\t\t}\n\t\t\tm.log.Printf(\"generated a new %s keypair, private key is in %s, TXT record with public key is in %s,\\n\"+\n\t\t\t\t\"put its contents into TXT record for %s._domainkey.%s to make signing and verification work\",\n\t\t\t\tnewKeyAlgo, keyPath, dnsPath, m.selector, domain)\n\t\t}\n\n\t\tnormDomain, err := dns.ForLookup(domain)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"sign_skim: unable to normalize domain %s: %w\", domain, err)\n\t\t}\n\t\tm.signers[normDomain] = signer\n\t}\n\n\treturn nil\n}\n\nfunc (m *Modifier) fieldsToSign(h *textproto.Header) []string {\n\t// Filter out duplicated fields from configs so they\n\t// will not cause panic() in go-msgauth internals.\n\tseen := make(map[string]struct{})\n\n\tres := make([]string, 0, len(m.oversignHeader)+len(m.signHeader))\n\tfor _, key := range m.oversignHeader {\n\t\tif _, ok := seen[strings.ToLower(key)]; ok {\n\t\t\tcontinue\n\t\t}\n\t\tseen[strings.ToLower(key)] = struct{}{}\n\n\t\t// Add to signing list once per each key use.\n\t\tfor field := h.FieldsByKey(key); field.Next(); {\n\t\t\tres = append(res, key)\n\t\t}\n\t\t// And once more to \"oversign\" it.\n\t\tres = append(res, key)\n\t}\n\tfor _, key := range m.signHeader {\n\t\tif _, ok := seen[strings.ToLower(key)]; ok {\n\t\t\tcontinue\n\t\t}\n\t\tseen[strings.ToLower(key)] = struct{}{}\n\n\t\t// Add to signing list once per each key use.\n\t\tfor field := h.FieldsByKey(key); field.Next(); {\n\t\t\tres = append(res, key)\n\t\t}\n\t}\n\treturn res\n}\n\ntype state struct {\n\tm    *Modifier\n\tmeta *module.MsgMetadata\n\tfrom string\n\tlog  *log.Logger\n}\n\nfunc (m *Modifier) ModStateForMsg(ctx context.Context, msgMeta *module.MsgMetadata) (module.ModifierState, error) {\n\treturn &state{\n\t\tm:    m,\n\t\tmeta: msgMeta,\n\t\tlog:  target.DeliveryLogger(m.log, msgMeta),\n\t}, nil\n}\n\nfunc (s *state) RewriteSender(ctx context.Context, mailFrom string) (string, error) {\n\ts.from = mailFrom\n\treturn mailFrom, nil\n}\n\nfunc (s *state) RewriteRcpt(ctx context.Context, rcptTo string) ([]string, error) {\n\treturn []string{rcptTo}, nil\n}\n\nfunc (s *state) RewriteBody(ctx context.Context, h *textproto.Header, body buffer.Buffer) error {\n\tdefer trace.StartRegion(ctx, \"modify.dkim/RewriteBody\").End()\n\n\tvar domain string\n\tif s.from != \"\" {\n\t\tvar err error\n\t\t_, domain, err = address.Split(s.from)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\t// Use first key for null return path (<>) and postmaster (<postmaster>)\n\tif domain == \"\" {\n\t\tdomain = s.m.domains[0]\n\t}\n\tselector := s.m.selector\n\n\tif s.m.signSubdomains {\n\t\ttopDomain := s.m.domains[0]\n\t\tif strings.HasSuffix(domain, \".\"+topDomain) {\n\t\t\tdomain = topDomain\n\t\t}\n\t}\n\tnormDomain, err := dns.ForLookup(domain)\n\tif err != nil {\n\t\ts.log.Error(\"unable to normalize domain from envelope sender\", err, \"domain\", domain)\n\t\treturn nil\n\t}\n\tkeySigner := s.m.signers[normDomain]\n\tif keySigner == nil {\n\t\ts.log.Msg(\"no key for domain\", \"domain\", normDomain)\n\t\treturn nil\n\t}\n\n\t// If the message is non-EAI, we are not allowed to use domains in U-labels,\n\t// attempt to convert.\n\tif !s.meta.SMTPOpts.UTF8 {\n\t\tvar err error\n\t\tdomain, err = idna.ToASCII(domain)\n\t\tif err != nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tselector, err = idna.ToASCII(selector)\n\t\tif err != nil {\n\t\t\treturn nil\n\t\t}\n\t}\n\n\topts := dkim.SignOptions{\n\t\tDomain:                 domain,\n\t\tSelector:               selector,\n\t\tIdentifier:             \"@\" + domain,\n\t\tSigner:                 keySigner,\n\t\tHash:                   s.m.hash,\n\t\tHeaderCanonicalization: s.m.headerCanon,\n\t\tBodyCanonicalization:   s.m.bodyCanon,\n\t\tHeaderKeys:             s.m.fieldsToSign(h),\n\t}\n\tif s.m.sigExpiry != 0 {\n\t\topts.Expiration = time.Now().Add(s.m.sigExpiry)\n\t}\n\tsigner, err := dkim.NewSigner(&opts)\n\tif err != nil {\n\t\treturn exterrors.WithFields(err, map[string]interface{}{\"modifier\": \"modify.dkim\"})\n\t}\n\tif err := textproto.WriteHeader(signer, *h); err != nil {\n\t\t_ = signer.Close()\n\t\treturn exterrors.WithFields(err, map[string]interface{}{\"modifier\": \"modify.dkim\"})\n\t}\n\tr, err := body.Open()\n\tif err != nil {\n\t\t_ = signer.Close()\n\t\treturn exterrors.WithFields(err, map[string]interface{}{\"modifier\": \"modify.dkim\"})\n\t}\n\tif _, err := io.Copy(signer, r); err != nil {\n\t\t_ = signer.Close()\n\t\treturn exterrors.WithFields(err, map[string]interface{}{\"modifier\": \"modify.dkim\"})\n\t}\n\n\tif err := signer.Close(); err != nil {\n\t\treturn exterrors.WithFields(err, map[string]interface{}{\"modifier\": \"modify.dkim\"})\n\t}\n\n\th.AddRaw([]byte(signer.Signature()))\n\n\ts.m.log.DebugMsg(\"signed\", \"domain\", domain)\n\n\treturn nil\n}\n\nfunc (s *state) Close() error {\n\treturn nil\n}\n\nfunc init() {\n\tmodules.Register(\"modify.dkim\", New)\n}\n"
  },
  {
    "path": "internal/modify/dkim/dkim_test.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage dkim\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"reflect\"\n\t\"sort\"\n\t\"testing\"\n\n\t\"github.com/emersion/go-message/textproto\"\n\t\"github.com/emersion/go-msgauth/dkim\"\n\t\"github.com/foxcpp/go-mockdns\"\n\t\"github.com/foxcpp/maddy/framework/buffer\"\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/internal/testutils\"\n)\n\nfunc newTestModifier(t *testing.T, dir, keyAlgo string, domains []string) *Modifier {\n\tmod, err := New(container.New(), \"\", \"test\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tm := mod.(*Modifier)\n\tm.log = testutils.Logger(t, m.Name())\n\n\terr = m.Configure(nil, config.NewMap(nil, config.Node{\n\t\tChildren: []config.Node{\n\t\t\t{\n\t\t\t\tName: \"domains\",\n\t\t\t\tArgs: domains,\n\t\t\t},\n\t\t\t{\n\t\t\t\tName: \"selector\",\n\t\t\t\tArgs: []string{\"default\"},\n\t\t\t},\n\t\t\t{\n\t\t\t\tName: \"key_path\",\n\t\t\t\tArgs: []string{filepath.Join(dir, \"{domain}.key\")},\n\t\t\t},\n\t\t\t{\n\t\t\t\tName: \"newkey_algo\",\n\t\t\t\tArgs: []string{keyAlgo},\n\t\t\t},\n\t\t},\n\t}))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\treturn m\n}\n\nfunc signTestMsg(t *testing.T, m *Modifier, envelopeFrom string) (textproto.Header, []byte) {\n\tt.Helper()\n\n\tstate, err := m.ModStateForMsg(context.Background(), &module.MsgMetadata{})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\ttestHdr := textproto.Header{}\n\ttestHdr.Add(\"From\", \"<hello@hello>\")\n\ttestHdr.Add(\"Subject\", \"heya\")\n\ttestHdr.Add(\"To\", \"<heya@heya>\")\n\tbody := []byte(\"hello there\\r\\n\")\n\n\t// modify.dkim expects RewriteSender to be called to get envelope sender\n\t//  (see module.Modifier docs)\n\n\t// RewriteSender does not fail for modify.dkim. It just sets envelopeFrom.\n\tif _, err := state.RewriteSender(context.Background(), envelopeFrom); err != nil {\n\t\tpanic(err)\n\t}\n\terr = state.RewriteBody(context.Background(), &testHdr, buffer.MemoryBuffer{Slice: body})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\treturn testHdr, body\n}\n\nfunc verifyTestMsg(t *testing.T, keysPath string, expectedDomains []string, hdr textproto.Header, body []byte) {\n\tt.Helper()\n\n\tdomainsMap := make(map[string]bool)\n\tzones := map[string]mockdns.Zone{}\n\tfor _, domain := range expectedDomains {\n\t\tdnsRecord, err := os.ReadFile(filepath.Join(keysPath, domain+\".dns\"))\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tt.Log(\"DNS record:\", string(dnsRecord))\n\t\tzones[\"default._domainkey.\"+domain+\".\"] = mockdns.Zone{TXT: []string{string(dnsRecord)}}\n\t\tdomainsMap[domain] = false\n\t}\n\n\tvar fullBody bytes.Buffer\n\tif err := textproto.WriteHeader(&fullBody, hdr); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif _, err := fullBody.Write(body); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tresolver := &mockdns.Resolver{Zones: zones}\n\tverifs, err := dkim.VerifyWithOptions(bytes.NewReader(fullBody.Bytes()), &dkim.VerifyOptions{\n\t\tLookupTXT: func(domain string) ([]string, error) {\n\t\t\treturn resolver.LookupTXT(context.Background(), domain)\n\t\t},\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tfor _, v := range verifs {\n\t\tif v.Err != nil {\n\t\t\tt.Errorf(\"Verification error for %s: %v\", v.Domain, v.Err)\n\t\t}\n\t\tif _, ok := domainsMap[v.Domain]; !ok {\n\t\t\tt.Errorf(\"Unexpected verification for domain %s\", v.Domain)\n\t\t}\n\n\t\tdomainsMap[v.Domain] = true\n\t}\n\tfor domain, ok := range domainsMap {\n\t\tif !ok {\n\t\t\tt.Errorf(\"Missing verification for domain %s\", domain)\n\t\t}\n\t}\n}\n\nfunc TestGenerateSignVerify(t *testing.T) {\n\t// This test verifies whether a freshly generated key can be used for\n\t// signing and verification.\n\t//\n\t// It is a kind of \"integration\" test for DKIM modifier, as it tests\n\t// whether everything works correctly together.\n\t//\n\t// Additionally it also tests whether key selection works correctly.\n\n\ttest := func(domains []string, envelopeFrom string, expectDomain []string, keyAlgo string, headerCanon, bodyCanon dkim.Canonicalization, reload bool) {\n\t\tt.Helper()\n\n\t\tdir := t.TempDir()\n\n\t\tm := newTestModifier(t, dir, keyAlgo, domains)\n\t\tm.bodyCanon = bodyCanon\n\t\tm.headerCanon = headerCanon\n\t\tif reload {\n\t\t\tm = newTestModifier(t, dir, keyAlgo, domains)\n\t\t}\n\n\t\ttestHdr, body := signTestMsg(t, m, envelopeFrom)\n\t\tverifyTestMsg(t, dir, expectDomain, testHdr, body)\n\t}\n\n\tfor _, algo := range [2]string{\"rsa2048\", \"ed25519\"} {\n\t\tfor _, hdrCanon := range [2]dkim.Canonicalization{dkim.CanonicalizationSimple, dkim.CanonicalizationRelaxed} {\n\t\t\tfor _, bodyCanon := range [2]dkim.Canonicalization{dkim.CanonicalizationSimple, dkim.CanonicalizationRelaxed} {\n\t\t\t\ttest([]string{\"maddy.test\"}, \"test@maddy.test\", []string{\"maddy.test\"}, algo, hdrCanon, bodyCanon, false)\n\t\t\t\ttest([]string{\"maddy.test\"}, \"test@maddy.test\", []string{\"maddy.test\"}, algo, hdrCanon, bodyCanon, true)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Key selection tests\n\ttest(\n\t\t[]string{\"maddy.test\"}, // Generated keys.\n\t\t\"test@maddy.test\",      // Envelope sender.\n\t\t[]string{\"maddy.test\"}, // Expected signature domains.\n\t\t\"ed25519\", dkim.CanonicalizationRelaxed, dkim.CanonicalizationRelaxed, false)\n\ttest(\n\t\t[]string{\"maddy.test\"},\n\t\t\"test@unrelated.maddy.test\",\n\t\t[]string{},\n\t\t\"ed25519\", dkim.CanonicalizationRelaxed, dkim.CanonicalizationRelaxed, false)\n\ttest(\n\t\t[]string{\"maddy.test\", \"related.maddy.test\"},\n\t\t\"test@related.maddy.test\",\n\t\t[]string{\"related.maddy.test\"},\n\t\t\"ed25519\", dkim.CanonicalizationRelaxed, dkim.CanonicalizationRelaxed, false)\n\ttest(\n\t\t[]string{\"fallback.maddy.test\", \"maddy.test\"},\n\t\t\"postmaster\",\n\t\t[]string{\"fallback.maddy.test\"},\n\t\t\"ed25519\", dkim.CanonicalizationRelaxed, dkim.CanonicalizationRelaxed, false)\n\ttest(\n\t\t[]string{\"fallback.maddy.test\", \"maddy.test\"},\n\t\t\"\",\n\t\t[]string{\"fallback.maddy.test\"},\n\t\t\"ed25519\", dkim.CanonicalizationRelaxed, dkim.CanonicalizationRelaxed, false)\n\ttest(\n\t\t[]string{\"another.maddy.test\", \"another.maddy.test\", \"maddy.test\"},\n\t\t\"test@another.maddy.test\",\n\t\t[]string{\"another.maddy.test\"},\n\t\t\"ed25519\", dkim.CanonicalizationRelaxed, dkim.CanonicalizationRelaxed, false)\n\ttest(\n\t\t[]string{\"another.maddy.test\", \"another.maddy.test\", \"maddy.test\"},\n\t\t\"\",\n\t\t[]string{\"another.maddy.test\"},\n\t\t\"ed25519\", dkim.CanonicalizationRelaxed, dkim.CanonicalizationRelaxed, false)\n}\n\nfunc TestFieldsToSign(t *testing.T) {\n\th := textproto.Header{}\n\th.Add(\"A\", \"1\")\n\th.Add(\"c\", \"2\")\n\th.Add(\"C\", \"3\")\n\th.Add(\"a\", \"4\")\n\th.Add(\"b\", \"5\")\n\th.Add(\"unrelated\", \"6\")\n\n\tm := Modifier{\n\t\toversignHeader: []string{\"A\", \"B\"},\n\t\tsignHeader:     []string{\"C\"},\n\t}\n\tfields := m.fieldsToSign(&h)\n\tsort.Strings(fields)\n\texpected := []string{\"A\", \"A\", \"A\", \"B\", \"B\", \"C\", \"C\"}\n\n\tif !reflect.DeepEqual(fields, expected) {\n\t\tt.Errorf(\"incorrect set of fields to sign\\nwant: %v\\ngot:  %v\", expected, fields)\n\t}\n}\n"
  },
  {
    "path": "internal/modify/dkim/keys.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage dkim\n\nimport (\n\t\"crypto\"\n\t\"crypto/ecdsa\"\n\t\"crypto/ed25519\"\n\t\"crypto/rand\"\n\t\"crypto/rsa\"\n\t\"crypto/x509\"\n\t\"encoding/base64\"\n\t\"encoding/pem\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n)\n\nfunc (m *Modifier) loadOrGenerateKey(keyPath, newKeyAlgo string) (pkey crypto.Signer, newKey bool, err error) {\n\tf, err := os.Open(keyPath)\n\tif err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\tpkey, err = m.generateAndWrite(keyPath, newKeyAlgo)\n\t\t\treturn pkey, true, err\n\t\t}\n\t\treturn nil, false, err\n\t}\n\tdefer func() {\n\t\tif err := f.Close(); err != nil {\n\t\t\tm.log.Error(\"failed to close key file\", err)\n\t\t}\n\t}()\n\n\tpemBlob, err := io.ReadAll(f)\n\tif err != nil {\n\t\treturn nil, false, err\n\t}\n\n\tblock, _ := pem.Decode(pemBlob)\n\tif block == nil {\n\t\treturn nil, false, fmt.Errorf(\"modify.dkim: %s: invalid PEM block\", keyPath)\n\t}\n\n\tvar key interface{}\n\tswitch block.Type {\n\tcase \"PRIVATE KEY\": // RFC 5208 aka PKCS #8\n\t\tkey, err = x509.ParsePKCS8PrivateKey(block.Bytes)\n\t\tif err != nil {\n\t\t\treturn nil, false, fmt.Errorf(\"modify.dkim: %s: %w\", keyPath, err)\n\t\t}\n\tcase \"RSA PRIVATE KEY\": // RFC 3447 aka PKCS #1\n\t\tkey, err = x509.ParsePKCS1PrivateKey(block.Bytes)\n\t\tif err != nil {\n\t\t\treturn nil, false, fmt.Errorf(\"modify.dkim: %s: %w\", keyPath, err)\n\t\t}\n\tcase \"EC PRIVATE KEY\": // RFC 5915\n\t\tkey, err = x509.ParseECPrivateKey(block.Bytes)\n\t\tif err != nil {\n\t\t\treturn nil, false, fmt.Errorf(\"modify.dkim: %s: %w\", keyPath, err)\n\t\t}\n\tdefault:\n\t\treturn nil, false, fmt.Errorf(\"modify.dkim: %s: not a private key or unsupported format\", keyPath)\n\t}\n\n\tswitch key := key.(type) {\n\tcase *rsa.PrivateKey:\n\t\tif err := key.Validate(); err != nil {\n\t\t\treturn nil, false, err\n\t\t}\n\t\tkey.Precompute()\n\t\treturn key, false, nil\n\tcase ed25519.PrivateKey:\n\t\treturn key, false, nil\n\tcase *ecdsa.PublicKey:\n\t\treturn nil, false, fmt.Errorf(\"modify.dkim: %s: ECDSA keys are not supported\", keyPath)\n\tdefault:\n\t\treturn nil, false, fmt.Errorf(\"modify.dkim: %s: unknown key type: %T\", keyPath, key)\n\t}\n}\n\nfunc (m *Modifier) generateAndWrite(keyPath, newKeyAlgo string) (crypto.Signer, error) {\n\twrapErr := func(err error) error {\n\t\treturn fmt.Errorf(\"modify.dkim: generate %s: %w\", keyPath, err)\n\t}\n\n\tm.log.Printf(\"generating a new %s keypair...\", newKeyAlgo)\n\n\tvar (\n\t\tpkey     crypto.Signer\n\t\tdkimName = newKeyAlgo\n\t\terr      error\n\t)\n\tswitch newKeyAlgo {\n\tcase \"rsa4096\":\n\t\tdkimName = \"rsa\"\n\t\tpkey, err = rsa.GenerateKey(rand.Reader, 4096)\n\tcase \"rsa2048\":\n\t\tdkimName = \"rsa\"\n\t\tpkey, err = rsa.GenerateKey(rand.Reader, 2048)\n\tcase \"ed25519\":\n\t\t_, pkey, err = ed25519.GenerateKey(rand.Reader)\n\tdefault:\n\t\terr = fmt.Errorf(\"unknown key algorithm: %s\", newKeyAlgo)\n\t}\n\tif err != nil {\n\t\treturn nil, wrapErr(err)\n\t}\n\n\tkeyBlob, err := x509.MarshalPKCS8PrivateKey(pkey)\n\tif err != nil {\n\t\treturn nil, wrapErr(err)\n\t}\n\n\t// 0777 because we have public keys in here too and they don't\n\t// need protection. Individual private key files have 0600 perms.\n\tif err := os.MkdirAll(filepath.Dir(keyPath), 0o777); err != nil {\n\t\treturn nil, wrapErr(err)\n\t}\n\n\t_, err = writeDNSRecord(keyPath, dkimName, pkey)\n\tif err != nil {\n\t\treturn nil, wrapErr(err)\n\t}\n\n\tf, err := os.OpenFile(keyPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600)\n\tif err != nil {\n\t\treturn nil, wrapErr(err)\n\t}\n\n\tif err := pem.Encode(f, &pem.Block{\n\t\tType:  \"PRIVATE KEY\",\n\t\tBytes: keyBlob,\n\t}); err != nil {\n\t\treturn nil, wrapErr(err)\n\t}\n\n\treturn pkey, nil\n}\n\nfunc writeDNSRecord(keyPath, dkimAlgoName string, pkey crypto.Signer) (string, error) {\n\tvar (\n\t\tkeyBlob []byte\n\t\tpubkey  = pkey.Public()\n\t)\n\tswitch pubkey := pubkey.(type) {\n\tcase *rsa.PublicKey:\n\t\tvar err error\n\t\tkeyBlob, err = x509.MarshalPKIXPublicKey(pubkey)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\tcase ed25519.PublicKey:\n\t\tkeyBlob = pubkey\n\tdefault:\n\t\tpanic(\"modify.dkim.writeDNSRecord: unknown key algorithm\")\n\t}\n\n\tdnsPath := keyPath + \".dns\"\n\tif filepath.Ext(keyPath) == \".key\" {\n\t\tdnsPath = keyPath[:len(keyPath)-4] + \".dns\"\n\t}\n\tdnsF, err := os.Create(dnsPath)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tkeyRecord := fmt.Sprintf(\"v=DKIM1; k=%s; p=%s\", dkimAlgoName, base64.StdEncoding.EncodeToString(keyBlob))\n\tif _, err := io.WriteString(dnsF, keyRecord); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn dnsPath, nil\n}\n"
  },
  {
    "path": "internal/modify/dkim/keys_test.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage dkim\n\nimport (\n\t\"crypto/ed25519\"\n\t\"crypto/rsa\"\n\t\"encoding/base64\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/foxcpp/maddy/internal/testutils\"\n)\n\nfunc TestKeyLoad_new(t *testing.T) {\n\tm := Modifier{}\n\tm.log = testutils.Logger(t, m.Name())\n\n\tdir := t.TempDir()\n\n\tsigner, newKey, err := m.loadOrGenerateKey(filepath.Join(dir, \"testkey.key\"), \"ed25519\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif !newKey {\n\t\tt.Fatal(\"newKey=false\")\n\t}\n\n\trecordBlob, err := os.ReadFile(filepath.Join(dir, \"testkey.dns\"))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tvar keyBlob []byte\n\tfor _, part := range strings.Split(string(recordBlob), \";\") {\n\t\tpart = strings.TrimSpace(part)\n\t\tif strings.HasPrefix(part, \"k=\") {\n\t\t\tif part != \"k=ed25519\" {\n\t\t\t\tt.Fatalf(\"Wrong type of generated key, want ed25519, got %s\", part)\n\t\t\t}\n\t\t}\n\t\tif strings.HasPrefix(part, \"p=\") {\n\t\t\tkeyBlob, err = base64.StdEncoding.DecodeString(part[2:])\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t}\n\t}\n\n\tblob := signer.Public().(ed25519.PublicKey)\n\tif string(blob) != string(keyBlob) {\n\t\tt.Fatal(\"wrong public key placed into record file\")\n\t}\n}\n\nconst pkeyEd25519 = `-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEIJG9zs4vi2MYNkL9gUQwlmBLCzDODIJ5/1CwTAZFDm5U\n-----END PRIVATE KEY-----`\n\nconst pubkeyEd25519 = `5TPcCxzVByMyRsMFs5Dx23pnxKilI+1UrGg0t+O2oZU=`\n\nfunc TestKeyLoad_existing_pkcs8(t *testing.T) {\n\tm := Modifier{}\n\tm.log = testutils.Logger(t, m.Name())\n\n\tdir := t.TempDir()\n\n\tif err := os.WriteFile(filepath.Join(dir, \"testkey.key\"), []byte(pkeyEd25519), 0o600); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tsigner, newKey, err := m.loadOrGenerateKey(filepath.Join(dir, \"testkey.key\"), \"ed25519\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif newKey {\n\t\tt.Fatal(\"newKey = true\")\n\t}\n\n\tblob := signer.Public().(ed25519.PublicKey)\n\tif signerKey := base64.StdEncoding.EncodeToString(blob); signerKey != pubkeyEd25519 {\n\t\tt.Fatalf(\"wrong public key returned by loadOrGenerateKey, \\nwant %s\\ngot  %s\", pubkeyEd25519, signerKey)\n\t}\n}\n\nconst pkeyRSA = `-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAuxWwDR9ADiuV2b9xF+btOIgwS5W0yJeS/Dht4HlUELrye2JZ\n7TCQpx2Hs1FY5Tkj4VLnYHTPftS6cLYNx6hQbWZMhj5qmP9ccQ8rqdgdLB5RqCn3\nzo8wbKFZ8ygYt1yZyNOfJLNTBjIcC1BCKoZosA7MWHUOwRtt1ARVmldsNH3iio0l\nwHjyKNYd0Kqw4uGEg6sulK69lw4G8YTnKtCt0G8vCpQHyQepolOMF7Q1NZEw02/U\nE54qgaaC+ym+BQsqqF5iodmuIfLX+W0kKDee2YYhjuxNaFcPhE5j35LlGHCsrL0X\nh4+2VZSYXuAO5aWpwX9jrrSFyCJLD/aYGMgdrwIDAQABAoIBAEZrF2UZCidLSJA5\nevwgM9I/kM4if3Wxd+Xv54vCn13cwECo+GhLC2ebueRJDkjZhSPe7LBlx2RZ9gNO\nw0kPlZZYFx3AiKcmF0mHCExZyEE++EVv5pKdWwDIiu73fLYn6MqqvRA3X1zJp7yq\nbP1MskLyjwAMr40IIgLXztDVbykiRC2Rw+o5cu7o3e0p0sFqJsjCUKtXZuzLePOk\ngYYZ4FsmmVYh7pf244NEQao+fT19RtFL85E17yAHv+YD7qUBdbxoWIuAher9N/C0\nvOj4xYbNxbkS0+BTbygLAog5mFtNbAGysUZZ3YOYfKYgj9/u+aKwr2ZS2zIEeJj0\neAiHtWECgYEA48dqxrR76JyukHid+XyI4Nqt+2EHEeDi23WTTT6lSZL1F3I2q7FF\nDSHOA3hGw57GAMNQYCSzYxC4TBpZwJ7/8NdhA/kJg7tLOqcvZtS3Bu5bzLqLOCqL\nE1tgh2LrpWjit2v+VSsQlf+QjG7QAEiWtya+AOfNWenILfxk2VNPP3MCgYEA0kOM\nym/EcgcSSihbFyyYO4UHZZ7rWiPRB+BtatJbEADMXMlwSAXvvVCpWSZBKBKjIE2y\nZM+kvv50QUd4ue7dKVEnqOy26XuAmuTE14smx1QyNonRvBV/HItJ0tKfMIZbXOpq\nS2ESXkFybCzdOfzWOhx0PHjr40w8XUeSZi0LodUCgYAsC8bhD8uaKpozA7AAq41I\ndeEI6DVWxrb3mx/V4xRRSuKsGwDpaIkixfOxhhOhBlXhleM4BEDQGk6ZIMtUTSrO\n5scy3nhxick9WVD4QI/3/iWwTC5ZuRhVsOjUpVNOFB8rOu3eiEpXxyirj04Xj/Hd\nDtfVEv4JsgRsqA7UW6DKcwKBgQCiCvMXFDnWEwMSabWBz5lmzWfc9jO1HUM8Ccbp\ne0I4vBTDMW854nFXejF5BhVS18Il5BsmvCvgEePwZy9wQ9jnvaaN9hglKkv7k3Ds\nGE6DcazdASvFAuAaVHJJao7Ka9E/c10FyMLKJzASlCTOSr+iu0kNTbelTZx72uvF\nmNONHQKBgCEUuJMM11mV0FCsVfJsmIv6z/zqOiPiOVbP1Bv2WlVzipvkI9bm6OyN\nVHO8+oqFWyhJ3qRzebuPIefL8U6xjfMshX8MB23cB0J5LTPDZH3LCSmFvjr942EK\n5+ewYHKtmS+6aaE+J+oB11r7XU8FyEI0kv6rAPDwJ19K4BMG/x7J\n-----END RSA PRIVATE KEY-----`\n\nfunc TestKeyLoad_existing_pkcs1(t *testing.T) {\n\tm := Modifier{}\n\tm.log = testutils.Logger(t, m.Name())\n\n\tdir := t.TempDir()\n\n\tif err := os.WriteFile(filepath.Join(dir, \"testkey.key\"), []byte(pkeyRSA), 0o600); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tsigner, newKey, err := m.loadOrGenerateKey(filepath.Join(dir, \"testkey.key\"), \"rsa2048\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif newKey {\n\t\tt.Fatal(\"newKey=true\")\n\t}\n\n\tpubkey := signer.Public().(*rsa.PublicKey)\n\tif pubkey.E != 65537 {\n\t\tt.Fatalf(\"wrong public key returned by loadOrGenerateKey, got %d\", pubkey.E)\n\t}\n\tif pubkey.N.String() != \"23617257632228188386824425094266725423560758883229529475904285522114491665694237598874002862630696077162868821164059728985148713872807170386818903503533709975391952347175641552635505497204925274569104682448177717429244936284920784061388978739927939000424446717818401440783667723710780854637197555911253613285419663410256437304926940168312631109994734698918250930969511949067760562140706765511288141008942649676427142664185811322596443990204153105455693515405445788622172538582060141770589195075185467867938584021491237815987395835392935511032761463924045865609068314478096903374718657496007822964380498648030935260591\" {\n\t\tt.Fatalf(\"wrong public key returned by loadOrGenerateKey, got %s\", pubkey.N.String())\n\t}\n}\n"
  },
  {
    "path": "internal/modify/group.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage modify\n\nimport (\n\t\"context\"\n\n\t\"github.com/emersion/go-message/textproto\"\n\t\"github.com/foxcpp/maddy/framework/buffer\"\n\t\"github.com/foxcpp/maddy/framework/config\"\n\tmodconfig \"github.com/foxcpp/maddy/framework/config/module\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/log\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n)\n\ntype (\n\t// Group wraps multiple modifiers and runs them serially.\n\t//\n\t// It is also registered as a module under 'modifiers' name and acts as a\n\t// module group.\n\tGroup struct {\n\t\tinstName  string\n\t\tModifiers []module.Modifier\n\t}\n\n\tgroupState struct {\n\t\tstates []module.ModifierState\n\t}\n)\n\nfunc (g *Group) Configure(inlineArgs []string, cfg *config.Map) error {\n\tfor _, node := range cfg.Block.Children {\n\t\tmod, err := modconfig.MsgModifier(cfg.Globals, append([]string{node.Name}, node.Args...), node)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tg.Modifiers = append(g.Modifiers, mod)\n\t}\n\n\treturn nil\n}\n\nfunc (g *Group) Name() string {\n\treturn \"modifiers\"\n}\n\nfunc (g *Group) InstanceName() string {\n\treturn g.instName\n}\n\nfunc (g *Group) ModStateForMsg(ctx context.Context, msgMeta *module.MsgMetadata) (module.ModifierState, error) {\n\tgs := groupState{}\n\tfor _, modifier := range g.Modifiers {\n\t\tstate, err := modifier.ModStateForMsg(ctx, msgMeta)\n\t\tif err != nil {\n\t\t\t// Free state objects we initialized already.\n\t\t\tfor _, state := range gs.states {\n\t\t\t\tif err := state.Close(); err != nil {\n\t\t\t\t\tlog.DefaultLogger.Error(\"failed to close modifier state\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn nil, err\n\t\t}\n\t\tgs.states = append(gs.states, state)\n\t}\n\treturn gs, nil\n}\n\nfunc (gs groupState) RewriteSender(ctx context.Context, mailFrom string) (string, error) {\n\tvar err error\n\tfor _, state := range gs.states {\n\t\tmailFrom, err = state.RewriteSender(ctx, mailFrom)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\treturn mailFrom, nil\n}\n\nfunc (gs groupState) RewriteRcpt(ctx context.Context, rcptTo string) ([]string, error) {\n\tvar err error\n\tvar result = []string{rcptTo}\n\tfor _, state := range gs.states {\n\t\tvar intermediateResult = []string{}\n\t\tfor _, partResult := range result {\n\t\t\tvar partResult_multi []string\n\t\t\tpartResult_multi, err = state.RewriteRcpt(ctx, partResult)\n\t\t\tif err != nil {\n\t\t\t\treturn []string{\"\"}, err\n\t\t\t}\n\t\t\tintermediateResult = append(intermediateResult, partResult_multi...)\n\t\t}\n\t\tresult = intermediateResult\n\t}\n\treturn result, nil\n}\n\nfunc (gs groupState) RewriteBody(ctx context.Context, h *textproto.Header, body buffer.Buffer) error {\n\tfor _, state := range gs.states {\n\t\tif err := state.RewriteBody(ctx, h, body); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (gs groupState) Close() error {\n\t// We still try close all state objects to minimize\n\t// resource leaks when Close fails for one object..\n\n\tvar lastErr error\n\tfor _, state := range gs.states {\n\t\tif err := state.Close(); err != nil {\n\t\t\tlastErr = err\n\t\t}\n\t}\n\treturn lastErr\n}\n\nfunc init() {\n\tmodules.Register(\"modifiers\", func(c *container.C, _, instName string) (module.Module, error) {\n\t\treturn &Group{\n\t\t\tinstName: instName,\n\t\t}, nil\n\t})\n}\n"
  },
  {
    "path": "internal/modify/replace_addr.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage modify\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/emersion/go-message/textproto\"\n\t\"github.com/foxcpp/maddy/framework/address\"\n\t\"github.com/foxcpp/maddy/framework/buffer\"\n\t\"github.com/foxcpp/maddy/framework/config\"\n\tmodconfig \"github.com/foxcpp/maddy/framework/config/module\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n)\n\n// replaceAddr is a simple module that replaces matching sender (or recipient) address\n// in messages using module.Table implementation.\n//\n// If created with modName = \"modify.replace_sender\", it will change sender address.\n// If created with modName = \"modify.replace_rcpt\", it will change recipient addresses.\ntype replaceAddr struct {\n\tmodName  string\n\tinstName string\n\n\treplaceSender bool\n\treplaceRcpt   bool\n\ttable         module.MultiTable\n}\n\nfunc NewReplaceAddr(c *container.C, modName, instName string) (module.Module, error) {\n\tr := replaceAddr{\n\t\tmodName:       modName,\n\t\tinstName:      instName,\n\t\treplaceSender: modName == \"modify.replace_sender\",\n\t\treplaceRcpt:   modName == \"modify.replace_rcpt\",\n\t}\n\n\treturn &r, nil\n}\n\nfunc (r *replaceAddr) Configure(inlineArgs []string, cfg *config.Map) error {\n\treturn modconfig.ModuleFromNode(\"table\", inlineArgs, cfg.Block, cfg.Globals, &r.table)\n}\n\nfunc (r *replaceAddr) Name() string {\n\treturn r.modName\n}\n\nfunc (r *replaceAddr) InstanceName() string {\n\treturn r.instName\n}\n\nfunc (r *replaceAddr) ModStateForMsg(ctx context.Context, msgMeta *module.MsgMetadata) (module.ModifierState, error) {\n\treturn r, nil\n}\n\nfunc (r *replaceAddr) RewriteSender(ctx context.Context, mailFrom string) (string, error) {\n\tif r.replaceSender {\n\t\tresults, err := r.rewrite(ctx, mailFrom)\n\t\tif err != nil {\n\t\t\treturn mailFrom, err\n\t\t}\n\t\tmailFrom = results[0]\n\t}\n\treturn mailFrom, nil\n}\n\nfunc (r *replaceAddr) RewriteRcpt(ctx context.Context, rcptTo string) ([]string, error) {\n\tif r.replaceRcpt {\n\t\treturn r.rewrite(ctx, rcptTo)\n\t}\n\treturn []string{rcptTo}, nil\n}\n\nfunc (r *replaceAddr) RewriteBody(ctx context.Context, h *textproto.Header, body buffer.Buffer) error {\n\treturn nil\n}\n\nfunc (r *replaceAddr) Close() error {\n\treturn nil\n}\n\nfunc (r *replaceAddr) rewrite(ctx context.Context, val string) ([]string, error) {\n\tnormAddr, err := address.ForLookup(val)\n\tif err != nil {\n\t\treturn []string{val}, fmt.Errorf(\"malformed address: %v\", err)\n\t}\n\n\treplacements, err := r.table.LookupMulti(ctx, normAddr)\n\tif err != nil {\n\t\treturn []string{val}, err\n\t}\n\tif len(replacements) > 0 {\n\t\tfor _, replacement := range replacements {\n\t\t\tif !address.Valid(replacement) {\n\t\t\t\treturn []string{\"\"}, fmt.Errorf(\"refusing to replace recipient with the invalid address %s\", replacement)\n\t\t\t}\n\t\t}\n\t\treturn replacements, nil\n\t}\n\n\tmbox, domain, err := address.Split(normAddr)\n\tif err != nil {\n\t\t// If we have malformed address here, something is really wrong, but let's\n\t\t// ignore it silently then anyway.\n\t\treturn []string{val}, nil\n\t}\n\n\t// mbox is already normalized, since it is a part of address.ForLookup\n\t// result.\n\treplacements, err = r.table.LookupMulti(ctx, mbox)\n\tif err != nil {\n\t\treturn []string{val}, err\n\t}\n\tif len(replacements) > 0 {\n\t\tvar results = make([]string, len(replacements))\n\t\tfor i, replacement := range replacements {\n\t\t\tif strings.Contains(replacement, \"@\") && !strings.HasPrefix(replacement, `\"`) && !strings.HasSuffix(replacement, `\"`) {\n\t\t\t\tif !address.Valid(replacement) {\n\t\t\t\t\treturn []string{\"\"}, fmt.Errorf(\"refusing to replace recipient with invalid address %s\", replacement)\n\t\t\t\t}\n\t\t\t\tresults[i] = replacement\n\t\t\t} else {\n\t\t\t\tresults[i] = replacement + \"@\" + domain\n\t\t\t}\n\t\t}\n\t\treturn results, nil\n\t}\n\n\treturn []string{val}, nil\n}\n\nfunc init() {\n\tmodules.Register(\"modify.replace_sender\", NewReplaceAddr)\n\tmodules.Register(\"modify.replace_rcpt\", NewReplaceAddr)\n}\n"
  },
  {
    "path": "internal/modify/replace_addr_test.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage modify\n\nimport (\n\t\"context\"\n\t\"reflect\"\n\t\"testing\"\n\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/internal/testutils\"\n)\n\nfunc testReplaceAddr(t *testing.T, modName string) {\n\ttest := func(addr string, expectedMulti []string, aliases map[string][]string) {\n\t\tt.Helper()\n\n\t\tmod, err := NewReplaceAddr(container.New(), modName, \"\")\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tm := mod.(*replaceAddr)\n\t\tif err := m.Configure([]string{\"dummy\"}, config.NewMap(nil, config.Node{})); err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tm.table = testutils.MultiTable{M: aliases}\n\n\t\tvar actualMulti []string\n\t\tif modName == \"modify.replace_sender\" {\n\t\t\tvar actual string\n\t\t\tactual, err = m.RewriteSender(context.Background(), addr)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tactualMulti = []string{actual}\n\t\t}\n\t\tif modName == \"modify.replace_rcpt\" {\n\t\t\tactualMulti, err = m.RewriteRcpt(context.Background(), addr)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t}\n\n\t\tif !reflect.DeepEqual(actualMulti, expectedMulti) {\n\t\t\tt.Errorf(\"want %s, got %s\", expectedMulti, actualMulti)\n\t\t}\n\t}\n\n\ttest(\"test@example.org\", []string{\"test@example.org\"}, nil)\n\ttest(\"postmaster\", []string{\"postmaster\"}, nil)\n\ttest(\"test@example.com\", []string{\"test@example.org\"},\n\t\tmap[string][]string{\"test@example.com\": []string{\"test@example.org\"}})\n\ttest(`\"\\\"test @ test\\\"\"@example.com`, []string{\"test@example.org\"},\n\t\tmap[string][]string{`\"\\\"test @ test\\\"\"@example.com`: []string{\"test@example.org\"}})\n\ttest(`test@example.com`, []string{`\"\\\"test @ test\\\"\"@example.org`},\n\t\tmap[string][]string{`test@example.com`: []string{`\"\\\"test @ test\\\"\"@example.org`}})\n\ttest(`\"\\\"test @ test\\\"\"@example.com`, []string{`\"\\\"b @ b\\\"\"@example.com`},\n\t\tmap[string][]string{`\"\\\"test @ test\\\"\"`: []string{`\"\\\"b @ b\\\"\"`}})\n\ttest(\"TeSt@eXAMple.com\", []string{\"test@example.org\"},\n\t\tmap[string][]string{\"test@example.com\": []string{\"test@example.org\"}})\n\ttest(\"test@example.com\", []string{\"test2@example.com\"},\n\t\tmap[string][]string{\"test\": []string{\"test2\"}})\n\ttest(\"test@example.com\", []string{\"test2@example.org\"},\n\t\tmap[string][]string{\"test\": []string{\"test2@example.org\"}})\n\ttest(\"postmaster\", []string{\"test2@example.org\"},\n\t\tmap[string][]string{\"postmaster\": []string{\"test2@example.org\"}})\n\ttest(\"TeSt@examPLE.com\", []string{\"test2@example.com\"},\n\t\tmap[string][]string{\"test\": []string{\"test2\"}})\n\ttest(\"test@example.com\", []string{\"test3@example.com\"},\n\t\tmap[string][]string{\n\t\t\t\"test@example.com\": []string{\"test3@example.com\"},\n\t\t\t\"test\":             []string{\"test2\"},\n\t\t})\n\ttest(\"rcpt@E\\u0301.example.com\", []string{\"rcpt@foo.example.com\"},\n\t\tmap[string][]string{\n\t\t\t\"rcpt@\\u00E9.example.com\": []string{\"rcpt@foo.example.com\"},\n\t\t})\n\ttest(\"E\\u0301@foo.example.com\", []string{\"rcpt@foo.example.com\"},\n\t\tmap[string][]string{\n\t\t\t\"\\u00E9@foo.example.com\": []string{\"rcpt@foo.example.com\"},\n\t\t})\n\n\tif modName == \"modify.replace_rcpt\" {\n\t\t//multiple aliases\n\t\ttest(\"test@example.com\", []string{\"test@example.org\", \"test@example.net\"},\n\t\t\tmap[string][]string{\"test@example.com\": []string{\"test@example.org\", \"test@example.net\"}})\n\t\ttest(\"test@example.com\", []string{\"1@example.com\", \"2@example.com\", \"3@example.com\"},\n\t\t\tmap[string][]string{\"test@example.com\": []string{\"1@example.com\", \"2@example.com\", \"3@example.com\"}})\n\t}\n}\n\nfunc TestReplaceAddr_RewriteSender(t *testing.T) {\n\ttestReplaceAddr(t, \"modify.replace_sender\")\n}\n\nfunc TestReplaceAddr_RewriteRcpt(t *testing.T) {\n\ttestReplaceAddr(t, \"modify.replace_rcpt\")\n}\n"
  },
  {
    "path": "internal/msgpipeline/bench_test.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage msgpipeline\n\nimport (\n\t\"strconv\"\n\t\"testing\"\n\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/internal/testutils\"\n)\n\nfunc BenchmarkMsgPipelineSimple(b *testing.B) {\n\ttarget := testutils.Target{InstName: \"test_target\", DiscardMessages: true}\n\td := MsgPipeline{msgpipelineCfg: msgpipelineCfg{\n\t\tperSource: map[string]sourceBlock{},\n\t\tdefaultSource: sourceBlock{\n\t\t\tperRcpt: map[string]*rcptBlock{},\n\t\t\tdefaultRcpt: &rcptBlock{\n\t\t\t\ttargets: []module.DeliveryTarget{&target},\n\t\t\t},\n\t\t},\n\t}}\n\n\ttestutils.BenchDelivery(b, &d, \"sender@example.org\", []string{\"rcpt-X@example.org\"})\n}\n\nfunc BenchmarkMsgPipelineGlobalChecks(b *testing.B) {\n\ttestWithCount := func(checksCount int) {\n\t\tb.Run(strconv.Itoa(checksCount), func(b *testing.B) {\n\t\t\tchecks := make([]module.Check, 0, checksCount)\n\t\t\tfor i := 0; i < checksCount; i++ {\n\t\t\t\tchecks = append(checks, &testutils.Check{InstName: \"check_\" + strconv.Itoa(i)})\n\t\t\t}\n\n\t\t\ttarget := testutils.Target{InstName: \"test_target\", DiscardMessages: true}\n\t\t\td := MsgPipeline{msgpipelineCfg: msgpipelineCfg{\n\t\t\t\tglobalChecks: checks,\n\t\t\t\tperSource:    map[string]sourceBlock{},\n\t\t\t\tdefaultSource: sourceBlock{\n\t\t\t\t\tperRcpt: map[string]*rcptBlock{},\n\t\t\t\t\tdefaultRcpt: &rcptBlock{\n\t\t\t\t\t\ttargets: []module.DeliveryTarget{&target},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}}\n\n\t\t\ttestutils.BenchDelivery(b, &d, \"sender@example.org\", []string{\"rcpt-X@example.org\"})\n\t\t})\n\t}\n\n\ttestWithCount(5)\n\ttestWithCount(10)\n\ttestWithCount(15)\n}\n\nfunc BenchmarkMsgPipelineTargets(b *testing.B) {\n\ttestWithCount := func(targetCount int) {\n\t\tb.Run(strconv.Itoa(targetCount), func(b *testing.B) {\n\t\t\ttargets := make([]module.DeliveryTarget, 0, targetCount)\n\t\t\tfor i := 0; i < targetCount; i++ {\n\t\t\t\ttargets = append(targets, &testutils.Target{InstName: \"target_\" + strconv.Itoa(i), DiscardMessages: true})\n\t\t\t}\n\n\t\t\td := MsgPipeline{msgpipelineCfg: msgpipelineCfg{\n\t\t\t\tperSource: map[string]sourceBlock{},\n\t\t\t\tdefaultSource: sourceBlock{\n\t\t\t\t\tperRcpt: map[string]*rcptBlock{},\n\t\t\t\t\tdefaultRcpt: &rcptBlock{\n\t\t\t\t\t\ttargets: targets,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}}\n\n\t\t\ttestutils.BenchDelivery(b, &d, \"sender@example.org\", []string{\"rcpt-X@example.org\"})\n\t\t})\n\t}\n\n\ttestWithCount(5)\n\ttestWithCount(10)\n\ttestWithCount(15)\n}\n"
  },
  {
    "path": "internal/msgpipeline/bodynonatomic_test.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage msgpipeline\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/internal/modify\"\n\t\"github.com/foxcpp/maddy/internal/testutils\"\n)\n\ntype multipleErrs map[string]error\n\nfunc (m multipleErrs) SetStatus(rcptTo string, err error) {\n\tm[rcptTo] = err\n}\n\nfunc TestMsgPipeline_BodyNonAtomic(t *testing.T) {\n\terr := errors.New(\"go away\")\n\n\ttarget := testutils.Target{\n\t\tPartialBodyErr: map[string]error{\n\t\t\t\"tester@example.org\": err,\n\t\t},\n\t}\n\td := MsgPipeline{\n\t\tmsgpipelineCfg: msgpipelineCfg{\n\t\t\tperSource: map[string]sourceBlock{},\n\t\t\tdefaultSource: sourceBlock{\n\t\t\t\tperRcpt: map[string]*rcptBlock{},\n\t\t\t\tdefaultRcpt: &rcptBlock{\n\t\t\t\t\ttargets: []module.DeliveryTarget{&target},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tLog: testutils.Logger(t, \"msgpipeline\"),\n\t}\n\n\tc := multipleErrs{}\n\ttestutils.DoTestDeliveryNonAtomic(t, c, &d, \"sender@example.org\", []string{\"tester@example.org\", \"tester2@example.org\"})\n\n\tif c[\"tester@example.org\"] == nil {\n\t\tt.Fatalf(\"no error for tester@example.org\")\n\t}\n\tif c[\"tester@example.org\"].Error() != err.Error() {\n\t\tt.Errorf(\"wrong error for tester@example.org: %v\", err)\n\t}\n}\n\nfunc TestMsgPipeline_BodyNonAtomic_ModifiedRcpt(t *testing.T) {\n\terr := errors.New(\"go away\")\n\n\ttarget := testutils.Target{\n\t\tPartialBodyErr: map[string]error{\n\t\t\t\"tester-alias@example.org\": err,\n\t\t},\n\t}\n\td := MsgPipeline{\n\t\tmsgpipelineCfg: msgpipelineCfg{\n\t\t\tglobalModifiers: modify.Group{\n\t\t\t\tModifiers: []module.Modifier{\n\t\t\t\t\ttestutils.Modifier{\n\t\t\t\t\t\tInstName: \"test_modifier\",\n\t\t\t\t\t\tRcptTo: map[string][]string{\n\t\t\t\t\t\t\t\"tester@example.org\": []string{\"tester-alias@example.org\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tperSource: map[string]sourceBlock{},\n\t\t\tdefaultSource: sourceBlock{\n\t\t\t\tperRcpt: map[string]*rcptBlock{},\n\t\t\t\tdefaultRcpt: &rcptBlock{\n\t\t\t\t\ttargets: []module.DeliveryTarget{&target},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tLog: testutils.Logger(t, \"msgpipeline\"),\n\t}\n\n\tc := multipleErrs{}\n\ttestutils.DoTestDeliveryNonAtomic(t, c, &d, \"sender@example.org\", []string{\"tester@example.org\"})\n\n\tif c[\"tester@example.org\"] == nil {\n\t\tt.Fatalf(\"no error for tester@example.org\")\n\t}\n\tif c[\"tester@example.org\"].Error() != err.Error() {\n\t\tt.Errorf(\"wrong error for tester@example.org: %v\", err)\n\t}\n}\n\nfunc TestMsgPipeline_BodyNonAtomic_ExpandAtomic(t *testing.T) {\n\terr := errors.New(\"go away\")\n\n\ttarget, target2 := testutils.Target{\n\t\tPartialBodyErr: map[string]error{\n\t\t\t\"tester@example.org\": err,\n\t\t},\n\t}, testutils.Target{\n\t\tBodyErr: err,\n\t}\n\td := MsgPipeline{\n\t\tmsgpipelineCfg: msgpipelineCfg{\n\t\t\tperSource: map[string]sourceBlock{},\n\t\t\tdefaultSource: sourceBlock{\n\t\t\t\tperRcpt: map[string]*rcptBlock{},\n\t\t\t\tdefaultRcpt: &rcptBlock{\n\t\t\t\t\ttargets: []module.DeliveryTarget{&target, &target2},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tLog: testutils.Logger(t, \"msgpipeline\"),\n\t}\n\n\tc := multipleErrs{}\n\ttestutils.DoTestDeliveryNonAtomic(t, c, &d, \"sender@example.org\", []string{\"tester@example.org\", \"tester2@example.org\"})\n\n\tif c[\"tester@example.org\"] == nil {\n\t\tt.Fatalf(\"no error for tester@example.org\")\n\t}\n\tif c[\"tester@example.org\"].Error() != err.Error() {\n\t\tt.Errorf(\"wrong error for tester@example.org: %v\", err)\n\t}\n\tif c[\"tester2@example.org\"] == nil {\n\t\tt.Fatalf(\"no error for tester@example.org\")\n\t}\n\tif c[\"tester2@example.org\"].Error() != err.Error() {\n\t\tt.Errorf(\"wrong error for tester@example.org: %v\", err)\n\t}\n}\n"
  },
  {
    "path": "internal/msgpipeline/check_group.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage msgpipeline\n\nimport (\n\t\"github.com/foxcpp/maddy/framework/config\"\n\tmodconfig \"github.com/foxcpp/maddy/framework/config/module\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n)\n\n// CheckGroup is a module container for a group of Check implementations.\n//\n// It allows to share a set of filter configurations between using named\n// configuration blocks (module instances) system.\n//\n// It is registered globally under the name 'checks'. The object does not\n// implement any standard module interfaces besides module.Module and is\n// specific to the message pipeline.\ntype CheckGroup struct {\n\tinstName string\n\tL        []module.Check\n}\n\nfunc (cg *CheckGroup) Configure(inlineArgs []string, cfg *config.Map) error {\n\tfor _, node := range cfg.Block.Children {\n\t\tchk, err := modconfig.MessageCheck(cfg.Globals, append([]string{node.Name}, node.Args...), node)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tcg.L = append(cg.L, chk)\n\t}\n\n\treturn nil\n}\n\nfunc (*CheckGroup) Name() string {\n\treturn \"checks\"\n}\n\nfunc (cg *CheckGroup) InstanceName() string {\n\treturn cg.instName\n}\n\nfunc init() {\n\tmodules.Register(\"checks\", func(_ *container.C, _, instName string) (module.Module, error) {\n\t\treturn &CheckGroup{\n\t\t\tinstName: instName,\n\t\t}, nil\n\t})\n}\n"
  },
  {
    "path": "internal/msgpipeline/check_runner.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage msgpipeline\n\nimport (\n\t\"context\"\n\t\"runtime/debug\"\n\t\"sync\"\n\n\t\"github.com/emersion/go-message/textproto\"\n\t\"github.com/emersion/go-msgauth/authres\"\n\t\"github.com/foxcpp/maddy/framework/buffer\"\n\t\"github.com/foxcpp/maddy/framework/dns\"\n\t\"github.com/foxcpp/maddy/framework/exterrors\"\n\t\"github.com/foxcpp/maddy/framework/log\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/internal/dmarc\"\n)\n\n// checkRunner runs groups of checks, collects and merges results.\n// It also makes sure that each check gets only one state object created.\ntype checkRunner struct {\n\tmsgMeta          *module.MsgMetadata\n\tmailFrom         string\n\tmailFromReceived bool\n\n\tcheckedRcpts         []string\n\tcheckedRcptsPerCheck map[module.CheckState]map[string]struct{}\n\tcheckedRcptsLock     sync.Mutex\n\n\tresolver      dns.Resolver\n\tdoDMARC       bool\n\tdidDMARCFetch bool\n\tdmarcVerify   *dmarc.Verifier\n\n\tlog *log.Logger\n\n\tstates map[module.Check]module.CheckState\n\n\tmergedRes module.CheckResult\n}\n\nfunc newCheckRunner(msgMeta *module.MsgMetadata, log *log.Logger, r dns.Resolver) *checkRunner {\n\treturn &checkRunner{\n\t\tmsgMeta:              msgMeta,\n\t\tcheckedRcptsPerCheck: map[module.CheckState]map[string]struct{}{},\n\t\tlog:                  log,\n\t\tresolver:             r,\n\t\tdmarcVerify:          dmarc.NewVerifier(r),\n\t\tstates:               make(map[module.Check]module.CheckState),\n\t}\n}\n\nfunc (cr *checkRunner) checkStates(ctx context.Context, checks []module.Check) ([]module.CheckState, error) {\n\tstates := make([]module.CheckState, 0, len(checks))\n\tnewStates := make([]module.CheckState, 0, len(checks))\n\tnewStatesMap := make(map[module.Check]module.CheckState, len(checks))\n\tcloseStates := func() {\n\t\tfor _, state := range states {\n\t\t\tif err := state.Close(); err != nil {\n\t\t\t\tcr.log.Error(\"failed to close check state\", err)\n\t\t\t}\n\t\t}\n\t}\n\n\tfor _, check := range checks {\n\t\tstate, ok := cr.states[check]\n\t\tif ok {\n\t\t\tstates = append(states, state)\n\t\t\tcontinue\n\t\t}\n\n\t\tcr.log.Debugf(\"initializing state for %v (%p)\", objectName(check), check)\n\t\tstate, err := check.CheckStateForMsg(ctx, cr.msgMeta)\n\t\tif err != nil {\n\t\t\tcloseStates()\n\t\t\treturn nil, err\n\t\t}\n\t\tstates = append(states, state)\n\t\tnewStates = append(newStates, state)\n\t\tnewStatesMap[check] = state\n\t}\n\n\tif len(newStates) == 0 {\n\t\treturn states, nil\n\t}\n\n\t// Here we replay previous CheckConnection/CheckSender/CheckRcpt calls\n\t// for any newly initialized checks so they all get change to see all these things.\n\t//\n\t// Done outside of check loop above to make sure we can run these for multiple\n\t// checks in parallel.\n\tif cr.mailFromReceived {\n\t\terr := cr.runAndMergeResults(newStates, func(s module.CheckState) module.CheckResult {\n\t\t\tres := s.CheckConnection(ctx)\n\t\t\treturn res\n\t\t})\n\t\tif err != nil {\n\t\t\tcloseStates()\n\t\t\treturn nil, err\n\t\t}\n\t\terr = cr.runAndMergeResults(newStates, func(s module.CheckState) module.CheckResult {\n\t\t\tres := s.CheckSender(ctx, cr.mailFrom)\n\t\t\treturn res\n\t\t})\n\t\tif err != nil {\n\t\t\tcloseStates()\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tif len(cr.checkedRcpts) != 0 {\n\t\tfor _, rcpt := range cr.checkedRcpts {\n\t\t\terr := cr.runAndMergeResults(states, func(s module.CheckState) module.CheckResult {\n\t\t\t\t// Avoid calling CheckRcpt for the same recipient for the same check\n\t\t\t\t// multiple times, even if requested.\n\t\t\t\tcr.checkedRcptsLock.Lock()\n\t\t\t\tif _, ok := cr.checkedRcptsPerCheck[s][rcpt]; ok {\n\t\t\t\t\tcr.checkedRcptsLock.Unlock()\n\t\t\t\t\treturn module.CheckResult{}\n\t\t\t\t}\n\t\t\t\tif cr.checkedRcptsPerCheck[s] == nil {\n\t\t\t\t\tcr.checkedRcptsPerCheck[s] = make(map[string]struct{})\n\t\t\t\t}\n\t\t\t\tcr.checkedRcptsPerCheck[s][rcpt] = struct{}{}\n\t\t\t\tcr.checkedRcptsLock.Unlock()\n\n\t\t\t\tres := s.CheckRcpt(ctx, rcpt)\n\t\t\t\treturn res\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\tcloseStates()\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t}\n\n\t// This is done after all actions that can fail so we will not have to remove\n\t// state objects from main map.\n\tfor check, state := range newStatesMap {\n\t\tcr.states[check] = state\n\t}\n\n\treturn states, nil\n}\n\nfunc (cr *checkRunner) runAndMergeResults(states []module.CheckState, runner func(module.CheckState) module.CheckResult) error {\n\tdata := struct {\n\t\tauthResLock sync.Mutex\n\t\theaderLock  sync.Mutex\n\n\t\tquarantineErr    error\n\t\tquarantineCheck  string\n\t\tsetQuarantineErr sync.Once\n\n\t\trejectErr    error\n\t\trejectCheck  string\n\t\tsetRejectErr sync.Once\n\n\t\twg sync.WaitGroup\n\t}{}\n\n\tfor _, state := range states {\n\t\tdata.wg.Add(1)\n\t\tgo func() {\n\t\t\tdefer func() {\n\t\t\t\tdata.wg.Done()\n\t\t\t\tif err := recover(); err != nil {\n\t\t\t\t\tstack := debug.Stack()\n\t\t\t\t\tlog.Printf(\"panic during check execution: %v\\n%s\", err, stack)\n\t\t\t\t}\n\t\t\t}()\n\n\t\t\tsubCheckRes := runner(state)\n\n\t\t\t// We check the length because we don't want to take locks\n\t\t\t// when it is not necessary.\n\t\t\tif len(subCheckRes.AuthResult) != 0 {\n\t\t\t\tdata.authResLock.Lock()\n\t\t\t\tcr.mergedRes.AuthResult = append(cr.mergedRes.AuthResult, subCheckRes.AuthResult...)\n\t\t\t\tdata.authResLock.Unlock()\n\t\t\t}\n\t\t\tif subCheckRes.Header.Len() != 0 {\n\t\t\t\tdata.headerLock.Lock()\n\t\t\t\tfor field := subCheckRes.Header.Fields(); field.Next(); {\n\t\t\t\t\tformatted, err := field.Raw()\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tcr.log.Error(\"malformed header field added by check\", err)\n\t\t\t\t\t}\n\t\t\t\t\tcr.mergedRes.Header.AddRaw(formatted)\n\t\t\t\t}\n\t\t\t\tdata.headerLock.Unlock()\n\t\t\t}\n\n\t\t\tif subCheckRes.Quarantine {\n\t\t\t\tdata.setQuarantineErr.Do(func() {\n\t\t\t\t\tdata.quarantineErr = subCheckRes.Reason\n\t\t\t\t})\n\t\t\t} else if subCheckRes.Reject {\n\t\t\t\tdata.setRejectErr.Do(func() {\n\t\t\t\t\tdata.rejectErr = subCheckRes.Reason\n\t\t\t\t})\n\t\t\t} else if subCheckRes.Reason != nil {\n\t\t\t\t// 'action ignore' case. There is Reason, but action.Apply set\n\t\t\t\t// both Reject and Quarantine to false. Log the reason for\n\t\t\t\t// purposes of deployment testing.\n\t\t\t\tcr.log.Error(\"no check action\", subCheckRes.Reason)\n\t\t\t}\n\t\t}()\n\t}\n\n\tdata.wg.Wait()\n\tif data.rejectErr != nil {\n\t\treturn data.rejectErr\n\t}\n\n\tif data.quarantineErr != nil {\n\t\tcr.log.Error(\"quarantined\", data.quarantineErr)\n\t\tcr.mergedRes.Quarantine = true\n\t}\n\n\treturn nil\n}\n\nfunc (cr *checkRunner) checkConnSender(ctx context.Context, checks []module.Check, mailFrom string) error {\n\tcr.mailFrom = mailFrom\n\tcr.mailFromReceived = true\n\n\t// checkStates will run CheckConnection and CheckSender.\n\t_, err := cr.checkStates(ctx, checks)\n\treturn err\n}\n\nfunc (cr *checkRunner) checkRcpt(ctx context.Context, checks []module.Check, rcptTo string) error {\n\tstates, err := cr.checkStates(ctx, checks)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = cr.runAndMergeResults(states, func(s module.CheckState) module.CheckResult {\n\t\tcr.checkedRcptsLock.Lock()\n\t\tif _, ok := cr.checkedRcptsPerCheck[s][rcptTo]; ok {\n\t\t\tcr.checkedRcptsLock.Unlock()\n\t\t\treturn module.CheckResult{}\n\t\t}\n\t\tif cr.checkedRcptsPerCheck[s] == nil {\n\t\t\tcr.checkedRcptsPerCheck[s] = make(map[string]struct{})\n\t\t}\n\t\tcr.checkedRcptsPerCheck[s][rcptTo] = struct{}{}\n\t\tcr.checkedRcptsLock.Unlock()\n\n\t\tres := s.CheckRcpt(ctx, rcptTo)\n\t\treturn res\n\t})\n\n\tcr.checkedRcpts = append(cr.checkedRcpts, rcptTo)\n\treturn err\n}\n\nfunc (cr *checkRunner) checkBody(ctx context.Context, checks []module.Check, header textproto.Header, body buffer.Buffer) error {\n\tstates, err := cr.checkStates(ctx, checks)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif cr.doDMARC && !cr.didDMARCFetch {\n\t\tcr.dmarcVerify.FetchRecord(ctx, header)\n\t\tcr.didDMARCFetch = true\n\t}\n\n\treturn cr.runAndMergeResults(states, func(s module.CheckState) module.CheckResult {\n\t\tres := s.CheckBody(ctx, header, body)\n\t\treturn res\n\t})\n}\n\nfunc (cr *checkRunner) applyResults(hostname string, header *textproto.Header) error {\n\tif cr.mergedRes.Quarantine {\n\t\tcr.msgMeta.Quarantine = true\n\t}\n\n\tif cr.doDMARC {\n\t\tdmarcRes, policy := cr.dmarcVerify.Apply(cr.mergedRes.AuthResult)\n\t\tcr.mergedRes.AuthResult = append(cr.mergedRes.AuthResult, &dmarcRes.Authres)\n\t\tswitch policy {\n\t\tcase dmarc.PolicyReject:\n\t\t\tcode := 550\n\t\t\tenchCode := exterrors.EnhancedCode{5, 7, 1}\n\t\t\tif dmarcRes.Authres.Value == authres.ResultTempError {\n\t\t\t\tcode = 450\n\t\t\t\tenchCode[0] = 4\n\t\t\t}\n\t\t\treturn &exterrors.SMTPError{\n\t\t\t\tCode:         code,\n\t\t\t\tEnhancedCode: enchCode,\n\t\t\t\tMessage:      \"DMARC check failed\",\n\t\t\t\tCheckName:    \"dmarc\",\n\t\t\t\tMisc: map[string]interface{}{\n\t\t\t\t\t\"reason\":      dmarcRes.Authres.Reason,\n\t\t\t\t\t\"dkim_res\":    dmarcRes.DKIMResult.Value,\n\t\t\t\t\t\"dkim_domain\": dmarcRes.DKIMResult.Domain,\n\t\t\t\t\t\"spf_res\":     dmarcRes.SPFResult.Value,\n\t\t\t\t\t\"spf_from\":    dmarcRes.SPFResult.From,\n\t\t\t\t},\n\t\t\t}\n\t\tcase dmarc.PolicyQuarantine:\n\t\t\tcr.msgMeta.Quarantine = true\n\n\t\t\t// Mimick the message structure for regular checks.\n\t\t\tcr.log.Msg(\"quarantined\", \"reason\", dmarcRes.Authres.Reason, \"check\", \"dmarc\")\n\t\t}\n\t}\n\n\t// After results for all checks are checked, authRes will be populated with values\n\t// we should put into Authentication-Results header.\n\tif len(cr.mergedRes.AuthResult) != 0 {\n\t\theader.Add(\"Authentication-Results\", authres.Format(hostname, cr.mergedRes.AuthResult))\n\t}\n\n\tfor field := cr.mergedRes.Header.Fields(); field.Next(); {\n\t\tformatted, err := field.Raw()\n\t\tif err != nil {\n\t\t\tcr.log.Error(\"malformed header field added by check\", err)\n\t\t}\n\t\theader.AddRaw(formatted)\n\t}\n\treturn nil\n}\n\nfunc (cr *checkRunner) close() {\n\tif err := cr.dmarcVerify.Close(); err != nil {\n\t\tcr.log.Error(\"failed to close dmarc verify state\", err)\n\t}\n\tfor _, state := range cr.states {\n\t\tif err := state.Close(); err != nil {\n\t\t\tcr.log.Error(\"failed to close check state\", err)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/msgpipeline/check_test.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage msgpipeline\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\n\t\"github.com/emersion/go-message/textproto\"\n\t\"github.com/emersion/go-msgauth/authres\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/internal/testutils\"\n)\n\nfunc TestMsgPipeline_Checks(t *testing.T) {\n\ttarget := testutils.Target{}\n\tcheck1, check2 := testutils.Check{}, testutils.Check{}\n\td := MsgPipeline{\n\t\tmsgpipelineCfg: msgpipelineCfg{\n\t\t\tglobalChecks: []module.Check{&check1, &check2},\n\t\t\tperSource:    map[string]sourceBlock{},\n\t\t\tdefaultSource: sourceBlock{\n\t\t\t\tperRcpt: map[string]*rcptBlock{},\n\t\t\t\tdefaultRcpt: &rcptBlock{\n\t\t\t\t\ttargets: []module.DeliveryTarget{&target},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tLog: testutils.Logger(t, \"msgpipeline\"),\n\t}\n\n\ttestutils.DoTestDelivery(t, &d, \"whatever@whatever\", []string{\"whatever@whatever\"})\n\n\tif len(target.Messages) != 1 {\n\t\tt.Fatalf(\"wrong amount of messages received, want %d, got %d\", 1, len(target.Messages))\n\t}\n\tif target.Messages[0].MsgMeta.Quarantine {\n\t\tt.Fatalf(\"message is quarantined when it shouldn't\")\n\t}\n\n\tif check1.UnclosedStates != 0 || check2.UnclosedStates != 0 {\n\t\tt.Fatalf(\"checks state objects leak or double-closed, alive counters: %v, %v\", check1.UnclosedStates, check2.UnclosedStates)\n\t}\n}\n\nfunc TestMsgPipeline_AuthResults(t *testing.T) {\n\ttarget := testutils.Target{}\n\tcheck1, check2 := testutils.Check{\n\t\tBodyRes: module.CheckResult{\n\t\t\tAuthResult: []authres.Result{\n\t\t\t\t&authres.SPFResult{\n\t\t\t\t\tValue: authres.ResultFail,\n\t\t\t\t\tFrom:  \"FROM\",\n\t\t\t\t\tHelo:  \"HELO\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}, testutils.Check{\n\t\tBodyRes: module.CheckResult{\n\t\t\tAuthResult: []authres.Result{\n\t\t\t\t&authres.SPFResult{\n\t\t\t\t\tValue: authres.ResultFail,\n\t\t\t\t\tFrom:  \"FROM2\",\n\t\t\t\t\tHelo:  \"HELO2\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\td := MsgPipeline{\n\t\tmsgpipelineCfg: msgpipelineCfg{\n\t\t\tglobalChecks: []module.Check{&check1, &check2},\n\t\t\tperSource:    map[string]sourceBlock{},\n\t\t\tdefaultSource: sourceBlock{\n\t\t\t\tperRcpt: map[string]*rcptBlock{},\n\t\t\t\tdefaultRcpt: &rcptBlock{\n\t\t\t\t\ttargets: []module.DeliveryTarget{&target},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tHostname: \"TEST-HOST\",\n\t\tLog:      testutils.Logger(t, \"msgpipeline\"),\n\t}\n\n\ttestutils.DoTestDelivery(t, &d, \"whatever@whatever\", []string{\"whatever@whatever\"})\n\n\tif len(target.Messages) != 1 {\n\t\tt.Fatalf(\"wrong amount of messages received, want %d, got %d\", 1, len(target.Messages))\n\t}\n\n\tauthRes := target.Messages[0].Header.Get(\"Authentication-Results\")\n\tid, parsed, err := authres.Parse(authRes)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to parse results\")\n\t}\n\tif id != \"TEST-HOST\" {\n\t\tt.Fatalf(\"wrong authres identifier\")\n\t}\n\tif len(parsed) != 2 {\n\t\tt.Fatalf(\"wrong amount of parts, want %d, got %d\", 2, len(parsed))\n\t}\n\n\tvar seen1, seen2 bool\n\tfor _, parts := range parsed {\n\t\tspfPart, ok := parts.(*authres.SPFResult)\n\t\tif !ok {\n\t\t\tt.Fatalf(\"Not SPFResult\")\n\t\t}\n\n\t\tif spfPart.From == \"FROM\" {\n\t\t\tseen1 = true\n\t\t}\n\t\tif spfPart.From == \"FROM2\" {\n\t\t\tseen2 = true\n\t\t}\n\t}\n\n\tif !seen1 {\n\t\tt.Fatalf(\"First authRes is missing\")\n\t}\n\tif !seen2 {\n\t\tt.Fatalf(\"Second authRes is missing\")\n\t}\n\n\tif check1.UnclosedStates != 0 || check2.UnclosedStates != 0 {\n\t\tt.Fatalf(\"checks state objects leak or double-closed, alive counters: %v, %v\", check1.UnclosedStates, check2.UnclosedStates)\n\t}\n}\n\nfunc TestMsgPipeline_Headers(t *testing.T) {\n\thdr1 := textproto.Header{}\n\thdr1.Add(\"HDR1\", \"1\")\n\thdr2 := textproto.Header{}\n\thdr2.Add(\"HDR2\", \"2\")\n\n\ttarget := testutils.Target{}\n\tcheck1, check2 := testutils.Check{\n\t\tBodyRes: module.CheckResult{\n\t\t\tHeader: hdr1,\n\t\t},\n\t}, testutils.Check{\n\t\tBodyRes: module.CheckResult{\n\t\t\tHeader: hdr2,\n\t\t},\n\t}\n\td := MsgPipeline{\n\t\tmsgpipelineCfg: msgpipelineCfg{\n\t\t\tglobalChecks: []module.Check{&check1, &check2},\n\t\t\tperSource:    map[string]sourceBlock{},\n\t\t\tdefaultSource: sourceBlock{\n\t\t\t\tperRcpt: map[string]*rcptBlock{},\n\t\t\t\tdefaultRcpt: &rcptBlock{\n\t\t\t\t\ttargets: []module.DeliveryTarget{&target},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tHostname: \"TEST-HOST\",\n\t\tLog:      testutils.Logger(t, \"msgpipeline\"),\n\t}\n\n\ttestutils.DoTestDelivery(t, &d, \"whatever@whatever\", []string{\"whatever@whatever\"})\n\n\tif len(target.Messages) != 1 {\n\t\tt.Fatalf(\"wrong amount of messages received, want %d, got %d\", 1, len(target.Messages))\n\t}\n\n\tif target.Messages[0].Header.Get(\"HDR1\") != \"1\" {\n\t\tt.Fatalf(\"wrong HDR1 value, want %s, got %s\", \"1\", target.Messages[0].Header.Get(\"HDR1\"))\n\t}\n\tif target.Messages[0].Header.Get(\"HDR2\") != \"2\" {\n\t\tt.Fatalf(\"wrong HDR2 value, want %s, got %s\", \"1\", target.Messages[0].Header.Get(\"HDR2\"))\n\t}\n\n\tif check1.UnclosedStates != 0 || check2.UnclosedStates != 0 {\n\t\tt.Fatalf(\"checks state objects leak or double-closed, alive counters: %v, %v\", check1.UnclosedStates, check2.UnclosedStates)\n\t}\n}\n\nfunc TestMsgPipeline_Globalcheck_Errors(t *testing.T) {\n\ttarget := testutils.Target{}\n\tcheck_ := testutils.Check{\n\t\tInitErr:   errors.New(\"1\"),\n\t\tConnRes:   module.CheckResult{Reject: true, Reason: errors.New(\"2\")},\n\t\tSenderRes: module.CheckResult{Reject: true, Reason: errors.New(\"3\")},\n\t\tRcptRes:   module.CheckResult{Reject: true, Reason: errors.New(\"4\")},\n\t\tBodyRes:   module.CheckResult{Reject: true, Reason: errors.New(\"5\")},\n\t}\n\td := MsgPipeline{\n\t\tmsgpipelineCfg: msgpipelineCfg{\n\t\t\tglobalChecks: []module.Check{&check_},\n\t\t\tperSource:    map[string]sourceBlock{},\n\t\t\tdefaultSource: sourceBlock{\n\t\t\t\tperRcpt: map[string]*rcptBlock{},\n\t\t\t\tdefaultRcpt: &rcptBlock{\n\t\t\t\t\ttargets: []module.DeliveryTarget{&target},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tHostname: \"TEST-HOST\",\n\t\tLog:      testutils.Logger(t, \"msgpipeline\"),\n\t}\n\n\tt.Run(\"init err\", func(t *testing.T) {\n\t\t_, err := testutils.DoTestDeliveryErr(t, &d, \"sender@example.com\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"})\n\t\tif err == nil {\n\t\t\tt.Fatal(\"expected error\")\n\t\t}\n\t})\n\n\tcheck_.InitErr = nil\n\n\tt.Run(\"conn err\", func(t *testing.T) {\n\t\t_, err := testutils.DoTestDeliveryErr(t, &d, \"sender@example.com\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"})\n\t\tif err == nil {\n\t\t\tt.Fatal(\"expected error\")\n\t\t}\n\t})\n\n\tcheck_.ConnRes.Reject = false\n\n\tt.Run(\"mail from err\", func(t *testing.T) {\n\t\t_, err := testutils.DoTestDeliveryErr(t, &d, \"sender@example.com\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"})\n\t\tif err == nil {\n\t\t\tt.Fatal(\"expected error\")\n\t\t}\n\t})\n\n\tcheck_.SenderRes.Reject = false\n\n\tt.Run(\"rcpt to err\", func(t *testing.T) {\n\t\t_, err := testutils.DoTestDeliveryErr(t, &d, \"sender@example.com\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"})\n\t\tif err == nil {\n\t\t\tt.Fatal(\"expected error\")\n\t\t}\n\t})\n\n\tcheck_.RcptRes.Reject = false\n\n\tt.Run(\"body err\", func(t *testing.T) {\n\t\t_, err := testutils.DoTestDeliveryErr(t, &d, \"sender@example.com\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"})\n\t\tif err == nil {\n\t\t\tt.Fatal(\"expected error\")\n\t\t}\n\t})\n\n\tcheck_.BodyRes.Reject = false\n\n\tt.Run(\"no err\", func(t *testing.T) {\n\t\ttestutils.DoTestDelivery(t, &d, \"sender@example.com\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"})\n\t})\n\n\tif check_.UnclosedStates != 0 {\n\t\tt.Fatalf(\"check state objects leak or double-closed, counters: %d\", check_.UnclosedStates)\n\t}\n}\n\nfunc TestMsgPipeline_SourceCheck_Errors(t *testing.T) {\n\ttarget := testutils.Target{}\n\tcheck_ := testutils.Check{\n\t\tInitErr:   errors.New(\"1\"),\n\t\tConnRes:   module.CheckResult{Reject: true, Reason: errors.New(\"2\")},\n\t\tSenderRes: module.CheckResult{Reject: true, Reason: errors.New(\"3\")},\n\t\tRcptRes:   module.CheckResult{Reject: true, Reason: errors.New(\"4\")},\n\t\tBodyRes:   module.CheckResult{Reject: true, Reason: errors.New(\"5\")},\n\t}\n\tglobalCheck := testutils.Check{}\n\td := MsgPipeline{\n\t\tmsgpipelineCfg: msgpipelineCfg{\n\t\t\tglobalChecks: []module.Check{&globalCheck},\n\t\t\tperSource:    map[string]sourceBlock{},\n\t\t\tdefaultSource: sourceBlock{\n\t\t\t\tperRcpt: map[string]*rcptBlock{},\n\t\t\t\tchecks:  []module.Check{&check_},\n\t\t\t\tdefaultRcpt: &rcptBlock{\n\t\t\t\t\ttargets: []module.DeliveryTarget{&target},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tHostname: \"TEST-HOST\",\n\t\tLog:      testutils.Logger(t, \"msgpipeline\"),\n\t}\n\n\tt.Run(\"init err\", func(t *testing.T) {\n\t\t_, err := testutils.DoTestDeliveryErr(t, &d, \"sender@example.com\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"})\n\t\tif err == nil {\n\t\t\tt.Fatal(\"expected error\")\n\t\t}\n\t})\n\n\tcheck_.InitErr = nil\n\n\tt.Run(\"conn err\", func(t *testing.T) {\n\t\t_, err := testutils.DoTestDeliveryErr(t, &d, \"sender@example.com\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"})\n\t\tif err == nil {\n\t\t\tt.Fatal(\"expected error\")\n\t\t}\n\t})\n\n\tcheck_.ConnRes.Reject = false\n\n\tt.Run(\"mail from err\", func(t *testing.T) {\n\t\t_, err := testutils.DoTestDeliveryErr(t, &d, \"sender@example.com\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"})\n\t\tif err == nil {\n\t\t\tt.Fatal(\"expected error\")\n\t\t}\n\t})\n\n\tcheck_.SenderRes.Reject = false\n\n\tt.Run(\"rcpt to err\", func(t *testing.T) {\n\t\t_, err := testutils.DoTestDeliveryErr(t, &d, \"sender@example.com\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"})\n\t\tif err == nil {\n\t\t\tt.Fatal(\"expected error\")\n\t\t}\n\t})\n\n\tcheck_.RcptRes.Reject = false\n\n\tt.Run(\"body err\", func(t *testing.T) {\n\t\t_, err := testutils.DoTestDeliveryErr(t, &d, \"sender@example.com\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"})\n\t\tif err == nil {\n\t\t\tt.Fatal(\"expected error\")\n\t\t}\n\t})\n\n\tcheck_.BodyRes.Reject = false\n\n\tt.Run(\"no err\", func(t *testing.T) {\n\t\ttestutils.DoTestDelivery(t, &d, \"sender@example.com\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"})\n\t})\n\n\tif check_.UnclosedStates != 0 || globalCheck.UnclosedStates != 0 {\n\t\tt.Fatalf(\"check state objects leak or double-closed, counters: %d, %d\",\n\t\t\tcheck_.UnclosedStates, globalCheck.UnclosedStates)\n\t}\n}\n\nfunc TestMsgPipeline_RcptCheck_Errors(t *testing.T) {\n\ttarget := testutils.Target{}\n\tcheck_ := testutils.Check{\n\t\tInitErr:   errors.New(\"1\"),\n\t\tConnRes:   module.CheckResult{Reject: true, Reason: errors.New(\"2\")},\n\t\tSenderRes: module.CheckResult{Reject: true, Reason: errors.New(\"3\")},\n\t\tRcptRes:   module.CheckResult{Reject: true, Reason: errors.New(\"4\")},\n\t\tBodyRes:   module.CheckResult{Reject: true, Reason: errors.New(\"5\")},\n\n\t\tInstName: \"err_check\",\n\t}\n\t// Added to check whether it leaks.\n\tglobalCheck := testutils.Check{InstName: \"global_check\"}\n\tsourceCheck := testutils.Check{InstName: \"source_check\"}\n\td := MsgPipeline{\n\t\tmsgpipelineCfg: msgpipelineCfg{\n\t\t\tglobalChecks: []module.Check{&globalCheck},\n\t\t\tperSource:    map[string]sourceBlock{},\n\t\t\tdefaultSource: sourceBlock{\n\t\t\t\tperRcpt: map[string]*rcptBlock{},\n\t\t\t\tchecks:  []module.Check{&check_},\n\t\t\t\tdefaultRcpt: &rcptBlock{\n\t\t\t\t\ttargets: []module.DeliveryTarget{&target},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tHostname: \"TEST-HOST\",\n\t\tLog:      testutils.Logger(t, \"msgpipeline\"),\n\t}\n\n\tt.Run(\"init err\", func(t *testing.T) {\n\t\td.Log = testutils.Logger(t, \"msgpipeline\")\n\t\t_, err := testutils.DoTestDeliveryErr(t, &d, \"sender@example.com\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"})\n\t\tif err == nil {\n\t\t\tt.Fatal(\"expected error\")\n\t\t}\n\n\t\tt.Log(\"!!!\", check_.UnclosedStates)\n\t})\n\n\tcheck_.InitErr = nil\n\n\tt.Run(\"conn err\", func(t *testing.T) {\n\t\td.Log = testutils.Logger(t, \"msgpipeline\")\n\t\t_, err := testutils.DoTestDeliveryErr(t, &d, \"sender@example.com\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"})\n\t\tif err == nil {\n\t\t\tt.Fatal(\"expected error\")\n\t\t}\n\n\t\tt.Log(\"!!!\", check_.UnclosedStates)\n\t})\n\n\tcheck_.ConnRes.Reject = false\n\n\tt.Run(\"mail from err\", func(t *testing.T) {\n\t\td.Log = testutils.Logger(t, \"msgpipeline\")\n\t\t_, err := testutils.DoTestDeliveryErr(t, &d, \"sender@example.com\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"})\n\t\tif err == nil {\n\t\t\tt.Fatal(\"expected error\")\n\t\t}\n\n\t\tt.Log(\"!!!\", check_.UnclosedStates)\n\t})\n\n\tcheck_.SenderRes.Reject = false\n\n\tt.Run(\"rcpt to err\", func(t *testing.T) {\n\t\td.Log = testutils.Logger(t, \"msgpipeline\")\n\t\t_, err := testutils.DoTestDeliveryErr(t, &d, \"sender@example.com\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"})\n\t\tif err == nil {\n\t\t\tt.Fatal(\"expected error\")\n\t\t}\n\t})\n\n\tcheck_.RcptRes.Reject = false\n\n\tt.Run(\"body err\", func(t *testing.T) {\n\t\td.Log = testutils.Logger(t, \"msgpipeline\")\n\t\t_, err := testutils.DoTestDeliveryErr(t, &d, \"sender@example.com\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"})\n\t\tif err == nil {\n\t\t\tt.Fatal(\"expected error\")\n\t\t}\n\t})\n\n\tcheck_.BodyRes.Reject = false\n\n\tt.Run(\"no err\", func(t *testing.T) {\n\t\td.Log = testutils.Logger(t, \"msgpipeline\")\n\t\ttestutils.DoTestDelivery(t, &d, \"sender@example.com\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"})\n\t})\n\n\tif check_.UnclosedStates != 0 || sourceCheck.UnclosedStates != 0 || globalCheck.UnclosedStates != 0 {\n\t\tt.Fatalf(\"check state objects leak or double-closed, counters: %d, %d, %d\",\n\t\t\tcheck_.UnclosedStates, sourceCheck.UnclosedStates, globalCheck.UnclosedStates)\n\t}\n}\n"
  },
  {
    "path": "internal/msgpipeline/config.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage msgpipeline\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/foxcpp/maddy/framework/address\"\n\t\"github.com/foxcpp/maddy/framework/config\"\n\tmodconfig \"github.com/foxcpp/maddy/framework/config/module\"\n\t\"github.com/foxcpp/maddy/framework/dns\"\n\t\"github.com/foxcpp/maddy/framework/exterrors\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/internal/modify\"\n)\n\ntype sourceIn struct {\n\tt     module.Table\n\tblock sourceBlock\n}\n\ntype msgpipelineCfg struct {\n\tglobalChecks    []module.Check\n\tglobalModifiers modify.Group\n\tsourceIn        []sourceIn\n\tperSource       map[string]sourceBlock\n\tdefaultSource   sourceBlock\n\tdoDMARC         bool\n}\n\nfunc parseMsgPipelineRootCfg(globals map[string]interface{}, nodes []config.Node) (msgpipelineCfg, error) {\n\tcfg := msgpipelineCfg{\n\t\tperSource: map[string]sourceBlock{},\n\t}\n\tvar defaultSrcRaw []config.Node\n\tvar othersRaw []config.Node\n\tfor _, node := range nodes {\n\t\tswitch node.Name {\n\t\tcase \"check\":\n\t\t\tglobalChecks, err := parseChecksGroup(globals, node)\n\t\t\tif err != nil {\n\t\t\t\treturn msgpipelineCfg{}, err\n\t\t\t}\n\n\t\t\tcfg.globalChecks = append(cfg.globalChecks, globalChecks...)\n\t\tcase \"modify\":\n\t\t\tglobalModifiers, err := parseModifiersGroup(globals, node)\n\t\t\tif err != nil {\n\t\t\t\treturn msgpipelineCfg{}, err\n\t\t\t}\n\n\t\t\tcfg.globalModifiers.Modifiers = append(cfg.globalModifiers.Modifiers, globalModifiers.Modifiers...)\n\t\tcase \"source_in\":\n\t\t\tvar tbl module.Table\n\t\t\tif err := modconfig.ModuleFromNode(\"table\", node.Args, config.Node{}, globals, &tbl); err != nil {\n\t\t\t\treturn msgpipelineCfg{}, err\n\t\t\t}\n\t\t\tsrcBlock, err := parseMsgPipelineSrcCfg(globals, node.Children)\n\t\t\tif err != nil {\n\t\t\t\treturn msgpipelineCfg{}, err\n\t\t\t}\n\t\t\tcfg.sourceIn = append(cfg.sourceIn, sourceIn{\n\t\t\t\tt:     tbl,\n\t\t\t\tblock: srcBlock,\n\t\t\t})\n\t\tcase \"source\":\n\t\t\tsrcBlock, err := parseMsgPipelineSrcCfg(globals, node.Children)\n\t\t\tif err != nil {\n\t\t\t\treturn msgpipelineCfg{}, err\n\t\t\t}\n\n\t\t\tif len(node.Args) == 0 {\n\t\t\t\treturn msgpipelineCfg{}, config.NodeErr(node, \"expected at least one source matching rule\")\n\t\t\t}\n\n\t\t\tfor _, rule := range node.Args {\n\t\t\t\tif strings.Contains(rule, \"@\") {\n\t\t\t\t\trule, err = address.ForLookup(rule)\n\t\t\t\t} else {\n\t\t\t\t\trule, err = dns.ForLookup(rule)\n\t\t\t\t}\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn msgpipelineCfg{}, config.NodeErr(node, \"invalid source match rule: %v: %v\", rule, err)\n\t\t\t\t}\n\n\t\t\t\tif !validMatchRule(rule) {\n\t\t\t\t\treturn msgpipelineCfg{}, config.NodeErr(node, \"invalid source routing rule: %v\", rule)\n\t\t\t\t}\n\n\t\t\t\tif _, ok := cfg.perSource[rule]; ok {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tcfg.perSource[rule] = srcBlock\n\t\t\t}\n\t\tcase \"default_source\":\n\t\t\tif defaultSrcRaw != nil {\n\t\t\t\treturn msgpipelineCfg{}, config.NodeErr(node, \"duplicate 'default_source' block\")\n\t\t\t}\n\t\t\tdefaultSrcRaw = node.Children\n\t\tcase \"dmarc\":\n\t\t\tswitch len(node.Args) {\n\t\t\tcase 1:\n\t\t\t\tswitch node.Args[0] {\n\t\t\t\tcase \"yes\":\n\t\t\t\t\tcfg.doDMARC = true\n\t\t\t\tcase \"no\":\n\t\t\t\tdefault:\n\t\t\t\t\treturn msgpipelineCfg{}, config.NodeErr(node, \"invalid argument for dmarc\")\n\t\t\t\t}\n\t\t\tcase 0:\n\t\t\t\tcfg.doDMARC = true\n\t\t\t}\n\t\tcase \"deliver_to\", \"reroute\", \"destination_in\", \"destination\", \"default_destination\", \"reject\":\n\t\t\tothersRaw = append(othersRaw, node)\n\t\tdefault:\n\t\t\treturn msgpipelineCfg{}, config.NodeErr(node, \"unknown pipeline directive: %s\", node.Name)\n\t\t}\n\t}\n\n\tif len(cfg.perSource) == 0 && len(defaultSrcRaw) == 0 {\n\t\tif len(othersRaw) == 0 {\n\t\t\treturn msgpipelineCfg{}, fmt.Errorf(\"empty pipeline configuration, use 'reject' to reject messages\")\n\t\t}\n\n\t\tvar err error\n\t\tcfg.defaultSource, err = parseMsgPipelineSrcCfg(globals, othersRaw)\n\t\treturn cfg, err\n\t} else if len(othersRaw) != 0 {\n\t\treturn msgpipelineCfg{}, config.NodeErr(othersRaw[0], \"can't put handling directives together with source rules, did you mean to put it into 'default_source' block or into all source blocks?\")\n\t}\n\n\tif len(defaultSrcRaw) == 0 {\n\t\treturn msgpipelineCfg{}, config.NodeErr(nodes[0], \"missing or empty default source block, use default_source { reject } to reject messages\")\n\t}\n\n\tvar err error\n\tcfg.defaultSource, err = parseMsgPipelineSrcCfg(globals, defaultSrcRaw)\n\treturn cfg, err\n}\n\nfunc parseMsgPipelineSrcCfg(globals map[string]interface{}, nodes []config.Node) (sourceBlock, error) {\n\tsrc := sourceBlock{\n\t\tperRcpt: map[string]*rcptBlock{},\n\t}\n\tvar defaultRcptRaw []config.Node\n\tvar othersRaw []config.Node\n\tfor _, node := range nodes {\n\t\tswitch node.Name {\n\t\tcase \"check\":\n\t\t\tchecks, err := parseChecksGroup(globals, node)\n\t\t\tif err != nil {\n\t\t\t\treturn sourceBlock{}, err\n\t\t\t}\n\n\t\t\tsrc.checks = append(src.checks, checks...)\n\t\tcase \"modify\":\n\t\t\tmodifiers, err := parseModifiersGroup(globals, node)\n\t\t\tif err != nil {\n\t\t\t\treturn sourceBlock{}, err\n\t\t\t}\n\n\t\t\tsrc.modifiers.Modifiers = append(src.modifiers.Modifiers, modifiers.Modifiers...)\n\t\tcase \"destination_in\":\n\t\t\tvar tbl module.Table\n\t\t\tif err := modconfig.ModuleFromNode(\"table\", node.Args, config.Node{}, globals, &tbl); err != nil {\n\t\t\t\treturn sourceBlock{}, err\n\t\t\t}\n\t\t\trcptBlock, err := parseMsgPipelineRcptCfg(globals, node.Children)\n\t\t\tif err != nil {\n\t\t\t\treturn sourceBlock{}, err\n\t\t\t}\n\t\t\tsrc.rcptIn = append(src.rcptIn, rcptIn{\n\t\t\t\tt:     tbl,\n\t\t\t\tblock: rcptBlock,\n\t\t\t})\n\t\tcase \"destination\":\n\t\t\trcptBlock, err := parseMsgPipelineRcptCfg(globals, node.Children)\n\t\t\tif err != nil {\n\t\t\t\treturn sourceBlock{}, err\n\t\t\t}\n\n\t\t\tif len(node.Args) == 0 {\n\t\t\t\treturn sourceBlock{}, config.NodeErr(node, \"expected at least one destination match rule\")\n\t\t\t}\n\n\t\t\tfor _, rule := range node.Args {\n\t\t\t\tif strings.Contains(rule, \"@\") {\n\t\t\t\t\trule, err = address.ForLookup(rule)\n\t\t\t\t} else {\n\t\t\t\t\trule, err = dns.ForLookup(rule)\n\t\t\t\t}\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn sourceBlock{}, config.NodeErr(node, \"invalid destination match rule: %v: %v\", rule, err)\n\t\t\t\t}\n\n\t\t\t\tif !validMatchRule(rule) {\n\t\t\t\t\treturn sourceBlock{}, config.NodeErr(node, \"invalid destination match rule: %v\", rule)\n\t\t\t\t}\n\n\t\t\t\tif _, ok := src.perRcpt[rule]; ok {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tsrc.perRcpt[rule] = rcptBlock\n\t\t\t}\n\t\tcase \"default_destination\":\n\t\t\tif defaultRcptRaw != nil {\n\t\t\t\treturn sourceBlock{}, config.NodeErr(node, \"duplicate 'default_destination' block\")\n\t\t\t}\n\t\t\tdefaultRcptRaw = node.Children\n\t\tcase \"deliver_to\", \"reroute\", \"reject\":\n\t\t\tothersRaw = append(othersRaw, node)\n\t\tdefault:\n\t\t\treturn sourceBlock{}, config.NodeErr(node, \"unknown pipeline directive: %s\", node.Name)\n\t\t}\n\t}\n\n\tif len(src.perRcpt) == 0 && len(defaultRcptRaw) == 0 {\n\t\tif len(othersRaw) == 0 {\n\t\t\treturn sourceBlock{}, fmt.Errorf(\"empty source block, use 'reject' to reject messages\")\n\t\t}\n\n\t\tvar err error\n\t\tsrc.defaultRcpt, err = parseMsgPipelineRcptCfg(globals, othersRaw)\n\t\treturn src, err\n\t} else if len(othersRaw) != 0 {\n\t\treturn sourceBlock{}, config.NodeErr(othersRaw[0], \"can't put handling directives together with destination rules, did you mean to put it into 'default' block or into all recipient blocks?\")\n\t}\n\n\tif len(defaultRcptRaw) == 0 {\n\t\treturn sourceBlock{}, config.NodeErr(nodes[0], \"missing or empty default destination block, use default_destination { reject } to reject messages\")\n\t}\n\n\tvar err error\n\tsrc.defaultRcpt, err = parseMsgPipelineRcptCfg(globals, defaultRcptRaw)\n\treturn src, err\n}\n\nfunc parseMsgPipelineRcptCfg(globals map[string]interface{}, nodes []config.Node) (*rcptBlock, error) {\n\trcpt := rcptBlock{}\n\tfor _, node := range nodes {\n\t\tswitch node.Name {\n\t\tcase \"check\":\n\t\t\tchecks, err := parseChecksGroup(globals, node)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\trcpt.checks = append(rcpt.checks, checks...)\n\t\tcase \"modify\":\n\t\t\tmodifiers, err := parseModifiersGroup(globals, node)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\trcpt.modifiers.Modifiers = append(rcpt.modifiers.Modifiers, modifiers.Modifiers...)\n\t\tcase \"deliver_to\":\n\t\t\tif rcpt.rejectErr != nil {\n\t\t\t\treturn nil, config.NodeErr(node, \"can't use 'reject' and 'deliver_to' together\")\n\t\t\t}\n\n\t\t\tif len(node.Args) == 0 {\n\t\t\t\treturn nil, config.NodeErr(node, \"required at least one argument\")\n\t\t\t}\n\t\t\tmod, err := modconfig.DeliveryTarget(globals, node.Args, node)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\trcpt.targets = append(rcpt.targets, mod)\n\t\tcase \"reroute\":\n\t\t\tif len(node.Children) == 0 {\n\t\t\t\treturn nil, config.NodeErr(node, \"missing or empty reroute pipeline configuration\")\n\t\t\t}\n\n\t\t\tpipeline, err := New(globals, node.Children)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\trcpt.targets = append(rcpt.targets, pipeline)\n\t\tcase \"reject\":\n\t\t\tif len(rcpt.targets) != 0 {\n\t\t\t\treturn nil, config.NodeErr(node, \"can't use 'reject' and 'deliver_to' together\")\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\trcpt.rejectErr, err = parseRejectDirective(node)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\tdefault:\n\t\t\treturn nil, config.NodeErr(node, \"invalid directive\")\n\t\t}\n\t}\n\treturn &rcpt, nil\n}\n\nfunc parseRejectDirective(node config.Node) (*exterrors.SMTPError, error) {\n\tcode := 554\n\tenchCode := exterrors.EnhancedCode{5, 7, 0}\n\tmsg := \"Message rejected due to a local policy\"\n\tvar err error\n\tswitch len(node.Args) {\n\tcase 3:\n\t\tmsg = node.Args[2]\n\t\tif msg == \"\" {\n\t\t\treturn nil, config.NodeErr(node, \"message can't be empty\")\n\t\t}\n\t\tfallthrough\n\tcase 2:\n\t\tenchCode, err = parseEnhancedCode(node.Args[1])\n\t\tif err != nil {\n\t\t\treturn nil, config.NodeErr(node, \"%v\", err)\n\t\t}\n\t\tif enchCode[0] != 4 && enchCode[0] != 5 {\n\t\t\treturn nil, config.NodeErr(node, \"enhanced code should use either 4 or 5 as a first number\")\n\t\t}\n\t\tfallthrough\n\tcase 1:\n\t\tcode, err = strconv.Atoi(node.Args[0])\n\t\tif err != nil {\n\t\t\treturn nil, config.NodeErr(node, \"invalid error code integer: %v\", err)\n\t\t}\n\t\tif (code/100) != 4 && (code/100) != 5 {\n\t\t\treturn nil, config.NodeErr(node, \"error code should start with either 4 or 5\")\n\t\t}\n\tcase 0:\n\tdefault:\n\t\treturn nil, config.NodeErr(node, \"invalid count of arguments\")\n\t}\n\treturn &exterrors.SMTPError{\n\t\tCode:         code,\n\t\tEnhancedCode: enchCode,\n\t\tMessage:      msg,\n\t\tReason:       \"reject directive used\",\n\t}, nil\n}\n\nfunc parseEnhancedCode(s string) (exterrors.EnhancedCode, error) {\n\tparts := strings.Split(s, \".\")\n\tif len(parts) != 3 {\n\t\treturn exterrors.EnhancedCode{}, fmt.Errorf(\"wrong amount of enhanced code parts\")\n\t}\n\n\tcode := exterrors.EnhancedCode{}\n\tfor i, part := range parts {\n\t\tnum, err := strconv.Atoi(part)\n\t\tif err != nil {\n\t\t\treturn code, err\n\t\t}\n\t\tcode[i] = num\n\t}\n\treturn code, nil\n}\n\nfunc parseChecksGroup(globals map[string]interface{}, node config.Node) ([]module.Check, error) {\n\tvar cg *CheckGroup\n\terr := modconfig.GroupFromNode(\"checks\", node.Args, node, globals, &cg)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn cg.L, nil\n}\n\nfunc parseModifiersGroup(globals map[string]interface{}, node config.Node) (modify.Group, error) {\n\t// Module object is *modify.Group, not modify.Group.\n\tvar mg *modify.Group\n\terr := modconfig.GroupFromNode(\"modifiers\", node.Args, node, globals, &mg)\n\tif err != nil {\n\t\treturn modify.Group{}, err\n\t}\n\treturn *mg, nil\n}\n\nfunc validMatchRule(rule string) bool {\n\treturn address.ValidDomain(rule) || address.Valid(rule)\n}\n"
  },
  {
    "path": "internal/msgpipeline/config_test.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage msgpipeline\n\nimport (\n\t\"reflect\"\n\t\"strings\"\n\t\"testing\"\n\n\tparser \"github.com/foxcpp/maddy/framework/cfgparser\"\n\t\"github.com/foxcpp/maddy/framework/exterrors\"\n)\n\nfunc policyError(code int) error {\n\treturn &exterrors.SMTPError{\n\t\tMessage:      \"Message rejected due to a local policy\",\n\t\tCode:         code,\n\t\tEnhancedCode: exterrors.EnhancedCode{5, 7, 0},\n\t\tReason:       \"reject directive used\",\n\t}\n}\n\nfunc TestMsgPipelineCfg(t *testing.T) {\n\tcases := []struct {\n\t\tname  string\n\t\tstr   string\n\t\tvalue msgpipelineCfg\n\t\tfail  bool\n\t}{\n\t\t{\n\t\t\tname: \"basic\",\n\t\t\tstr: `\n\t\t\t\tsource example.com {\n\t\t\t\t\tdestination example.org {\n\t\t\t\t\t\treject 410\n\t\t\t\t  \t}\n\t\t\t\t  \tdefault_destination {\n\t\t\t\t\t\treject 420\n\t\t\t\t  \t}\n\t\t\t\t}\n\t\t\t\tdefault_source {\n\t\t\t\t  \tdestination example.org {\n\t\t\t\t\t\treject 430\n\t\t\t\t  \t}\n\t\t\t\t  \tdefault_destination {\n\t\t\t\t\t\treject 440\n\t\t\t\t  \t}\n\t\t\t\t}`,\n\t\t\tvalue: msgpipelineCfg{\n\t\t\t\tperSource: map[string]sourceBlock{\n\t\t\t\t\t\"example.com\": {\n\t\t\t\t\t\tperRcpt: map[string]*rcptBlock{\n\t\t\t\t\t\t\t\"example.org\": {\n\t\t\t\t\t\t\t\trejectErr: policyError(410),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tdefaultRcpt: &rcptBlock{\n\t\t\t\t\t\t\trejectErr: policyError(420),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdefaultSource: sourceBlock{\n\t\t\t\t\tperRcpt: map[string]*rcptBlock{\n\t\t\t\t\t\t\"example.org\": {\n\t\t\t\t\t\t\trejectErr: policyError(430),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tdefaultRcpt: &rcptBlock{\n\t\t\t\t\t\trejectErr: policyError(440),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"implied default destination\",\n\t\t\tstr: `\n\t\t\t\tsource example.com {\n\t\t\t\t\treject 410\n\t\t\t\t}\n\t\t\t\tdefault_source {\n\t\t\t\t\treject 420\n\t\t\t\t}`,\n\t\t\tvalue: msgpipelineCfg{\n\t\t\t\tperSource: map[string]sourceBlock{\n\t\t\t\t\t\"example.com\": {\n\t\t\t\t\t\tperRcpt: map[string]*rcptBlock{},\n\t\t\t\t\t\tdefaultRcpt: &rcptBlock{\n\t\t\t\t\t\t\trejectErr: policyError(410),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdefaultSource: sourceBlock{\n\t\t\t\t\tperRcpt: map[string]*rcptBlock{},\n\t\t\t\t\tdefaultRcpt: &rcptBlock{\n\t\t\t\t\t\trejectErr: policyError(420),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"implied default sender\",\n\t\t\tstr: `\n\t\t\t\tdestination example.com {\n\t\t\t\t\treject 410\n\t\t\t\t}\n\t\t\t\tdefault_destination {\n\t\t\t\t\treject 420\n\t\t\t\t}`,\n\t\t\tvalue: msgpipelineCfg{\n\t\t\t\tperSource: map[string]sourceBlock{},\n\t\t\t\tdefaultSource: sourceBlock{\n\t\t\t\t\tperRcpt: map[string]*rcptBlock{\n\t\t\t\t\t\t\"example.com\": {\n\t\t\t\t\t\t\trejectErr: policyError(410),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tdefaultRcpt: &rcptBlock{\n\t\t\t\t\t\trejectErr: policyError(420),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"missing default source handler\",\n\t\t\tstr: `\n\t\t\t\tsource example.org {\n\t\t\t\t\treject 410\n\t\t\t\t}`,\n\t\t\tfail: true,\n\t\t},\n\t\t{\n\t\t\tname: \"missing default destination handler\",\n\t\t\tstr: `\n\t\t\t\tdestination example.org {\n\t\t\t\t\treject 410\n\t\t\t\t}`,\n\t\t\tfail: true,\n\t\t},\n\t\t{\n\t\t\tname: \"invalid domain\",\n\t\t\tstr: `\n\t\t\t\tdestination .. {\n\t\t\t\t\treject 410\n\t\t\t\t}\n\t\t\t\tdefault_destination {\n\t\t\t\t\treject 500\n\t\t\t\t}`,\n\t\t\tfail: true,\n\t\t},\n\t\t{\n\t\t\tname: \"invalid address\",\n\t\t\tstr: `\n\t\t\t\tdestination @example. {\n\t\t\t\t\treject 410\n\t\t\t\t}\n\t\t\t\tdefault_destination {\n\t\t\t\t\treject 500\n\t\t\t\t}`,\n\t\t\tfail: true,\n\t\t},\n\t\t{\n\t\t\tname: \"invalid address\",\n\t\t\tstr: `\n\t\t\t\tdestination @example. {\n\t\t\t\t\treject 421\n\t\t\t\t}\n\t\t\t\tdefault_destination {\n\t\t\t\t\treject 500\n\t\t\t\t}`,\n\t\t\tfail: true,\n\t\t},\n\t\t{\n\t\t\tname: \"invalid reject code\",\n\t\t\tstr: `\n\t\t\t\tdestination example.com {\n\t\t\t\t\treject 200\n\t\t\t\t}\n\t\t\t\tdefault_destination {\n\t\t\t\t\treject 500\n\t\t\t\t}`,\n\t\t\tfail: true,\n\t\t},\n\t\t{\n\t\t\tname: \"destination together with source\",\n\t\t\tstr: `\n\t\t\t\tdestination example.com {\n\t\t\t\t\treject 410\n\t\t\t\t}\n\t\t\t\tsource example.org {\n\t\t\t\t\treject 420\n\t\t\t\t}\n\t\t\t\tdefault_source {\n\t\t\t\t\treject 430\n\t\t\t\t}`,\n\t\t\tfail: true,\n\t\t},\n\t\t{\n\t\t\tname: \"empty destination rule\",\n\t\t\tstr: `\n\t\t\t\tdestination {\n\t\t\t\t\treject 410\n\t\t\t\t}\n\t\t\t\tdefault_destination {\n\t\t\t\t\treject 420\n\t\t\t\t}`,\n\t\t\tfail: true,\n\t\t},\n\t}\n\n\tfor _, case_ := range cases {\n\t\tt.Run(case_.name, func(t *testing.T) {\n\t\t\tcfg, _ := parser.Read(strings.NewReader(case_.str), \"literal\")\n\t\t\tparsed, err := parseMsgPipelineRootCfg(nil, cfg)\n\t\t\tif err != nil && !case_.fail {\n\t\t\t\tt.Fatalf(\"unexpected parse error: %v\", err)\n\t\t\t}\n\t\t\tif err == nil && case_.fail {\n\t\t\t\tt.Fatalf(\"unexpected parse success\")\n\t\t\t}\n\t\t\tif case_.fail {\n\t\t\t\tt.Log(err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(parsed, case_.value) {\n\t\t\t\tt.Errorf(\"Wrong parsed configuration\")\n\t\t\t\tt.Errorf(\"Wanted: %+v\", case_.value)\n\t\t\t\tt.Errorf(\"Got: %+v\", parsed)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestMsgPipelineCfg_SourceIn(t *testing.T) {\n\tstr := `\n\t\tsource_in dummy {\n\t\t\tdeliver_to dummy\n\t\t}\n\t\tdefault_source {\n\t\t\treject 500\n\t\t}\n\t`\n\n\tcfg, _ := parser.Read(strings.NewReader(str), \"literal\")\n\tparsed, err := parseMsgPipelineRootCfg(nil, cfg)\n\tif err != nil {\n\t\tt.Fatalf(\"unexpected parse error: %v\", err)\n\t}\n\n\tif len(parsed.sourceIn) == 0 {\n\t\tt.Fatalf(\"missing source_in dummy\")\n\t}\n}\n\nfunc TestMsgPipelineCfg_DestIn(t *testing.T) {\n\tstr := `\n\t\tdestination_in dummy {\n\t\t\tdeliver_to dummy\n\t\t}\n\t\tdefault_destination {\n\t\t\treject 500\n\t\t}\n\t`\n\n\tcfg, _ := parser.Read(strings.NewReader(str), \"literal\")\n\tparsed, err := parseMsgPipelineRootCfg(nil, cfg)\n\tif err != nil {\n\t\tt.Fatalf(\"unexpected parse error: %v\", err)\n\t}\n\n\tif len(parsed.defaultSource.rcptIn) == 0 {\n\t\tt.Fatalf(\"missing destination_in dummy\")\n\t}\n}\n\nfunc TestMsgPipelineCfg_GlobalChecks(t *testing.T) {\n\tstr := `\n\t\tcheck {\n\t\t\ttest_check\n\t\t}\n\t\tdefault_destination {\n\t\t\treject 500\n\t\t}\n\t`\n\n\tcfg, _ := parser.Read(strings.NewReader(str), \"literal\")\n\tparsed, err := parseMsgPipelineRootCfg(nil, cfg)\n\tif err != nil {\n\t\tt.Fatalf(\"unexpected parse error: %v\", err)\n\t}\n\n\tif len(parsed.globalChecks) == 0 {\n\t\tt.Fatalf(\"missing test_check in globalChecks\")\n\t}\n}\n\nfunc TestMsgPipelineCfg_GlobalChecksMultiple(t *testing.T) {\n\tstr := `\n\t\tcheck {\n\t\t\ttest_check\n\t\t}\n\t\tcheck {\n\t\t\ttest_check\n\t\t}\n\t\tdefault_destination {\n\t\t\treject 500\n\t\t}\n\t`\n\n\tcfg, _ := parser.Read(strings.NewReader(str), \"literal\")\n\tparsed, err := parseMsgPipelineRootCfg(nil, cfg)\n\tif err != nil {\n\t\tt.Fatalf(\"unexpected parse error: %v\", err)\n\t}\n\n\tif len(parsed.globalChecks) != 2 {\n\t\tt.Fatalf(\"wrong amount of test_check's in globalChecks: %d\", len(parsed.globalChecks))\n\t}\n}\n\nfunc TestMsgPipelineCfg_SourceChecks(t *testing.T) {\n\tstr := `\n\t\tsource example.org {\n\t\t\tcheck {\n\t\t\t\ttest_check\n\t\t\t}\n\n\t\t\treject 500\n\t\t}\n\t\tdefault_source {\n\t\t\treject 500\n\t\t}\n\t`\n\n\tcfg, _ := parser.Read(strings.NewReader(str), \"literal\")\n\tparsed, err := parseMsgPipelineRootCfg(nil, cfg)\n\tif err != nil {\n\t\tt.Fatalf(\"unexpected parse error: %v\", err)\n\t}\n\n\tif len(parsed.perSource[\"example.org\"].checks) == 0 {\n\t\tt.Fatalf(\"missing test_check in source checks\")\n\t}\n}\n\nfunc TestMsgPipelineCfg_SourceChecks_Multiple(t *testing.T) {\n\tstr := `\n\t\tsource example.org {\n\t\t\tcheck {\n\t\t\t\ttest_check\n\t\t\t}\n\t\t\tcheck {\n\t\t\t\ttest_check\n\t\t\t}\n\n\t\t\treject 500\n\t\t}\n\t\tdefault_source {\n\t\t\treject 500\n\t\t}\n\t`\n\n\tcfg, _ := parser.Read(strings.NewReader(str), \"literal\")\n\tparsed, err := parseMsgPipelineRootCfg(nil, cfg)\n\tif err != nil {\n\t\tt.Fatalf(\"unexpected parse error: %v\", err)\n\t}\n\n\tif len(parsed.perSource[\"example.org\"].checks) != 2 {\n\t\tt.Fatalf(\"wrong amount of test_check's in source checks: %d\", len(parsed.perSource[\"example.org\"].checks))\n\t}\n}\n\nfunc TestMsgPipelineCfg_RcptChecks(t *testing.T) {\n\tstr := `\n\t\tdestination example.org {\n\t\t\tcheck {\n\t\t\t\ttest_check\n\t\t\t}\n\n\t\t\treject 500\n\t\t}\n\t\tdefault_destination {\n\t\t\treject 500\n\t\t}\n\t`\n\n\tcfg, _ := parser.Read(strings.NewReader(str), \"literal\")\n\tparsed, err := parseMsgPipelineRootCfg(nil, cfg)\n\tif err != nil {\n\t\tt.Fatalf(\"unexpected parse error: %v\", err)\n\t}\n\n\tif len(parsed.defaultSource.perRcpt[\"example.org\"].checks) == 0 {\n\t\tt.Fatalf(\"missing test_check in rcpt checks\")\n\t}\n}\n\nfunc TestMsgPipelineCfg_RcptChecks_Multiple(t *testing.T) {\n\tstr := `\n\t\tdestination example.org {\n\t\t\tcheck {\n\t\t\t\ttest_check\n\t\t\t}\n\t\t\tcheck {\n\t\t\t\ttest_check\n\t\t\t}\n\n\t\t\treject 500\n\t\t}\n\t\tdefault_destination {\n\t\t\treject 500\n\t\t}\n\t`\n\n\tcfg, _ := parser.Read(strings.NewReader(str), \"literal\")\n\tparsed, err := parseMsgPipelineRootCfg(nil, cfg)\n\tif err != nil {\n\t\tt.Fatalf(\"unexpected parse error: %v\", err)\n\t}\n\n\tif len(parsed.defaultSource.perRcpt[\"example.org\"].checks) != 2 {\n\t\tt.Fatalf(\"wrong amount of test_check's in rcpt checks: %d\", len(parsed.defaultSource.perRcpt[\"example.org\"].checks))\n\t}\n}\n"
  },
  {
    "path": "internal/msgpipeline/dmarc_test.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage msgpipeline\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"crypto/sha1\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"net\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/emersion/go-message/textproto\"\n\t\"github.com/emersion/go-msgauth/authres\"\n\t\"github.com/emersion/go-smtp\"\n\t\"github.com/foxcpp/go-mockdns\"\n\t\"github.com/foxcpp/maddy/framework/buffer\"\n\t\"github.com/foxcpp/maddy/framework/exterrors\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/internal/testutils\"\n)\n\nfunc doTestDelivery(t *testing.T, tgt module.DeliveryTarget, from string, to []string, hdr string) (string, error) {\n\tt.Helper()\n\n\tIDRaw := sha1.Sum([]byte(t.Name()))\n\tencodedID := hex.EncodeToString(IDRaw[:])\n\n\tbody := buffer.MemoryBuffer{Slice: []byte(\"foobar\")}\n\tctx := module.MsgMetadata{\n\t\tDontTraceSender: true,\n\t\tID:              encodedID,\n\t}\n\n\thdrParsed, err := textproto.ReadHeader(bufio.NewReader(strings.NewReader(hdr)))\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tdelivery, err := tgt.StartDelivery(context.Background(), &ctx, from)\n\tif err != nil {\n\t\treturn encodedID, err\n\t}\n\tfor _, rcpt := range to {\n\t\tif err := delivery.AddRcpt(context.Background(), rcpt, smtp.RcptOptions{}); err != nil {\n\t\t\tif err := delivery.Abort(context.Background()); err != nil {\n\t\t\t\tt.Log(\"delivery.Abort:\", err)\n\t\t\t}\n\t\t\treturn encodedID, err\n\t\t}\n\t}\n\tif err := delivery.Body(context.Background(), hdrParsed, body); err != nil {\n\t\tif err := delivery.Abort(context.Background()); err != nil {\n\t\t\tt.Log(\"delivery.Abort:\", err)\n\t\t}\n\t\treturn encodedID, err\n\t}\n\tif err := delivery.Commit(context.Background()); err != nil {\n\t\treturn encodedID, err\n\t}\n\n\treturn encodedID, err\n}\n\nfunc dmarcResult(t *testing.T, hdr textproto.Header) authres.ResultValue {\n\tfield := hdr.Get(\"Authentication-Results\")\n\tif field == \"\" {\n\t\tt.Fatalf(\"No results field\")\n\t}\n\n\t_, results, err := authres.Parse(field)\n\tif err != nil {\n\t\tt.Fatalf(\"Field parse err: %v\", err)\n\t}\n\n\tfor _, res := range results {\n\t\tdmarcRes, ok := res.(*authres.DMARCResult)\n\t\tif ok {\n\t\t\treturn dmarcRes.Value\n\t\t}\n\t}\n\n\tt.Fatalf(\"No DMARC authres found\")\n\treturn \"\"\n}\n\nfunc TestDMARC(t *testing.T) {\n\ttest := func(zones map[string]mockdns.Zone, hdr string, authres []authres.Result, reject, quarantine bool, dmarcRes authres.ResultValue) {\n\t\tt.Helper()\n\n\t\ttgt := testutils.Target{}\n\t\tp := MsgPipeline{\n\t\t\tmsgpipelineCfg: msgpipelineCfg{\n\t\t\t\tglobalChecks: []module.Check{\n\t\t\t\t\t&testutils.Check{\n\t\t\t\t\t\tBodyRes: module.CheckResult{\n\t\t\t\t\t\t\tAuthResult: authres,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tperSource: map[string]sourceBlock{},\n\t\t\t\tdefaultSource: sourceBlock{\n\t\t\t\t\tperRcpt: map[string]*rcptBlock{},\n\t\t\t\t\tdefaultRcpt: &rcptBlock{\n\t\t\t\t\t\ttargets: []module.DeliveryTarget{&tgt},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdoDMARC: true,\n\t\t\t},\n\t\t\tLog:      testutils.Logger(t, \"pipeline\"),\n\t\t\tResolver: &mockdns.Resolver{Zones: zones},\n\t\t}\n\n\t\t_, err := doTestDelivery(t, &p, \"test@example.org\", []string{\"test@example.com\"}, hdr)\n\t\tif reject {\n\t\t\tif err == nil {\n\t\t\t\tt.Errorf(\"expected message to be rejected\")\n\t\t\t\treturn\n\t\t\t}\n\t\t\tt.Log(err, exterrors.Fields(err))\n\t\t\treturn\n\t\t}\n\t\tif err != nil {\n\t\t\tt.Errorf(\"unexpected error: %v %+v\", err, exterrors.Fields(err))\n\t\t\treturn\n\t\t}\n\n\t\tif len(tgt.Messages) != 1 {\n\t\t\tt.Errorf(\"got %d messages\", len(tgt.Messages))\n\t\t\treturn\n\t\t}\n\t\tmsg := tgt.Messages[0]\n\n\t\tif msg.MsgMeta.Quarantine != quarantine {\n\t\t\tt.Errorf(\"msg.MsgMeta.Quarantine (%v) != quarantine (%v)\", msg.MsgMeta.Quarantine, quarantine)\n\t\t\treturn\n\t\t}\n\n\t\tres := dmarcResult(t, msg.Header)\n\t\tif res != dmarcRes {\n\t\t\tt.Errorf(\"expected DMARC result to be '%v', got '%v'\", dmarcRes, res)\n\t\t\treturn\n\t\t}\n\t}\n\n\t// No policy => DMARC 'none'\n\ttest(map[string]mockdns.Zone{}, \"From: hello@example.org\\r\\n\\r\\n\", []authres.Result{\n\t\t&authres.DKIMResult{Value: authres.ResultPass, Domain: \"example.org\"},\n\t\t&authres.SPFResult{Value: authres.ResultNone, From: \"example.org\", Helo: \"mx.example.org\"},\n\t}, false, false, authres.ResultNone)\n\n\t// Policy present & identifiers align => DMARC 'pass'\n\ttest(map[string]mockdns.Zone{\n\t\t\"_dmarc.example.org.\": {\n\t\t\tTXT: []string{\"v=DMARC1; p=none\"},\n\t\t},\n\t}, \"From: hello@example.org\\r\\n\\r\\n\", []authres.Result{\n\t\t&authres.DKIMResult{Value: authres.ResultPass, Domain: \"example.org\"},\n\t\t&authres.SPFResult{Value: authres.ResultNone, From: \"example.org\", Helo: \"mx.example.org\"},\n\t}, false, false, authres.ResultPass)\n\n\t// Policy fetch error => DMARC 'permerror' but the message\n\t// is accepted.\n\ttest(map[string]mockdns.Zone{\n\t\t\"_dmarc.example.com.\": {\n\t\t\tErr: errors.New(\"the dns server is going insane\"),\n\t\t},\n\t}, \"From: hello@example.com\\r\\n\\r\\n\", []authres.Result{\n\t\t&authres.DKIMResult{Value: authres.ResultPass, Domain: \"example.org\"},\n\t\t&authres.SPFResult{Value: authres.ResultNone, From: \"example.org\", Helo: \"mx.example.org\"},\n\t}, false, false, authres.ResultPermError)\n\n\t// Policy fetch error => DMARC 'temperror' but the message\n\t// is rejected (\"fail closed\")\n\ttest(map[string]mockdns.Zone{\n\t\t\"_dmarc.example.com.\": {\n\t\t\tErr: &net.DNSError{\n\t\t\t\tErr:         \"the dns server is going insane, temporary\",\n\t\t\t\tIsTemporary: true,\n\t\t\t},\n\t\t},\n\t}, \"From: hello@example.com\\r\\n\\r\\n\", []authres.Result{\n\t\t&authres.DKIMResult{Value: authres.ResultPass, Domain: \"example.org\"},\n\t\t&authres.SPFResult{Value: authres.ResultNone, From: \"example.org\", Helo: \"mx.example.org\"},\n\t}, true, false, authres.ResultTempError)\n\n\t// Misaligned From vs DKIM => DMARC 'fail', policy says to reject\n\ttest(map[string]mockdns.Zone{\n\t\t\"_dmarc.example.com.\": {\n\t\t\tTXT: []string{\"v=DMARC1; p=reject\"},\n\t\t},\n\t}, \"From: hello@example.com\\r\\n\\r\\n\", []authres.Result{\n\t\t&authres.DKIMResult{Value: authres.ResultPass, Domain: \"example.org\"},\n\t\t&authres.SPFResult{Value: authres.ResultNone, From: \"example.org\", Helo: \"mx.example.org\"},\n\t}, true, false, \"\")\n\n\t// Misaligned From vs DKIM => DMARC 'fail', policy says to quarantine.\n\ttest(map[string]mockdns.Zone{\n\t\t\"_dmarc.example.com.\": {\n\t\t\tTXT: []string{\"v=DMARC1; p=quarantine\"},\n\t\t},\n\t}, \"From: hello@example.com\\r\\n\\r\\n\", []authres.Result{\n\t\t&authres.DKIMResult{Value: authres.ResultPass, Domain: \"example.org\"},\n\t\t&authres.SPFResult{Value: authres.ResultNone, From: \"example.org\", Helo: \"mx.example.org\"},\n\t}, false, true, authres.ResultFail)\n}\n"
  },
  {
    "path": "internal/msgpipeline/metrics.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage msgpipeline\n\nimport \"github.com/prometheus/client_golang/prometheus\"\n\nvar (\n\tcheckReject = prometheus.NewCounterVec(\n\t\tprometheus.CounterOpts{\n\t\t\tNamespace: \"maddy\",\n\t\t\tSubsystem: \"check\",\n\t\t\tName:      \"reject\",\n\t\t\tHelp:      \"Number of times a check returned 'reject' result (may be more than processed messages if check does so on per-recipient basis)\",\n\t\t},\n\t\t[]string{\"check\"},\n\t)\n\tcheckQuarantined = prometheus.NewCounterVec(\n\t\tprometheus.CounterOpts{\n\t\t\tNamespace: \"maddy\",\n\t\t\tSubsystem: \"check\",\n\t\t\tName:      \"quarantined\",\n\t\t\tHelp:      \"Number of times a check returned 'quarantine' result (may be more than processed messages if check does so on per-recipient basis)\",\n\t\t},\n\t\t[]string{\"check\"},\n\t)\n)\n\nfunc init() {\n\tprometheus.MustRegister(checkReject)\n\tprometheus.MustRegister(checkQuarantined)\n}\n"
  },
  {
    "path": "internal/msgpipeline/modifier_test.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage msgpipeline\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/internal/modify\"\n\t\"github.com/foxcpp/maddy/internal/testutils\"\n)\n\nfunc TestMsgPipeline_SenderModifier(t *testing.T) {\n\ttarget := testutils.Target{}\n\tmodifier := testutils.Modifier{\n\t\tInstName: \"test_modifier\",\n\t\tMailFrom: map[string]string{\n\t\t\t\"sender@example.com\": \"sender2@example.com\",\n\t\t},\n\t}\n\td := MsgPipeline{\n\t\tmsgpipelineCfg: msgpipelineCfg{\n\t\t\tglobalModifiers: modify.Group{\n\t\t\t\tModifiers: []module.Modifier{modifier},\n\t\t\t},\n\t\t\tperSource: map[string]sourceBlock{},\n\t\t\tdefaultSource: sourceBlock{\n\t\t\t\tperRcpt: map[string]*rcptBlock{},\n\t\t\t\tdefaultRcpt: &rcptBlock{\n\t\t\t\t\ttargets: []module.DeliveryTarget{&target},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tLog: testutils.Logger(t, \"msgpipeline\"),\n\t}\n\n\ttestutils.DoTestDelivery(t, &d, \"sender@example.com\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"})\n\n\tif len(target.Messages) != 1 {\n\t\tt.Fatalf(\"wrong amount of messages received, want %d, got %d\", 1, len(target.Messages))\n\t}\n\n\ttestutils.CheckTestMessage(t, &target, 0, \"sender2@example.com\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"})\n\n\tif modifier.UnclosedStates != 0 {\n\t\tt.Fatalf(\"modifier state objects leak or double-closed, counter: %d\", modifier.UnclosedStates)\n\t}\n}\n\nfunc TestMsgPipeline_SenderModifier_Multiple(t *testing.T) {\n\ttarget := testutils.Target{}\n\tmod1, mod2 := testutils.Modifier{\n\t\tInstName: \"first_modifier\",\n\t\tMailFrom: map[string]string{\n\t\t\t\"sender@example.com\": \"sender2@example.com\",\n\t\t},\n\t}, testutils.Modifier{\n\t\tInstName: \"second_modifier\",\n\t\tMailFrom: map[string]string{\n\t\t\t\"sender2@example.com\": \"sender3@example.com\",\n\t\t},\n\t}\n\td := MsgPipeline{\n\t\tmsgpipelineCfg: msgpipelineCfg{\n\t\t\tglobalModifiers: modify.Group{\n\t\t\t\tModifiers: []module.Modifier{mod1, mod2},\n\t\t\t},\n\t\t\tperSource: map[string]sourceBlock{},\n\t\t\tdefaultSource: sourceBlock{\n\t\t\t\tperRcpt: map[string]*rcptBlock{},\n\t\t\t\tdefaultRcpt: &rcptBlock{\n\t\t\t\t\ttargets: []module.DeliveryTarget{&target},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tLog: testutils.Logger(t, \"msgpipeline\"),\n\t}\n\n\ttestutils.DoTestDelivery(t, &d, \"sender@example.com\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"})\n\n\tif len(target.Messages) != 1 {\n\t\tt.Fatalf(\"wrong amount of messages received, want %d, got %d\", 1, len(target.Messages))\n\t}\n\n\ttestutils.CheckTestMessage(t, &target, 0, \"sender3@example.com\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"})\n\n\tif mod1.UnclosedStates != 0 || mod2.UnclosedStates != 0 {\n\t\tt.Fatalf(\"modifier state objects leak or double-closed, counter: %d, %d\", mod1.UnclosedStates, mod2.UnclosedStates)\n\t}\n}\n\nfunc TestMsgPipeline_SenderModifier_PreDispatch(t *testing.T) {\n\ttarget := testutils.Target{InstName: \"target\"}\n\tmod := testutils.Modifier{\n\t\tInstName: \"test_modifier\",\n\t\tMailFrom: map[string]string{\n\t\t\t\"sender@example.com\": \"sender@example.org\",\n\t\t},\n\t}\n\td := MsgPipeline{\n\t\tmsgpipelineCfg: msgpipelineCfg{\n\t\t\tglobalModifiers: modify.Group{\n\t\t\t\tModifiers: []module.Modifier{mod},\n\t\t\t},\n\t\t\tperSource: map[string]sourceBlock{\n\t\t\t\t\"example.org\": {\n\t\t\t\t\tperRcpt: map[string]*rcptBlock{},\n\t\t\t\t\tdefaultRcpt: &rcptBlock{\n\t\t\t\t\t\ttargets: []module.DeliveryTarget{&target},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tdefaultSource: sourceBlock{rejectErr: errors.New(\"default src block used\")},\n\t\t},\n\t\tLog: testutils.Logger(t, \"msgpipeline\"),\n\t}\n\n\ttestutils.DoTestDelivery(t, &d, \"sender@example.com\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"})\n\n\tif len(target.Messages) != 1 {\n\t\tt.Fatalf(\"wrong amount of messages received for target, want %d, got %d\", 1, len(target.Messages))\n\t}\n\ttestutils.CheckTestMessage(t, &target, 0, \"sender@example.org\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"})\n\n\tif mod.UnclosedStates != 0 {\n\t\tt.Fatalf(\"modifier state objects leak or double-closed, counter: %d\", mod.UnclosedStates)\n\t}\n}\n\nfunc TestMsgPipeline_SenderModifier_PostDispatch(t *testing.T) {\n\ttarget := testutils.Target{InstName: \"target\"}\n\tmod := testutils.Modifier{\n\t\tInstName: \"test_modifier\",\n\t\tMailFrom: map[string]string{\n\t\t\t\"sender@example.org\": \"sender@example.com\",\n\t\t},\n\t}\n\td := MsgPipeline{\n\t\tmsgpipelineCfg: msgpipelineCfg{\n\t\t\tperSource: map[string]sourceBlock{\n\t\t\t\t\"example.org\": {\n\t\t\t\t\tmodifiers: modify.Group{\n\t\t\t\t\t\tModifiers: []module.Modifier{mod},\n\t\t\t\t\t},\n\t\t\t\t\tperRcpt: map[string]*rcptBlock{},\n\t\t\t\t\tdefaultRcpt: &rcptBlock{\n\t\t\t\t\t\ttargets: []module.DeliveryTarget{&target},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tdefaultSource: sourceBlock{rejectErr: errors.New(\"default src block used\")},\n\t\t},\n\t\tLog: testutils.Logger(t, \"msgpipeline\"),\n\t}\n\n\ttestutils.DoTestDelivery(t, &d, \"sender@example.org\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"})\n\n\tif len(target.Messages) != 1 {\n\t\tt.Fatalf(\"wrong amount of messages received for target, want %d, got %d\", 1, len(target.Messages))\n\t}\n\ttestutils.CheckTestMessage(t, &target, 0, \"sender@example.com\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"})\n\n\tif mod.UnclosedStates != 0 {\n\t\tt.Fatalf(\"modifier state objects leak or double-closed, counter: %d\", mod.UnclosedStates)\n\t}\n}\n\nfunc TestMsgPipeline_SenderModifier_PerRcpt(t *testing.T) {\n\t// Modifier below will be no-op due to implementation limitations.\n\n\tcomTarget, orgTarget := testutils.Target{InstName: \"com_target\"}, testutils.Target{InstName: \"org_target\"}\n\tmod := testutils.Modifier{\n\t\tInstName: \"test_modifier\",\n\t\tMailFrom: map[string]string{\n\t\t\t\"sender@example.com\": \"sender2@example.com\",\n\t\t},\n\t}\n\td := MsgPipeline{\n\t\tmsgpipelineCfg: msgpipelineCfg{\n\t\t\tperSource: map[string]sourceBlock{},\n\t\t\tdefaultSource: sourceBlock{\n\t\t\t\tperRcpt: map[string]*rcptBlock{\n\t\t\t\t\t\"example.com\": {\n\t\t\t\t\t\tmodifiers: modify.Group{\n\t\t\t\t\t\t\tModifiers: []module.Modifier{mod},\n\t\t\t\t\t\t},\n\t\t\t\t\t\ttargets: []module.DeliveryTarget{&comTarget},\n\t\t\t\t\t},\n\t\t\t\t\t\"example.org\": {\n\t\t\t\t\t\tmodifiers: modify.Group{},\n\t\t\t\t\t\ttargets:   []module.DeliveryTarget{&orgTarget},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tLog: testutils.Logger(t, \"msgpipeline\"),\n\t}\n\n\ttestutils.DoTestDelivery(t, &d, \"sender@example.com\", []string{\"rcpt@example.com\", \"rcpt@example.org\"})\n\n\tif len(comTarget.Messages) != 1 {\n\t\tt.Fatalf(\"wrong amount of messages received for comTarget, want %d, got %d\", 1, len(comTarget.Messages))\n\t}\n\ttestutils.CheckTestMessage(t, &comTarget, 0, \"sender@example.com\", []string{\"rcpt@example.com\"})\n\n\tif len(orgTarget.Messages) != 1 {\n\t\tt.Fatalf(\"wrong amount of messages received for orgTarget, want %d, got %d\", 1, len(orgTarget.Messages))\n\t}\n\ttestutils.CheckTestMessage(t, &orgTarget, 0, \"sender@example.com\", []string{\"rcpt@example.org\"})\n\n\tif mod.UnclosedStates != 0 {\n\t\tt.Fatalf(\"modifier state objects leak or double-closed, counter: %d\", mod.UnclosedStates)\n\t}\n}\n\nfunc TestMsgPipeline_RcptModifier(t *testing.T) {\n\ttarget := testutils.Target{}\n\tmod := testutils.Modifier{\n\t\tInstName: \"test_modifier\",\n\t\tRcptTo: map[string][]string{\n\t\t\t\"rcpt1@example.com\": []string{\"rcpt1-alias@example.com\"},\n\t\t\t\"rcpt2@example.com\": []string{\"rcpt2-alias@example.com\"},\n\t\t},\n\t}\n\td := MsgPipeline{\n\t\tmsgpipelineCfg: msgpipelineCfg{\n\t\t\tglobalModifiers: modify.Group{\n\t\t\t\tModifiers: []module.Modifier{mod},\n\t\t\t},\n\t\t\tperSource: map[string]sourceBlock{},\n\t\t\tdefaultSource: sourceBlock{\n\t\t\t\tperRcpt: map[string]*rcptBlock{},\n\t\t\t\tdefaultRcpt: &rcptBlock{\n\t\t\t\t\ttargets: []module.DeliveryTarget{&target},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tLog: testutils.Logger(t, \"msgpipeline\"),\n\t}\n\n\ttestutils.DoTestDelivery(t, &d, \"sender@example.com\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"})\n\n\tif len(target.Messages) != 1 {\n\t\tt.Fatalf(\"wrong amount of messages received, want %d, got %d\", 1, len(target.Messages))\n\t}\n\n\ttestutils.CheckTestMessage(t, &target, 0, \"sender@example.com\", []string{\"rcpt1-alias@example.com\", \"rcpt2-alias@example.com\"})\n\n\tif mod.UnclosedStates != 0 {\n\t\tt.Fatalf(\"modifier state objects leak or double-closed, counter: %d\", mod.UnclosedStates)\n\t}\n}\n\nfunc TestMsgPipeline_RcptModifier_OriginalRcpt(t *testing.T) {\n\ttarget := testutils.Target{}\n\tmod := testutils.Modifier{\n\t\tInstName: \"test_modifier\",\n\t\tRcptTo: map[string][]string{\n\t\t\t\"rcpt1@example.com\": []string{\"rcpt1-alias@example.com\"},\n\t\t\t\"rcpt2@example.com\": []string{\"rcpt2-alias@example.com\"},\n\t\t},\n\t}\n\td := MsgPipeline{\n\t\tmsgpipelineCfg: msgpipelineCfg{\n\t\t\tglobalModifiers: modify.Group{\n\t\t\t\tModifiers: []module.Modifier{mod},\n\t\t\t},\n\t\t\tperSource: map[string]sourceBlock{},\n\t\t\tdefaultSource: sourceBlock{\n\t\t\t\tperRcpt: map[string]*rcptBlock{},\n\t\t\t\tdefaultRcpt: &rcptBlock{\n\t\t\t\t\ttargets: []module.DeliveryTarget{&target},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tLog: testutils.Logger(t, \"msgpipeline\"),\n\t}\n\n\ttestutils.DoTestDelivery(t, &d, \"sender@example.com\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"})\n\n\tif len(target.Messages) != 1 {\n\t\tt.Fatalf(\"wrong amount of messages received, want %d, got %d\", 1, len(target.Messages))\n\t}\n\n\ttestutils.CheckTestMessage(t, &target, 0, \"sender@example.com\", []string{\"rcpt1-alias@example.com\", \"rcpt2-alias@example.com\"})\n\toriginal1 := target.Messages[0].MsgMeta.OriginalRcpts[\"rcpt1-alias@example.com\"]\n\tif original1 != \"rcpt1@example.com\" {\n\t\tt.Errorf(\"wrong OriginalRcpts value for first rcpt, want %s, got %s\", \"rcpt1@example.com\", original1)\n\t}\n\toriginal2 := target.Messages[0].MsgMeta.OriginalRcpts[\"rcpt2-alias@example.com\"]\n\tif original2 != \"rcpt2@example.com\" {\n\t\tt.Errorf(\"wrong OriginalRcpts value for first rcpt, want %s, got %s\", \"rcpt2@example.com\", original2)\n\t}\n\n\tif mod.UnclosedStates != 0 {\n\t\tt.Fatalf(\"modifier state objects leak or double-closed, counter: %d\", mod.UnclosedStates)\n\t}\n}\n\nfunc TestMsgPipeline_RcptModifier_OriginalRcpt_Multiple(t *testing.T) {\n\ttarget := testutils.Target{}\n\tmod1, mod2 := testutils.Modifier{\n\t\tInstName: \"first_modifier\",\n\t\tRcptTo: map[string][]string{\n\t\t\t\"rcpt1@example.com\": []string{\"rcpt1-alias@example.com\"},\n\t\t\t\"rcpt2@example.com\": []string{\"rcpt2-alias@example.com\"},\n\t\t},\n\t}, testutils.Modifier{\n\t\tInstName: \"second_modifier\",\n\t\tRcptTo: map[string][]string{\n\t\t\t\"rcpt1-alias@example.com\": []string{\"rcpt1-alias2@example.com\"},\n\t\t\t\"rcpt2@example.com\":       []string{\"wtf@example.com\"},\n\t\t},\n\t}\n\td := MsgPipeline{\n\t\tmsgpipelineCfg: msgpipelineCfg{\n\t\t\tglobalModifiers: modify.Group{\n\t\t\t\tModifiers: []module.Modifier{mod1},\n\t\t\t},\n\t\t\tperSource: map[string]sourceBlock{},\n\t\t\tdefaultSource: sourceBlock{\n\t\t\t\tmodifiers: modify.Group{\n\t\t\t\t\tModifiers: []module.Modifier{mod2},\n\t\t\t\t},\n\t\t\t\tperRcpt: map[string]*rcptBlock{},\n\t\t\t\tdefaultRcpt: &rcptBlock{\n\t\t\t\t\ttargets: []module.DeliveryTarget{&target},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tLog: testutils.Logger(t, \"msgpipeline\"),\n\t}\n\n\ttestutils.DoTestDelivery(t, &d, \"sender@example.com\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"})\n\n\tif len(target.Messages) != 1 {\n\t\tt.Fatalf(\"wrong amount of messages received, want %d, got %d\", 1, len(target.Messages))\n\t}\n\n\ttestutils.CheckTestMessage(t, &target, 0, \"sender@example.com\", []string{\"rcpt1-alias2@example.com\", \"rcpt2-alias@example.com\"})\n\toriginal1 := target.Messages[0].MsgMeta.OriginalRcpts[\"rcpt1-alias2@example.com\"]\n\tif original1 != \"rcpt1@example.com\" {\n\t\tt.Errorf(\"wrong OriginalRcpts value for first rcpt, want %s, got %s\", \"rcpt1@example.com\", original1)\n\t}\n\toriginal2 := target.Messages[0].MsgMeta.OriginalRcpts[\"rcpt2-alias@example.com\"]\n\tif original2 != \"rcpt2@example.com\" {\n\t\tt.Errorf(\"wrong OriginalRcpts value for first rcpt, want %s, got %s\", \"rcpt2@example.com\", original2)\n\t}\n\n\tif mod1.UnclosedStates != 0 || mod2.UnclosedStates != 0 {\n\t\tt.Fatalf(\"modifier state objects leak or double-closed, counter: %d, %d\", mod1.UnclosedStates, mod2.UnclosedStates)\n\t}\n}\n\nfunc TestMsgPipeline_RcptModifier_Multiple(t *testing.T) {\n\ttarget := testutils.Target{}\n\tmod1, mod2 := testutils.Modifier{\n\t\tInstName: \"first_modifier\",\n\t\tRcptTo: map[string][]string{\n\t\t\t\"rcpt1@example.com\": []string{\"rcpt1-alias@example.com\"},\n\t\t\t\"rcpt2@example.com\": []string{\"rcpt2-alias@example.com\"},\n\t\t},\n\t}, testutils.Modifier{\n\t\tInstName: \"second_modifier\",\n\t\tRcptTo: map[string][]string{\n\t\t\t\"rcpt1-alias@example.com\": []string{\"rcpt1-alias2@example.com\"},\n\t\t\t\"rcpt2@example.com\":       []string{\"wtf@example.com\"},\n\t\t},\n\t}\n\td := MsgPipeline{\n\t\tmsgpipelineCfg: msgpipelineCfg{\n\t\t\tglobalModifiers: modify.Group{\n\t\t\t\tModifiers: []module.Modifier{mod1, mod2},\n\t\t\t},\n\t\t\tperSource: map[string]sourceBlock{},\n\t\t\tdefaultSource: sourceBlock{\n\t\t\t\tperRcpt: map[string]*rcptBlock{},\n\t\t\t\tdefaultRcpt: &rcptBlock{\n\t\t\t\t\ttargets: []module.DeliveryTarget{&target},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tLog: testutils.Logger(t, \"msgpipeline\"),\n\t}\n\n\ttestutils.DoTestDelivery(t, &d, \"sender@example.com\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"})\n\n\tif len(target.Messages) != 1 {\n\t\tt.Fatalf(\"wrong amount of messages received, want %d, got %d\", 1, len(target.Messages))\n\t}\n\n\ttestutils.CheckTestMessage(t, &target, 0, \"sender@example.com\", []string{\"rcpt1-alias2@example.com\", \"rcpt2-alias@example.com\"})\n\n\tif mod1.UnclosedStates != 0 || mod2.UnclosedStates != 0 {\n\t\tt.Fatalf(\"modifier state objects leak or double-closed, counter: %d, %d\", mod1.UnclosedStates, mod2.UnclosedStates)\n\t}\n}\n\nfunc TestMsgPipeline_RcptModifier_PreDispatch(t *testing.T) {\n\ttarget := testutils.Target{}\n\tmod1, mod2 := testutils.Modifier{\n\t\tInstName: \"first_modifier\",\n\t\tRcptTo: map[string][]string{\n\t\t\t\"rcpt1@example.com\": []string{\"rcpt1-alias@example.com\"},\n\t\t\t\"rcpt2@example.com\": []string{\"rcpt2-alias@example.com\"},\n\t\t},\n\t}, testutils.Modifier{\n\t\tInstName: \"second_modifier\",\n\t\tRcptTo: map[string][]string{\n\t\t\t\"rcpt1-alias@example.com\": []string{\"rcpt1-alias2@example.com\"},\n\t\t\t\"rcpt2@example.com\":       []string{\"wtf@example.com\"},\n\t\t},\n\t}\n\td := MsgPipeline{\n\t\tmsgpipelineCfg: msgpipelineCfg{\n\t\t\tglobalModifiers: modify.Group{\n\t\t\t\tModifiers: []module.Modifier{mod1},\n\t\t\t},\n\t\t\tperSource: map[string]sourceBlock{},\n\t\t\tdefaultSource: sourceBlock{\n\t\t\t\tmodifiers: modify.Group{Modifiers: []module.Modifier{mod2}},\n\t\t\t\tperRcpt: map[string]*rcptBlock{\n\t\t\t\t\t\"rcpt2-alias@example.com\": {\n\t\t\t\t\t\ttargets: []module.DeliveryTarget{&target},\n\t\t\t\t\t},\n\t\t\t\t\t\"rcpt1-alias2@example.com\": {\n\t\t\t\t\t\ttargets: []module.DeliveryTarget{&target},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdefaultRcpt: &rcptBlock{\n\t\t\t\t\trejectErr: errors.New(\"default rcpt is used\"),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tLog: testutils.Logger(t, \"msgpipeline\"),\n\t}\n\n\ttestutils.DoTestDelivery(t, &d, \"sender@example.com\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"})\n\n\tif len(target.Messages) != 1 {\n\t\tt.Fatalf(\"wrong amount of messages received, want %d, got %d\", 1, len(target.Messages))\n\t}\n\n\ttestutils.CheckTestMessage(t, &target, 0, \"sender@example.com\", []string{\"rcpt1-alias2@example.com\", \"rcpt2-alias@example.com\"})\n\n\tif mod1.UnclosedStates != 0 || mod2.UnclosedStates != 0 {\n\t\tt.Fatalf(\"modifier state objects leak or double-closed, counter: %d, %d\", mod1.UnclosedStates, mod2.UnclosedStates)\n\t}\n}\n\nfunc TestMsgPipeline_RcptModifier_PostDispatch(t *testing.T) {\n\ttarget := testutils.Target{}\n\tmod := testutils.Modifier{\n\t\tInstName: \"test_modifier\",\n\t\tRcptTo: map[string][]string{\n\t\t\t\"rcpt1@example.com\": []string{\"rcpt1@example.org\"},\n\t\t\t\"rcpt2@example.com\": []string{\"rcpt2@example.org\"},\n\t\t},\n\t}\n\td := MsgPipeline{\n\t\tmsgpipelineCfg: msgpipelineCfg{\n\t\t\tperSource: map[string]sourceBlock{},\n\t\t\tdefaultSource: sourceBlock{\n\t\t\t\tperRcpt: map[string]*rcptBlock{\n\t\t\t\t\t\"example.com\": {\n\t\t\t\t\t\tmodifiers: modify.Group{\n\t\t\t\t\t\t\tModifiers: []module.Modifier{mod},\n\t\t\t\t\t\t},\n\t\t\t\t\t\ttargets: []module.DeliveryTarget{&target},\n\t\t\t\t\t},\n\t\t\t\t\t\"example.org\": {\n\t\t\t\t\t\trejectErr: errors.New(\"wrong rcpt block is used\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdefaultRcpt: &rcptBlock{\n\t\t\t\t\trejectErr: errors.New(\"default rcpt is used\"),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tLog: testutils.Logger(t, \"msgpipeline\"),\n\t}\n\n\ttestutils.DoTestDelivery(t, &d, \"sender@example.com\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"})\n\n\tif len(target.Messages) != 1 {\n\t\tt.Fatalf(\"wrong amount of messages received, want %d, got %d\", 1, len(target.Messages))\n\t}\n\n\ttestutils.CheckTestMessage(t, &target, 0, \"sender@example.com\", []string{\"rcpt1@example.org\", \"rcpt2@example.org\"})\n\n\tif mod.UnclosedStates != 0 {\n\t\tt.Fatalf(\"modifier state objects leak or double-closed, counter: %d\", mod.UnclosedStates)\n\t}\n}\n\nfunc TestMsgPipeline_GlobalModifier_Errors(t *testing.T) {\n\ttarget := testutils.Target{}\n\tmod := testutils.Modifier{\n\t\tInstName:    \"test_modifier\",\n\t\tInitErr:     errors.New(\"1\"),\n\t\tMailFromErr: errors.New(\"2\"),\n\t\tRcptToErr:   errors.New(\"3\"),\n\t\tBodyErr:     errors.New(\"4\"),\n\t}\n\td := MsgPipeline{\n\t\tmsgpipelineCfg: msgpipelineCfg{\n\t\t\tglobalModifiers: modify.Group{Modifiers: []module.Modifier{&mod}},\n\t\t\tperSource:       map[string]sourceBlock{},\n\t\t\tdefaultSource: sourceBlock{\n\t\t\t\tdefaultRcpt: &rcptBlock{\n\t\t\t\t\ttargets: []module.DeliveryTarget{&target},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tLog: testutils.Logger(t, \"msgpipeline\"),\n\t}\n\n\tt.Run(\"init err\", func(t *testing.T) {\n\t\t_, err := testutils.DoTestDeliveryErr(t, &d, \"sender@example.com\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"})\n\t\tif err == nil {\n\t\t\tt.Fatal(\"expected error\")\n\t\t}\n\t})\n\n\tmod.InitErr = nil\n\n\tt.Run(\"mail from err\", func(t *testing.T) {\n\t\t_, err := testutils.DoTestDeliveryErr(t, &d, \"sender@example.com\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"})\n\t\tif err == nil {\n\t\t\tt.Fatal(\"expected error\")\n\t\t}\n\t})\n\n\tmod.MailFromErr = nil\n\n\tt.Run(\"rcpt to err\", func(t *testing.T) {\n\t\t_, err := testutils.DoTestDeliveryErr(t, &d, \"sender@example.com\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"})\n\t\tif err == nil {\n\t\t\tt.Fatal(\"expected error\")\n\t\t}\n\t})\n\n\tmod.RcptToErr = nil\n\n\tt.Run(\"body err\", func(t *testing.T) {\n\t\t_, err := testutils.DoTestDeliveryErr(t, &d, \"sender@example.com\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"})\n\t\tif err == nil {\n\t\t\tt.Fatal(\"expected error\")\n\t\t}\n\t})\n\n\tmod.BodyErr = nil\n\n\tt.Run(\"no err\", func(t *testing.T) {\n\t\ttestutils.DoTestDelivery(t, &d, \"sender@example.com\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"})\n\t})\n\n\tif mod.UnclosedStates != 0 {\n\t\tt.Fatalf(\"modifier state objects leak or double-closed, counter: %d\", mod.UnclosedStates)\n\t}\n}\n\nfunc TestMsgPipeline_SourceModifier_Errors(t *testing.T) {\n\ttarget := testutils.Target{}\n\tmod := testutils.Modifier{\n\t\tInstName:    \"test_modifier\",\n\t\tInitErr:     errors.New(\"1\"),\n\t\tMailFromErr: errors.New(\"2\"),\n\t\tRcptToErr:   errors.New(\"3\"),\n\t\tBodyErr:     errors.New(\"4\"),\n\t}\n\t// Added to make sure it is freed properly too.\n\tglobalMod := testutils.Modifier{}\n\td := MsgPipeline{\n\t\tmsgpipelineCfg: msgpipelineCfg{\n\t\t\tperSource:       map[string]sourceBlock{},\n\t\t\tglobalModifiers: modify.Group{Modifiers: []module.Modifier{&globalMod}},\n\t\t\tdefaultSource: sourceBlock{\n\t\t\t\tmodifiers: modify.Group{Modifiers: []module.Modifier{&mod}},\n\t\t\t\tdefaultRcpt: &rcptBlock{\n\t\t\t\t\ttargets: []module.DeliveryTarget{&target},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tLog: testutils.Logger(t, \"msgpipeline\"),\n\t}\n\n\tt.Run(\"init err\", func(t *testing.T) {\n\t\t_, err := testutils.DoTestDeliveryErr(t, &d, \"sender@example.com\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"})\n\t\tif err == nil {\n\t\t\tt.Fatal(\"expected error\")\n\t\t}\n\t})\n\n\tmod.InitErr = nil\n\n\tt.Run(\"mail from err\", func(t *testing.T) {\n\t\t_, err := testutils.DoTestDeliveryErr(t, &d, \"sender@example.com\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"})\n\t\tif err == nil {\n\t\t\tt.Fatal(\"expected error\")\n\t\t}\n\t})\n\n\tmod.MailFromErr = nil\n\n\tt.Run(\"rcpt to err\", func(t *testing.T) {\n\t\t_, err := testutils.DoTestDeliveryErr(t, &d, \"sender@example.com\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"})\n\t\tif err == nil {\n\t\t\tt.Fatal(\"expected error\")\n\t\t}\n\t})\n\n\tmod.RcptToErr = nil\n\n\tt.Run(\"body err\", func(t *testing.T) {\n\t\t_, err := testutils.DoTestDeliveryErr(t, &d, \"sender@example.com\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"})\n\t\tif err == nil {\n\t\t\tt.Fatal(\"expected error\")\n\t\t}\n\t})\n\n\tmod.BodyErr = nil\n\n\tt.Run(\"no err\", func(t *testing.T) {\n\t\ttestutils.DoTestDelivery(t, &d, \"sender@example.com\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"})\n\t})\n\n\tif mod.UnclosedStates != 0 || globalMod.UnclosedStates != 0 {\n\t\tt.Fatalf(\"modifier state objects leak or double-closed, counters: %d, %d\",\n\t\t\tmod.UnclosedStates, globalMod.UnclosedStates)\n\t}\n}\n\nfunc TestMsgPipeline_RcptModifier_Errors(t *testing.T) {\n\ttarget := testutils.Target{}\n\tmod := testutils.Modifier{\n\t\tInstName:  \"test_modifier\",\n\t\tInitErr:   errors.New(\"1\"),\n\t\tRcptToErr: errors.New(\"3\"),\n\t}\n\t// Added to make sure it is freed properly too.\n\tglobalMod := testutils.Modifier{}\n\tsourceMod := testutils.Modifier{}\n\n\td := MsgPipeline{\n\t\tmsgpipelineCfg: msgpipelineCfg{\n\t\t\tperSource:       map[string]sourceBlock{},\n\t\t\tglobalModifiers: modify.Group{Modifiers: []module.Modifier{&globalMod}},\n\t\t\tdefaultSource: sourceBlock{\n\t\t\t\tmodifiers: modify.Group{Modifiers: []module.Modifier{&sourceMod}},\n\t\t\t\tdefaultRcpt: &rcptBlock{\n\t\t\t\t\tmodifiers: modify.Group{Modifiers: []module.Modifier{&mod}},\n\t\t\t\t\ttargets:   []module.DeliveryTarget{&target},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tLog: testutils.Logger(t, \"msgpipeline\"),\n\t}\n\n\tt.Run(\"init err\", func(t *testing.T) {\n\t\t_, err := testutils.DoTestDeliveryErr(t, &d, \"sender@example.com\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"})\n\t\tif err == nil {\n\t\t\tt.Fatal(\"expected error\")\n\t\t}\n\t})\n\n\tmod.InitErr = nil\n\n\t// MailFromErr test is inapplicable since RewriteSender is not called for per-rcpt\n\t// modifiers.\n\n\tt.Run(\"rcpt to err\", func(t *testing.T) {\n\t\t_, err := testutils.DoTestDeliveryErr(t, &d, \"sender@example.com\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"})\n\t\tif err == nil {\n\t\t\tt.Fatal(\"expected error\")\n\t\t}\n\t})\n\n\tmod.RcptToErr = nil\n\n\t// BodyErr test is inapplicable since RewriteBody is not called for per-rcpt\n\t// modifiers.\n\n\tt.Run(\"no err\", func(t *testing.T) {\n\t\ttestutils.DoTestDelivery(t, &d, \"sender@example.com\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"})\n\t})\n\n\tif mod.UnclosedStates != 0 || globalMod.UnclosedStates != 0 || sourceMod.UnclosedStates != 0 {\n\t\tt.Fatalf(\"modifier state objects leak or double-closed, counters: %d, %d, %d\",\n\t\t\tmod.UnclosedStates, globalMod.UnclosedStates, sourceMod.UnclosedStates)\n\t}\n}\n"
  },
  {
    "path": "internal/msgpipeline/module.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage msgpipeline\n\nimport (\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/log\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n)\n\ntype Module struct {\n\tinstName string\n\tlog      *log.Logger\n\t*MsgPipeline\n}\n\nfunc NewModule(c *container.C, modName, instName string) (module.Module, error) {\n\treturn &Module{\n\t\tlog:      c.DefaultLogger.Sublogger(modName),\n\t\tinstName: instName,\n\t}, nil\n}\n\nfunc (m *Module) Configure(inlineArgs []string, cfg *config.Map) error {\n\tvar hostname string\n\tcfg.String(\"hostname\", true, true, \"\", &hostname)\n\tcfg.Bool(\"debug\", true, false, &m.log.Debug)\n\tcfg.AllowUnknown()\n\tother, err := cfg.Process()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tp, err := New(cfg.Globals, other)\n\tif err != nil {\n\t\treturn err\n\t}\n\tm.MsgPipeline = p\n\tm.Log = m.log\n\n\treturn nil\n}\n\nfunc (m *Module) Name() string {\n\treturn \"msgpipeline\"\n}\n\nfunc (m *Module) InstanceName() string {\n\treturn m.instName\n}\n\nfunc init() {\n\tmodules.Register(\"msgpipeline\", NewModule)\n}\n"
  },
  {
    "path": "internal/msgpipeline/msgpipeline.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage msgpipeline\n\nimport (\n\t\"context\"\n\n\t\"github.com/emersion/go-message/textproto\"\n\t\"github.com/emersion/go-smtp\"\n\t\"github.com/foxcpp/maddy/framework/address\"\n\t\"github.com/foxcpp/maddy/framework/buffer\"\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/dns\"\n\t\"github.com/foxcpp/maddy/framework/exterrors\"\n\t\"github.com/foxcpp/maddy/framework/log\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/internal/modify\"\n\t\"github.com/foxcpp/maddy/internal/target\"\n\t\"golang.org/x/sync/errgroup\"\n)\n\n// MsgPipeline is a object that is responsible for selecting delivery targets\n// for the message and running necessary checks and modifiers.\n//\n// It implements module.DeliveryTarget.\n//\n// It is not a \"module object\" and is intended to be used as part of message\n// source (Submission, SMTP, JMAP modules) implementation.\ntype MsgPipeline struct {\n\tmsgpipelineCfg\n\tHostname string\n\tResolver dns.Resolver\n\n\t// Used to indicate the pipeline is handling messages received from the\n\t// external source and not from any other module. That is, this MsgPipeline\n\t// is an instance embedded in endpoint/smtp implementation, for example.\n\t//\n\t// This is a hack since only MsgPipeline can execute some operations at the\n\t// right time but it is not a good idea to execute them multiple multiple\n\t// times for a single message that might be actually handled my multiple\n\t// pipelines via 'msgpipeline' module or 'reroute' directive.\n\t//\n\t// At the moment, the only such operation is the addition of the Received\n\t// header field. See where it happens for explanation on why it is done\n\t// exactly in this place.\n\tFirstPipeline bool\n\n\tLog *log.Logger\n}\n\ntype rcptIn struct {\n\tt     module.Table\n\tblock *rcptBlock\n}\n\ntype sourceBlock struct {\n\tchecks      []module.Check\n\tmodifiers   modify.Group\n\trejectErr   error\n\trcptIn      []rcptIn\n\tperRcpt     map[string]*rcptBlock\n\tdefaultRcpt *rcptBlock\n}\n\ntype rcptBlock struct {\n\tchecks    []module.Check\n\tmodifiers modify.Group\n\trejectErr error\n\ttargets   []module.DeliveryTarget\n}\n\nfunc New(globals map[string]interface{}, cfg []config.Node) (*MsgPipeline, error) {\n\tparsedCfg, err := parseMsgPipelineRootCfg(globals, cfg)\n\treturn &MsgPipeline{\n\t\tmsgpipelineCfg: parsedCfg,\n\t\tResolver:       dns.DefaultResolver(),\n\t}, err\n}\n\nfunc (d *MsgPipeline) RunEarlyChecks(ctx context.Context, state *module.ConnState) error {\n\teg, checkCtx := errgroup.WithContext(ctx)\n\n\t// TODO: See if there is some point in parallelization of this\n\t// function.\n\tfor _, check := range d.globalChecks {\n\t\tearlyCheck, ok := check.(module.EarlyCheck)\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\n\t\teg.Go(func() error {\n\t\t\treturn earlyCheck.CheckConnection(checkCtx, state)\n\t\t})\n\t}\n\treturn eg.Wait()\n}\n\n// StartDelivery starts new message delivery, runs connection and sender checks, sender modifiers\n// and selects source block from config to use for handling.\n//\n// Returned module.Delivery implements PartialDelivery. If underlying target doesn't\n// support it, msgpipeline will copy the returned error for all recipients handled\n// by target.\nfunc (d *MsgPipeline) StartDelivery(ctx context.Context, msgMeta *module.MsgMetadata, mailFrom string) (module.Delivery, error) {\n\tdd := msgpipelineDelivery{\n\t\td:                  d,\n\t\trcptModifiersState: make(map[*rcptBlock]module.ModifierState),\n\t\tdeliveries:         make(map[module.DeliveryTarget]*delivery),\n\t\tmsgMeta:            msgMeta,\n\t\tlog:                target.DeliveryLogger(d.Log, msgMeta),\n\t}\n\tdd.checkRunner = newCheckRunner(msgMeta, dd.log, d.Resolver)\n\tdd.checkRunner.doDMARC = d.doDMARC\n\n\tif msgMeta.OriginalRcpts == nil {\n\t\tmsgMeta.OriginalRcpts = map[string]string{}\n\t}\n\n\tif err := dd.start(ctx, msgMeta, mailFrom); err != nil {\n\t\tdd.close()\n\t\treturn nil, err\n\t}\n\n\treturn &dd, nil\n}\n\nfunc (dd *msgpipelineDelivery) start(ctx context.Context, msgMeta *module.MsgMetadata, mailFrom string) error {\n\tvar err error\n\n\tif err := dd.checkRunner.checkConnSender(ctx, dd.d.globalChecks, mailFrom); err != nil {\n\t\treturn err\n\t}\n\n\tif mailFrom, err = dd.initRunGlobalModifiers(ctx, msgMeta, mailFrom); err != nil {\n\t\treturn err\n\t}\n\n\tsourceBlock, err := dd.srcBlockForAddr(ctx, mailFrom)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif sourceBlock.rejectErr != nil {\n\t\tdd.log.Debugf(\"sender %s rejected with error: %v\", mailFrom, sourceBlock.rejectErr)\n\t\treturn sourceBlock.rejectErr\n\t}\n\tdd.sourceBlock = sourceBlock\n\n\tif err := dd.checkRunner.checkConnSender(ctx, sourceBlock.checks, mailFrom); err != nil {\n\t\treturn err\n\t}\n\n\tsourceModifiersState, err := sourceBlock.modifiers.ModStateForMsg(ctx, msgMeta)\n\tif err != nil {\n\t\treturn err\n\t}\n\tmailFrom, err = sourceModifiersState.RewriteSender(ctx, mailFrom)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdd.sourceModifiersState = sourceModifiersState\n\n\tdd.sourceAddr = mailFrom\n\treturn nil\n}\n\nfunc (dd *msgpipelineDelivery) initRunGlobalModifiers(ctx context.Context, msgMeta *module.MsgMetadata, mailFrom string) (string, error) {\n\tglobalModifiersState, err := dd.d.globalModifiers.ModStateForMsg(ctx, msgMeta)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tmailFrom, err = globalModifiersState.RewriteSender(ctx, mailFrom)\n\tif err != nil {\n\t\tif err := globalModifiersState.Close(); err != nil {\n\t\t\tdd.log.Error(\"failed to close global modifiers state\", err)\n\t\t}\n\t\treturn \"\", err\n\t}\n\tdd.globalModifiersState = globalModifiersState\n\treturn mailFrom, nil\n}\n\nfunc (dd *msgpipelineDelivery) srcBlockForAddr(ctx context.Context, mailFrom string) (sourceBlock, error) {\n\tcleanFrom := mailFrom\n\tif mailFrom != \"\" {\n\t\tvar err error\n\t\tcleanFrom, err = address.ForLookup(mailFrom)\n\t\tif err != nil {\n\t\t\treturn sourceBlock{}, &exterrors.SMTPError{\n\t\t\t\tCode:         501,\n\t\t\t\tEnhancedCode: exterrors.EnhancedCode{5, 1, 7},\n\t\t\t\tMessage:      \"Unable to normalize the sender address\",\n\t\t\t\tErr:          err,\n\t\t\t}\n\t\t}\n\t}\n\n\tfor _, srcIn := range dd.d.sourceIn {\n\t\t_, ok, err := srcIn.t.Lookup(ctx, cleanFrom)\n\t\tif err != nil {\n\t\t\tdd.log.Error(\"source_in lookup failed\", err, \"key\", cleanFrom)\n\t\t\tcontinue\n\t\t}\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\t\treturn srcIn.block, nil\n\t}\n\n\t// First try to match against complete address.\n\tsrcBlock, ok := dd.d.perSource[cleanFrom]\n\tif !ok {\n\t\t// Then try domain-only.\n\t\t_, domain, err := address.Split(cleanFrom)\n\t\t// mailFrom != \"\" is added as a special condition\n\t\t// instead of extending address.Split because \"\"\n\t\t// is not a valid RFC 282 address and only a special\n\t\t// value for SMTP.\n\t\tif err != nil && cleanFrom != \"\" {\n\t\t\treturn sourceBlock{}, &exterrors.SMTPError{\n\t\t\t\tCode:         501,\n\t\t\t\tEnhancedCode: exterrors.EnhancedCode{5, 1, 3},\n\t\t\t\tMessage:      \"Invalid sender address\",\n\t\t\t\tErr:          err,\n\t\t\t\tReason:       \"Can't extract local-part and host-part\",\n\t\t\t}\n\t\t}\n\n\t\t// domain is already case-folded and normalized by the message source.\n\t\tsrcBlock, ok = dd.d.perSource[domain]\n\t\tif !ok {\n\t\t\t// Fallback to the default source block.\n\t\t\tsrcBlock = dd.d.defaultSource\n\t\t\tdd.log.Debugf(\"sender %s matched by default rule\", mailFrom)\n\t\t} else {\n\t\t\tdd.log.Debugf(\"sender %s matched by domain rule '%s'\", mailFrom, domain)\n\t\t}\n\t} else {\n\t\tdd.log.Debugf(\"sender %s matched by address rule '%s'\", mailFrom, cleanFrom)\n\t}\n\treturn srcBlock, nil\n}\n\ntype delivery struct {\n\tmodule.Delivery\n\t// Recipient addresses this delivery object is used for, original values (not modified by RewriteRcpt).\n\trecipients []string\n}\n\ntype msgpipelineDelivery struct {\n\td *MsgPipeline\n\n\tglobalModifiersState module.ModifierState\n\tsourceModifiersState module.ModifierState\n\trcptModifiersState   map[*rcptBlock]module.ModifierState\n\n\tlog *log.Logger\n\n\tsourceAddr  string\n\tsourceBlock sourceBlock\n\n\tdeliveries  map[module.DeliveryTarget]*delivery\n\tmsgMeta     *module.MsgMetadata\n\tcheckRunner *checkRunner\n}\n\nfunc (dd *msgpipelineDelivery) AddRcpt(ctx context.Context, to string, opts smtp.RcptOptions) error {\n\tif err := dd.checkRunner.checkRcpt(ctx, dd.d.globalChecks, to); err != nil {\n\t\treturn err\n\t}\n\tif err := dd.checkRunner.checkRcpt(ctx, dd.sourceBlock.checks, to); err != nil {\n\t\treturn err\n\t}\n\n\toriginalTo := to\n\n\tnewTo, err := dd.globalModifiersState.RewriteRcpt(ctx, to)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdd.log.Debugln(\"global rcpt modifiers:\", to, \"=>\", newTo)\n\tresultTo := newTo\n\tnewTo = []string{}\n\n\tfor _, to = range resultTo {\n\t\tvar tempTo []string\n\t\ttempTo, err = dd.sourceModifiersState.RewriteRcpt(ctx, to)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tnewTo = append(newTo, tempTo...)\n\t}\n\tdd.log.Debugln(\"per-source rcpt modifiers:\", to, \"=>\", newTo)\n\tresultTo = newTo\n\n\tfor _, to = range resultTo {\n\t\twrapErr := func(err error) error {\n\t\t\treturn exterrors.WithFields(err, map[string]interface{}{\n\t\t\t\t\"effective_rcpt\": to,\n\t\t\t})\n\t\t}\n\n\t\trcptBlock, err := dd.rcptBlockForAddr(ctx, to)\n\t\tif err != nil {\n\t\t\treturn wrapErr(err)\n\t\t}\n\n\t\tif rcptBlock.rejectErr != nil {\n\t\t\treturn wrapErr(rcptBlock.rejectErr)\n\t\t}\n\n\t\tif err := dd.checkRunner.checkRcpt(ctx, rcptBlock.checks, to); err != nil {\n\t\t\treturn wrapErr(err)\n\t\t}\n\n\t\trcptModifiersState, err := dd.getRcptModifiers(ctx, rcptBlock, to)\n\t\tif err != nil {\n\t\t\treturn wrapErr(err)\n\t\t}\n\n\t\tnewTo, err = rcptModifiersState.RewriteRcpt(ctx, to)\n\t\tif err != nil {\n\t\t\tif err := rcptModifiersState.Close(); err != nil {\n\t\t\t\tdd.log.Error(\"failed to close rcpt modifiers state\", err)\n\t\t\t}\n\t\t\treturn wrapErr(err)\n\t\t}\n\t\tdd.log.Debugln(\"per-rcpt modifiers:\", to, \"=>\", newTo)\n\n\t\tfor _, to = range newTo {\n\t\t\twrapErr = func(err error) error {\n\t\t\t\treturn exterrors.WithFields(err, map[string]interface{}{\n\t\t\t\t\t\"effective_rcpt\": to,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif originalTo != to {\n\t\t\t\tdd.msgMeta.OriginalRcpts[to] = originalTo\n\t\t\t}\n\n\t\t\tfor _, tgt := range rcptBlock.targets {\n\t\t\t\t// Do not wrap errors coming from nested pipeline target delivery since\n\t\t\t\t// that pipeline itself will insert effective_rcpt field and could do\n\t\t\t\t// its own rewriting - we do not want to hide it from the admin in\n\t\t\t\t// error messages.\n\t\t\t\twrapErr := wrapErr\n\t\t\t\tif _, ok := tgt.(*MsgPipeline); ok {\n\t\t\t\t\twrapErr = func(err error) error { return err }\n\t\t\t\t}\n\n\t\t\t\tdelivery, err := dd.getDelivery(ctx, tgt)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn wrapErr(err)\n\t\t\t\t}\n\n\t\t\t\tif err := delivery.AddRcpt(ctx, to, opts); err != nil {\n\t\t\t\t\treturn wrapErr(err)\n\t\t\t\t}\n\t\t\t\tdelivery.recipients = append(delivery.recipients, originalTo)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (dd *msgpipelineDelivery) Body(ctx context.Context, header textproto.Header, body buffer.Buffer) error {\n\tif err := dd.checkRunner.checkBody(ctx, dd.d.globalChecks, header, body); err != nil {\n\t\treturn err\n\t}\n\tif err := dd.checkRunner.checkBody(ctx, dd.sourceBlock.checks, header, body); err != nil {\n\t\treturn err\n\t}\n\tfor blk := range dd.rcptModifiersState {\n\t\tif err := dd.checkRunner.checkBody(ctx, blk.checks, header, body); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif dd.d.FirstPipeline {\n\t\t// Add Received *after* checks to make sure they see the message literally\n\t\t// how we received it BUT place it below any other field that might be\n\t\t// added by applyResults (including Authentication-Results)\n\t\t// per recommendation in RFC 7001, Section 4 (see GH issue #135).\n\t\treceived, err := target.GenerateReceived(ctx, dd.msgMeta, dd.d.Hostname, dd.msgMeta.OriginalFrom)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\theader.Add(\"Received\", received)\n\t}\n\n\tif err := dd.checkRunner.applyResults(dd.d.Hostname, &header); err != nil {\n\t\treturn err\n\t}\n\n\t// Run modifiers after Authentication-Results addition to make\n\t// sure signatures, etc will cover it.\n\tif err := dd.globalModifiersState.RewriteBody(ctx, &header, body); err != nil {\n\t\treturn err\n\t}\n\tif err := dd.sourceModifiersState.RewriteBody(ctx, &header, body); err != nil {\n\t\treturn err\n\t}\n\tfor _, modifiers := range dd.rcptModifiersState {\n\t\tif err := modifiers.RewriteBody(ctx, &header, body); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tfor _, delivery := range dd.deliveries {\n\t\tif err := delivery.Body(ctx, header, body); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdd.log.Debugf(\"delivery.Body ok, Delivery object = %T\", delivery)\n\t}\n\treturn nil\n}\n\n// statusCollector wraps StatusCollector and adds reverse translation\n// of recipients for all statuses.]\n//\n// We can't let delivery targets set statuses directly because they see\n// modified addresses (RewriteRcpt) and we are supposed to report\n// statuses using original values. Additionally, we should still avoid\n// collect-and-them-report approach since statuses should be reported\n// as soon as possible (that is required by LMTP).\ntype statusCollector struct {\n\toriginalRcpts map[string]string\n\twrapped       module.StatusCollector\n}\n\nfunc (sc statusCollector) SetStatus(rcptTo string, err error) {\n\toriginal, ok := sc.originalRcpts[rcptTo]\n\tif ok {\n\t\trcptTo = original\n\t}\n\tsc.wrapped.SetStatus(rcptTo, err)\n}\n\nfunc (dd *msgpipelineDelivery) BodyNonAtomic(ctx context.Context, c module.StatusCollector, header textproto.Header, body buffer.Buffer) {\n\tsetStatusAll := func(err error) {\n\t\tfor _, delivery := range dd.deliveries {\n\t\t\tfor _, rcpt := range delivery.recipients {\n\t\t\t\tc.SetStatus(rcpt, err)\n\t\t\t}\n\t\t}\n\t}\n\n\tif err := dd.checkRunner.checkBody(ctx, dd.d.globalChecks, header, body); err != nil {\n\t\tsetStatusAll(err)\n\t\treturn\n\t}\n\tif err := dd.checkRunner.checkBody(ctx, dd.sourceBlock.checks, header, body); err != nil {\n\t\tsetStatusAll(err)\n\t\treturn\n\t}\n\n\t// Run modifiers after Authentication-Results addition to make\n\t// sure signatures, etc will cover it.\n\tif err := dd.globalModifiersState.RewriteBody(ctx, &header, body); err != nil {\n\t\tsetStatusAll(err)\n\t\treturn\n\t}\n\tif err := dd.sourceModifiersState.RewriteBody(ctx, &header, body); err != nil {\n\t\tsetStatusAll(err)\n\t\treturn\n\t}\n\tfor _, modifiers := range dd.rcptModifiersState {\n\t\tif err := modifiers.RewriteBody(ctx, &header, body); err != nil {\n\t\t\tsetStatusAll(err)\n\t\t\treturn\n\t\t}\n\t}\n\n\tfor _, delivery := range dd.deliveries {\n\t\tpartDelivery, ok := delivery.Delivery.(module.PartialDelivery)\n\t\tif ok {\n\t\t\tpartDelivery.BodyNonAtomic(ctx, statusCollector{\n\t\t\t\toriginalRcpts: dd.msgMeta.OriginalRcpts,\n\t\t\t\twrapped:       c,\n\t\t\t}, header, body)\n\t\t\tcontinue\n\t\t}\n\n\t\tif err := delivery.Body(ctx, header, body); err != nil {\n\t\t\tfor _, rcpt := range delivery.recipients {\n\t\t\t\tc.SetStatus(rcpt, err)\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (dd *msgpipelineDelivery) Commit(ctx context.Context) error {\n\tdd.close()\n\n\tfor _, delivery := range dd.deliveries {\n\t\tif err := delivery.Commit(ctx); err != nil {\n\t\t\t// No point in Committing remaining deliveries, everything is broken already.\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (dd *msgpipelineDelivery) close() {\n\tdd.checkRunner.close()\n\n\tif dd.globalModifiersState != nil {\n\t\tif err := dd.globalModifiersState.Close(); err != nil {\n\t\t\tdd.log.Error(\"failed to close global modifiers state\", err)\n\t\t}\n\t}\n\tif dd.sourceModifiersState != nil {\n\t\tif err := dd.sourceModifiersState.Close(); err != nil {\n\t\t\tdd.log.Error(\"failed to close source modifiers state\", err)\n\t\t}\n\t}\n\tfor _, modifiers := range dd.rcptModifiersState {\n\t\tif err := modifiers.Close(); err != nil {\n\t\t\tdd.log.Error(\"failed to close rcpt modifiers state\", err)\n\t\t}\n\t}\n}\n\nfunc (dd *msgpipelineDelivery) Abort(ctx context.Context) error {\n\tdd.close()\n\n\tvar lastErr error\n\tfor _, delivery := range dd.deliveries {\n\t\tif err := delivery.Abort(ctx); err != nil {\n\t\t\tdd.log.Debugf(\"delivery.Abort failure, Delivery object = %T: %v\", delivery, err)\n\t\t\tlastErr = err\n\t\t\t// Continue anyway and try to Abort all remaining delivery objects.\n\t\t}\n\t}\n\treturn lastErr\n}\n\nfunc (dd *msgpipelineDelivery) rcptBlockForAddr(ctx context.Context, rcptTo string) (*rcptBlock, error) {\n\tcleanRcpt, err := address.ForLookup(rcptTo)\n\tif err != nil {\n\t\treturn nil, &exterrors.SMTPError{\n\t\t\tCode:         553,\n\t\t\tEnhancedCode: exterrors.EnhancedCode{5, 1, 2},\n\t\t\tMessage:      \"Unable to normalize the recipient address\",\n\t\t\tErr:          err,\n\t\t}\n\t}\n\n\tfor _, rcptIn := range dd.sourceBlock.rcptIn {\n\t\t_, ok, err := rcptIn.t.Lookup(ctx, cleanRcpt)\n\t\tif err != nil {\n\t\t\tdd.log.Error(\"destination_in lookup failed\", err, \"key\", cleanRcpt)\n\t\t\tcontinue\n\t\t}\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\t\treturn rcptIn.block, nil\n\t}\n\n\t// First try to match against complete address.\n\trcptBlock, ok := dd.sourceBlock.perRcpt[cleanRcpt]\n\tif !ok {\n\t\t// Then try domain-only.\n\t\t_, domain, err := address.Split(cleanRcpt)\n\t\tif err != nil {\n\t\t\treturn nil, &exterrors.SMTPError{\n\t\t\t\tCode:         501,\n\t\t\t\tEnhancedCode: exterrors.EnhancedCode{5, 1, 3},\n\t\t\t\tMessage:      \"Invalid recipient address\",\n\t\t\t\tErr:          err,\n\t\t\t\tReason:       \"Can't extract local-part and host-part\",\n\t\t\t}\n\t\t}\n\n\t\t// domain is already case-folded and normalized because it is a part of\n\t\t// cleanRcpt.\n\t\trcptBlock, ok = dd.sourceBlock.perRcpt[domain]\n\t\tif !ok {\n\t\t\t// Fallback to the default source block.\n\t\t\trcptBlock = dd.sourceBlock.defaultRcpt\n\t\t\tdd.log.Debugf(\"recipient %s matched by default rule (clean = %s)\", rcptTo, cleanRcpt)\n\t\t} else {\n\t\t\tdd.log.Debugf(\"recipient %s matched by domain rule '%s'\", rcptTo, domain)\n\t\t}\n\t} else {\n\t\tdd.log.Debugf(\"recipient %s matched by address rule '%s'\", rcptTo, cleanRcpt)\n\t}\n\treturn rcptBlock, nil\n}\n\nfunc (dd *msgpipelineDelivery) getRcptModifiers(ctx context.Context, rcptBlock *rcptBlock, rcptTo string) (module.ModifierState, error) {\n\trcptModifiersState, ok := dd.rcptModifiersState[rcptBlock]\n\tif ok {\n\t\treturn rcptModifiersState, nil\n\t}\n\n\trcptModifiersState, err := rcptBlock.modifiers.ModStateForMsg(ctx, dd.msgMeta)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tnewSender, err := rcptModifiersState.RewriteSender(ctx, dd.sourceAddr)\n\tif err == nil && newSender != dd.sourceAddr {\n\t\tdd.log.Msg(\"Per-recipient modifier changed sender address. This is not supported and will \"+\n\t\t\t\"be ignored.\", \"rcpt\", rcptTo, \"originalFrom\", dd.sourceAddr, \"modifiedFrom\", newSender)\n\t}\n\n\tdd.rcptModifiersState[rcptBlock] = rcptModifiersState\n\treturn rcptModifiersState, nil\n}\n\nfunc (dd *msgpipelineDelivery) getDelivery(ctx context.Context, tgt module.DeliveryTarget) (*delivery, error) {\n\tdelivery_, ok := dd.deliveries[tgt]\n\tif ok {\n\t\treturn delivery_, nil\n\t}\n\n\tdeliveryObj, err := tgt.StartDelivery(ctx, dd.msgMeta, dd.sourceAddr)\n\tif err != nil {\n\t\tdd.log.Debugf(\"tgt.StartDelivery(%s) failure, target = %s: %v\", dd.sourceAddr, objectName(tgt), err)\n\t\treturn nil, err\n\t}\n\tdelivery_ = &delivery{Delivery: deliveryObj}\n\n\tdd.log.Debugf(\"tgt.StartDelivery(%s) ok, target = %s\", dd.sourceAddr, objectName(tgt))\n\n\tdd.deliveries[tgt] = delivery_\n\treturn delivery_, nil\n}\n\n// Mock returns a MsgPipeline that merely delivers messages to a specified target\n// and runs a set of checks.\n//\n// It is meant for use in tests for modules that embed a pipeline object.\nfunc Mock(tgt module.DeliveryTarget, globalChecks []module.Check) *MsgPipeline {\n\treturn &MsgPipeline{\n\t\tmsgpipelineCfg: msgpipelineCfg{\n\t\t\tglobalChecks: globalChecks,\n\t\t\tperSource:    map[string]sourceBlock{},\n\t\t\tdefaultSource: sourceBlock{\n\t\t\t\tperRcpt: map[string]*rcptBlock{},\n\t\t\t\tdefaultRcpt: &rcptBlock{\n\t\t\t\t\ttargets: []module.DeliveryTarget{tgt},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n}\n"
  },
  {
    "path": "internal/msgpipeline/msgpipeline_test.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage msgpipeline\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"testing\"\n\n\t\"github.com/emersion/go-message/textproto\"\n\t\"github.com/emersion/go-smtp\"\n\t\"github.com/foxcpp/maddy/framework/buffer\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/internal/modify\"\n\t\"github.com/foxcpp/maddy/internal/testutils\"\n)\n\nfunc TestMsgPipeline_AllToTarget(t *testing.T) {\n\ttarget := testutils.Target{}\n\td := MsgPipeline{\n\t\tmsgpipelineCfg: msgpipelineCfg{\n\t\t\tperSource: map[string]sourceBlock{},\n\t\t\tdefaultSource: sourceBlock{\n\t\t\t\tperRcpt: map[string]*rcptBlock{},\n\t\t\t\tdefaultRcpt: &rcptBlock{\n\t\t\t\t\ttargets: []module.DeliveryTarget{&target},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tLog: testutils.Logger(t, \"msgpipeline\"),\n\t}\n\n\ttestutils.DoTestDelivery(t, &d, \"sender@example.com\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"})\n\n\tif len(target.Messages) != 1 {\n\t\tt.Fatalf(\"wrong amount of messages received, want %d, got %d\", 1, len(target.Messages))\n\t}\n\n\ttestutils.CheckTestMessage(t, &target, 0, \"sender@example.com\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"})\n}\n\nfunc TestMsgPipeline_PerSourceDomainSplit(t *testing.T) {\n\torgTarget, comTarget := testutils.Target{InstName: \"orgTarget\"}, testutils.Target{InstName: \"comTarget\"}\n\td := MsgPipeline{\n\t\tmsgpipelineCfg: msgpipelineCfg{\n\t\t\tperSource: map[string]sourceBlock{\n\t\t\t\t\"example.com\": {\n\t\t\t\t\tperRcpt: map[string]*rcptBlock{},\n\t\t\t\t\tdefaultRcpt: &rcptBlock{\n\t\t\t\t\t\ttargets: []module.DeliveryTarget{&comTarget},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t\"example.org\": {\n\t\t\t\t\tperRcpt: map[string]*rcptBlock{},\n\t\t\t\t\tdefaultRcpt: &rcptBlock{\n\t\t\t\t\t\ttargets: []module.DeliveryTarget{&orgTarget},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tdefaultSource: sourceBlock{rejectErr: errors.New(\"default src block used\")},\n\t\t},\n\t\tLog: testutils.Logger(t, \"msgpipeline\"),\n\t}\n\n\ttestutils.DoTestDelivery(t, &d, \"sender@example.com\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"})\n\ttestutils.DoTestDelivery(t, &d, \"sender@example.org\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"})\n\n\tif len(comTarget.Messages) != 1 {\n\t\tt.Fatalf(\"wrong amount of messages received for comTarget, want %d, got %d\", 1, len(comTarget.Messages))\n\t}\n\ttestutils.CheckTestMessage(t, &comTarget, 0, \"sender@example.com\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"})\n\n\tif len(orgTarget.Messages) != 1 {\n\t\tt.Fatalf(\"wrong amount of messages received for orgTarget, want %d, got %d\", 1, len(orgTarget.Messages))\n\t}\n\ttestutils.CheckTestMessage(t, &orgTarget, 0, \"sender@example.org\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"})\n}\n\nfunc TestMsgPipeline_SourceIn(t *testing.T) {\n\ttblTarget, comTarget := testutils.Target{InstName: \"tblTarget\"}, testutils.Target{InstName: \"comTarget\"}\n\td := MsgPipeline{\n\t\tmsgpipelineCfg: msgpipelineCfg{\n\t\t\tsourceIn: []sourceIn{\n\t\t\t\t{\n\t\t\t\t\tt:     testutils.Table{},\n\t\t\t\t\tblock: sourceBlock{rejectErr: errors.New(\"non-matching block was used\")},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tt:     testutils.Table{Err: errors.New(\"this one will fail\")},\n\t\t\t\t\tblock: sourceBlock{rejectErr: errors.New(\"failing block was used\")},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tt: testutils.Table{\n\t\t\t\t\t\tM: map[string]string{\n\t\t\t\t\t\t\t\"specific@example.com\": \"\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tblock: sourceBlock{\n\t\t\t\t\t\tperRcpt: map[string]*rcptBlock{},\n\t\t\t\t\t\tdefaultRcpt: &rcptBlock{\n\t\t\t\t\t\t\ttargets: []module.DeliveryTarget{&tblTarget},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tperSource: map[string]sourceBlock{\n\t\t\t\t\"example.com\": {\n\t\t\t\t\tperRcpt: map[string]*rcptBlock{},\n\t\t\t\t\tdefaultRcpt: &rcptBlock{\n\t\t\t\t\t\ttargets: []module.DeliveryTarget{&comTarget},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tdefaultSource: sourceBlock{rejectErr: errors.New(\"default src block used\")},\n\t\t},\n\t\tLog: testutils.Logger(t, \"msgpipeline\"),\n\t}\n\n\ttestutils.DoTestDelivery(t, &d, \"sender@example.com\", []string{\"rcpt@example.com\"})\n\ttestutils.DoTestDelivery(t, &d, \"specific@example.com\", []string{\"rcpt@example.com\"})\n\n\tif len(comTarget.Messages) != 1 {\n\t\tt.Fatalf(\"wrong amount of messages received for comTarget, want %d, got %d\", 1, len(comTarget.Messages))\n\t}\n\ttestutils.CheckTestMessage(t, &comTarget, 0, \"sender@example.com\", []string{\"rcpt@example.com\"})\n\n\tif len(tblTarget.Messages) != 1 {\n\t\tt.Fatalf(\"wrong amount of messages received for orgTarget, want %d, got %d\", 1, len(tblTarget.Messages))\n\t}\n\ttestutils.CheckTestMessage(t, &tblTarget, 0, \"specific@example.com\", []string{\"rcpt@example.com\"})\n}\n\nfunc TestMsgPipeline_EmptyMAILFROM(t *testing.T) {\n\ttarget := testutils.Target{InstName: \"target\"}\n\td := MsgPipeline{\n\t\tmsgpipelineCfg: msgpipelineCfg{\n\t\t\tperSource: map[string]sourceBlock{},\n\t\t\tdefaultSource: sourceBlock{\n\t\t\t\tperRcpt: map[string]*rcptBlock{},\n\t\t\t\tdefaultRcpt: &rcptBlock{\n\t\t\t\t\ttargets: []module.DeliveryTarget{&target},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tLog: testutils.Logger(t, \"msgpipeline\"),\n\t}\n\n\ttestutils.DoTestDelivery(t, &d, \"\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"})\n\n\tif len(target.Messages) != 1 {\n\t\tt.Fatalf(\"wrong amount of messages received for target, want %d, got %d\", 1, len(target.Messages))\n\t}\n\ttestutils.CheckTestMessage(t, &target, 0, \"\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"})\n}\n\nfunc TestMsgPipeline_EmptyMAILFROM_ExplicitDest(t *testing.T) {\n\ttarget := testutils.Target{InstName: \"target\"}\n\td := MsgPipeline{\n\t\tmsgpipelineCfg: msgpipelineCfg{\n\t\t\tperSource: map[string]sourceBlock{\n\t\t\t\t\"\": {\n\t\t\t\t\tperRcpt: map[string]*rcptBlock{},\n\t\t\t\t\tdefaultRcpt: &rcptBlock{\n\t\t\t\t\t\ttargets: []module.DeliveryTarget{&target},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tdefaultSource: sourceBlock{rejectErr: errors.New(\"default src block used\")},\n\t\t},\n\t\tLog: testutils.Logger(t, \"msgpipeline\"),\n\t}\n\n\ttestutils.DoTestDelivery(t, &d, \"\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"})\n\n\tif len(target.Messages) != 1 {\n\t\tt.Fatalf(\"wrong amount of messages received for target, want %d, got %d\", 1, len(target.Messages))\n\t}\n\ttestutils.CheckTestMessage(t, &target, 0, \"\", []string{\"rcpt1@example.com\", \"rcpt2@example.com\"})\n}\n\nfunc TestMsgPipeline_PerRcptAddrSplit(t *testing.T) {\n\ttarget1, target2 := testutils.Target{InstName: \"target1\"}, testutils.Target{InstName: \"target2\"}\n\td := MsgPipeline{\n\t\tmsgpipelineCfg: msgpipelineCfg{\n\t\t\tperSource: map[string]sourceBlock{},\n\t\t\tdefaultSource: sourceBlock{\n\t\t\t\tperRcpt: map[string]*rcptBlock{\n\t\t\t\t\t\"rcpt1@example.com\": {\n\t\t\t\t\t\ttargets: []module.DeliveryTarget{&target1},\n\t\t\t\t\t},\n\t\t\t\t\t\"rcpt2@example.com\": {\n\t\t\t\t\t\ttargets: []module.DeliveryTarget{&target2},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdefaultRcpt: &rcptBlock{\n\t\t\t\t\trejectErr: errors.New(\"defaultRcpt block used\"),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tLog: testutils.Logger(t, \"msgpipeline\"),\n\t}\n\n\ttestutils.DoTestDelivery(t, &d, \"sender@example.com\", []string{\"rcpt1@example.com\"})\n\ttestutils.DoTestDelivery(t, &d, \"sender@example.com\", []string{\"rcpt2@example.com\"})\n\n\tif len(target1.Messages) != 1 {\n\t\tt.Errorf(\"wrong amount of messages received for target1, want %d, got %d\", 1, len(target1.Messages))\n\t}\n\ttestutils.CheckTestMessage(t, &target1, 0, \"sender@example.com\", []string{\"rcpt1@example.com\"})\n\n\tif len(target2.Messages) != 1 {\n\t\tt.Errorf(\"wrong amount of messages received for target1, want %d, got %d\", 1, len(target2.Messages))\n\t}\n\ttestutils.CheckTestMessage(t, &target2, 0, \"sender@example.com\", []string{\"rcpt2@example.com\"})\n}\n\nfunc TestMsgPipeline_PerRcptDomainSplit(t *testing.T) {\n\ttarget1, target2 := testutils.Target{InstName: \"target1\"}, testutils.Target{InstName: \"target2\"}\n\td := MsgPipeline{\n\t\tmsgpipelineCfg: msgpipelineCfg{\n\t\t\tperSource: map[string]sourceBlock{},\n\t\t\tdefaultSource: sourceBlock{\n\t\t\t\tperRcpt: map[string]*rcptBlock{\n\t\t\t\t\t\"example.com\": {\n\t\t\t\t\t\ttargets: []module.DeliveryTarget{&target1},\n\t\t\t\t\t},\n\t\t\t\t\t\"example.org\": {\n\t\t\t\t\t\ttargets: []module.DeliveryTarget{&target2},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdefaultRcpt: &rcptBlock{\n\t\t\t\t\trejectErr: errors.New(\"defaultRcpt block used\"),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tLog: testutils.Logger(t, \"msgpipeline\"),\n\t}\n\n\ttestutils.DoTestDelivery(t, &d, \"sender@example.com\", []string{\"rcpt1@example.com\", \"rcpt2@example.org\"})\n\ttestutils.DoTestDelivery(t, &d, \"sender@example.com\", []string{\"rcpt1@example.org\", \"rcpt2@example.com\"})\n\n\tif len(target1.Messages) != 2 {\n\t\tt.Errorf(\"wrong amount of messages received for target1, want %d, got %d\", 2, len(target1.Messages))\n\t}\n\ttestutils.CheckTestMessage(t, &target1, 0, \"sender@example.com\", []string{\"rcpt1@example.com\"})\n\ttestutils.CheckTestMessage(t, &target1, 1, \"sender@example.com\", []string{\"rcpt2@example.com\"})\n\n\tif len(target2.Messages) != 2 {\n\t\tt.Errorf(\"wrong amount of messages received for target2, want %d, got %d\", 2, len(target2.Messages))\n\t}\n\ttestutils.CheckTestMessage(t, &target2, 0, \"sender@example.com\", []string{\"rcpt2@example.org\"})\n\ttestutils.CheckTestMessage(t, &target2, 1, \"sender@example.com\", []string{\"rcpt1@example.org\"})\n}\n\nfunc TestMsgPipeline_DestInSplit(t *testing.T) {\n\ttarget1, target2 := testutils.Target{InstName: \"target1\"}, testutils.Target{InstName: \"target2\"}\n\td := MsgPipeline{\n\t\tmsgpipelineCfg: msgpipelineCfg{\n\t\t\tperSource: map[string]sourceBlock{},\n\t\t\tdefaultSource: sourceBlock{\n\t\t\t\trcptIn: []rcptIn{\n\t\t\t\t\t{\n\t\t\t\t\t\tt:     testutils.Table{},\n\t\t\t\t\t\tblock: &rcptBlock{rejectErr: errors.New(\"non-matching block was used\")},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tt:     testutils.Table{Err: errors.New(\"nope\")},\n\t\t\t\t\t\tblock: &rcptBlock{rejectErr: errors.New(\"failing block was used\")},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tt: testutils.Table{\n\t\t\t\t\t\t\tM: map[string]string{\n\t\t\t\t\t\t\t\t\"specific@example.com\": \"\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tblock: &rcptBlock{\n\t\t\t\t\t\t\ttargets: []module.DeliveryTarget{&target2},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tperRcpt: map[string]*rcptBlock{\n\t\t\t\t\t\"example.com\": {\n\t\t\t\t\t\ttargets: []module.DeliveryTarget{&target1},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdefaultRcpt: &rcptBlock{\n\t\t\t\t\trejectErr: errors.New(\"defaultRcpt block used\"),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tLog: testutils.Logger(t, \"msgpipeline\"),\n\t}\n\n\ttestutils.DoTestDelivery(t, &d, \"sender@example.com\", []string{\"rcpt1@example.com\", \"specific@example.com\"})\n\n\tif len(target1.Messages) != 1 {\n\t\tt.Errorf(\"wrong amount of messages received for target1, want %d, got %d\", 1, len(target1.Messages))\n\t}\n\ttestutils.CheckTestMessage(t, &target1, 0, \"sender@example.com\", []string{\"rcpt1@example.com\"})\n\n\tif len(target2.Messages) != 1 {\n\t\tt.Errorf(\"wrong amount of messages received for target2, want %d, got %d\", 1, len(target2.Messages))\n\t}\n\ttestutils.CheckTestMessage(t, &target2, 0, \"sender@example.com\", []string{\"specific@example.com\"})\n}\n\nfunc TestMsgPipeline_PerSourceAddrAndDomainSplit(t *testing.T) {\n\ttarget1, target2 := testutils.Target{InstName: \"target1\"}, testutils.Target{InstName: \"target2\"}\n\td := MsgPipeline{\n\t\tmsgpipelineCfg: msgpipelineCfg{\n\t\t\tperSource: map[string]sourceBlock{\n\t\t\t\t\"sender1@example.com\": {\n\t\t\t\t\tperRcpt: map[string]*rcptBlock{},\n\t\t\t\t\tdefaultRcpt: &rcptBlock{\n\t\t\t\t\t\ttargets: []module.DeliveryTarget{&target1},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t\"example.com\": {\n\t\t\t\t\tperRcpt: map[string]*rcptBlock{},\n\t\t\t\t\tdefaultRcpt: &rcptBlock{\n\t\t\t\t\t\ttargets: []module.DeliveryTarget{&target2},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tdefaultSource: sourceBlock{rejectErr: errors.New(\"default src block used\")},\n\t\t},\n\t\tLog: testutils.Logger(t, \"msgpipeline\"),\n\t}\n\n\ttestutils.DoTestDelivery(t, &d, \"sender1@example.com\", []string{\"rcpt@example.com\"})\n\ttestutils.DoTestDelivery(t, &d, \"sender2@example.com\", []string{\"rcpt@example.com\"})\n\n\tif len(target1.Messages) != 1 {\n\t\tt.Fatalf(\"wrong amount of messages received for target1, want %d, got %d\", 1, len(target1.Messages))\n\t}\n\ttestutils.CheckTestMessage(t, &target1, 0, \"sender1@example.com\", []string{\"rcpt@example.com\"})\n\n\tif len(target2.Messages) != 1 {\n\t\tt.Fatalf(\"wrong amount of messages received for target2, want %d, got %d\", 1, len(target2.Messages))\n\t}\n\ttestutils.CheckTestMessage(t, &target2, 0, \"sender2@example.com\", []string{\"rcpt@example.com\"})\n}\n\nfunc TestMsgPipeline_PerSourceReject(t *testing.T) {\n\ttarget := testutils.Target{}\n\td := MsgPipeline{\n\t\tmsgpipelineCfg: msgpipelineCfg{\n\t\t\tperSource: map[string]sourceBlock{\n\t\t\t\t\"sender1@example.com\": {\n\t\t\t\t\tperRcpt: map[string]*rcptBlock{},\n\t\t\t\t\tdefaultRcpt: &rcptBlock{\n\t\t\t\t\t\ttargets: []module.DeliveryTarget{&target},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t\"example.com\": {\n\t\t\t\t\tperRcpt:   map[string]*rcptBlock{},\n\t\t\t\t\trejectErr: errors.New(\"go away\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\tdefaultSource: sourceBlock{rejectErr: errors.New(\"go away\")},\n\t\t},\n\t\tLog: testutils.Logger(t, \"msgpipeline\"),\n\t}\n\n\ttestutils.DoTestDelivery(t, &d, \"sender1@example.com\", []string{\"rcpt@example.com\"})\n\n\t_, err := d.StartDelivery(context.Background(), &module.MsgMetadata{ID: \"testing\"}, \"sender2@example.com\")\n\tif err == nil {\n\t\tt.Error(\"expected error for delivery.StartDelivery, got nil\")\n\t}\n\n\t_, err = d.StartDelivery(context.Background(), &module.MsgMetadata{ID: \"testing\"}, \"sender2@example.org\")\n\tif err == nil {\n\t\tt.Error(\"expected error for delivery.StartDelivery, got nil\")\n\t}\n}\n\nfunc TestMsgPipeline_PerRcptReject(t *testing.T) {\n\ttarget := testutils.Target{}\n\td := MsgPipeline{\n\t\tmsgpipelineCfg: msgpipelineCfg{\n\t\t\tperSource: map[string]sourceBlock{},\n\t\t\tdefaultSource: sourceBlock{\n\t\t\t\tperRcpt: map[string]*rcptBlock{\n\t\t\t\t\t\"rcpt1@example.com\": {\n\t\t\t\t\t\ttargets: []module.DeliveryTarget{&target},\n\t\t\t\t\t},\n\t\t\t\t\t\"example.com\": {\n\t\t\t\t\t\trejectErr: errors.New(\"go away\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdefaultRcpt: &rcptBlock{\n\t\t\t\t\trejectErr: errors.New(\"go away\"),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tLog: testutils.Logger(t, \"msgpipeline\"),\n\t}\n\n\tdelivery, err := d.StartDelivery(context.Background(), &module.MsgMetadata{ID: \"testing\"}, \"sender@example.com\")\n\tif err != nil {\n\t\tt.Fatalf(\"unexpected StartDelivery err: %v\", err)\n\t}\n\tdefer func() {\n\t\tif err := delivery.Abort(context.Background()); err != nil {\n\t\t\tt.Fatalf(\"unexpected Abort err: %v\", err)\n\t\t}\n\t}()\n\n\tif err := delivery.AddRcpt(context.Background(), \"rcpt2@example.com\", smtp.RcptOptions{}); err == nil {\n\t\tt.Fatalf(\"expected error for delivery.AddRcpt(rcpt2@example.com), got nil\")\n\t}\n\tif err := delivery.AddRcpt(context.Background(), \"rcpt1@example.com\", smtp.RcptOptions{}); err != nil {\n\t\tt.Fatalf(\"unexpected AddRcpt err for %s: %v\", \"rcpt1@example.com\", err)\n\t}\n\tif err := delivery.Body(context.Background(), textproto.Header{}, buffer.MemoryBuffer{Slice: []byte(\"foobar\")}); err != nil {\n\t\tt.Fatalf(\"unexpected Body err: %v\", err)\n\t}\n\tif err := delivery.Commit(context.Background()); err != nil {\n\t\tt.Fatalf(\"unexpected Commit err: %v\", err)\n\t}\n}\n\nfunc TestMsgPipeline_PostmasterRcpt(t *testing.T) {\n\ttarget := testutils.Target{}\n\td := MsgPipeline{\n\t\tmsgpipelineCfg: msgpipelineCfg{\n\t\t\tperSource: map[string]sourceBlock{},\n\t\t\tdefaultSource: sourceBlock{\n\t\t\t\tperRcpt: map[string]*rcptBlock{\n\t\t\t\t\t\"postmaster\": {\n\t\t\t\t\t\ttargets: []module.DeliveryTarget{&target},\n\t\t\t\t\t},\n\t\t\t\t\t\"example.com\": {\n\t\t\t\t\t\trejectErr: errors.New(\"go away\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdefaultRcpt: &rcptBlock{\n\t\t\t\t\trejectErr: errors.New(\"go away\"),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tLog: testutils.Logger(t, \"msgpipeline\"),\n\t}\n\n\ttestutils.DoTestDelivery(t, &d, \"disappointed-user@example.com\", []string{\"postmaster\"})\n\tif len(target.Messages) != 1 {\n\t\tt.Fatalf(\"wrong amount of messages received for target, want %d, got %d\", 1, len(target.Messages))\n\t}\n\ttestutils.CheckTestMessage(t, &target, 0, \"disappointed-user@example.com\", []string{\"postmaster\"})\n}\n\nfunc TestMsgPipeline_PostmasterSrc(t *testing.T) {\n\ttarget := testutils.Target{}\n\td := MsgPipeline{\n\t\tmsgpipelineCfg: msgpipelineCfg{\n\t\t\tperSource: map[string]sourceBlock{\n\t\t\t\t\"postmaster\": {\n\t\t\t\t\tperRcpt: map[string]*rcptBlock{},\n\t\t\t\t\tdefaultRcpt: &rcptBlock{\n\t\t\t\t\t\ttargets: []module.DeliveryTarget{&target},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t\"example.com\": {\n\t\t\t\t\trejectErr: errors.New(\"go away\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\tdefaultSource: sourceBlock{\n\t\t\t\trejectErr: errors.New(\"go away\"),\n\t\t\t},\n\t\t},\n\t\tLog: testutils.Logger(t, \"msgpipeline\"),\n\t}\n\n\ttestutils.DoTestDelivery(t, &d, \"postmaster\", []string{\"disappointed-user@example.com\"})\n\tif len(target.Messages) != 1 {\n\t\tt.Fatalf(\"wrong amount of messages received for target, want %d, got %d\", 1, len(target.Messages))\n\t}\n\ttestutils.CheckTestMessage(t, &target, 0, \"postmaster\", []string{\"disappointed-user@example.com\"})\n}\n\nfunc TestMsgPipeline_CaseInsensetiveMatch_Src(t *testing.T) {\n\ttarget := testutils.Target{}\n\td := MsgPipeline{\n\t\tmsgpipelineCfg: msgpipelineCfg{\n\t\t\tperSource: map[string]sourceBlock{\n\t\t\t\t\"postmaster\": {\n\t\t\t\t\tperRcpt: map[string]*rcptBlock{},\n\t\t\t\t\tdefaultRcpt: &rcptBlock{\n\t\t\t\t\t\ttargets: []module.DeliveryTarget{&target},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t\"sender@example.com\": {\n\t\t\t\t\tperRcpt: map[string]*rcptBlock{},\n\t\t\t\t\tdefaultRcpt: &rcptBlock{\n\t\t\t\t\t\ttargets: []module.DeliveryTarget{&target},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t\"example.com\": {\n\t\t\t\t\tperRcpt: map[string]*rcptBlock{},\n\t\t\t\t\tdefaultRcpt: &rcptBlock{\n\t\t\t\t\t\ttargets: []module.DeliveryTarget{&target},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tdefaultSource: sourceBlock{\n\t\t\t\trejectErr: errors.New(\"go away\"),\n\t\t\t},\n\t\t},\n\t\tLog: testutils.Logger(t, \"msgpipeline\"),\n\t}\n\n\ttestutils.DoTestDelivery(t, &d, \"POSTMastER\", []string{\"disappointed-user@example.com\"})\n\ttestutils.DoTestDelivery(t, &d, \"SenDeR@EXAMPLE.com\", []string{\"disappointed-user@example.com\"})\n\ttestutils.DoTestDelivery(t, &d, \"sender@exAMPle.com\", []string{\"disappointed-user@example.com\"})\n\tif len(target.Messages) != 3 {\n\t\tt.Fatalf(\"wrong amount of messages received for target, want %d, got %d\", 3, len(target.Messages))\n\t}\n\ttestutils.CheckTestMessage(t, &target, 0, \"POSTMastER\", []string{\"disappointed-user@example.com\"})\n\ttestutils.CheckTestMessage(t, &target, 1, \"SenDeR@EXAMPLE.com\", []string{\"disappointed-user@example.com\"})\n\ttestutils.CheckTestMessage(t, &target, 2, \"sender@exAMPle.com\", []string{\"disappointed-user@example.com\"})\n}\n\nfunc TestMsgPipeline_CaseInsensetiveMatch_Rcpt(t *testing.T) {\n\ttarget := testutils.Target{}\n\td := MsgPipeline{\n\t\tmsgpipelineCfg: msgpipelineCfg{\n\t\t\tperSource: map[string]sourceBlock{},\n\t\t\tdefaultSource: sourceBlock{\n\t\t\t\tperRcpt: map[string]*rcptBlock{\n\t\t\t\t\t\"postmaster\": {\n\t\t\t\t\t\ttargets: []module.DeliveryTarget{&target},\n\t\t\t\t\t},\n\t\t\t\t\t\"sender@example.com\": {\n\t\t\t\t\t\ttargets: []module.DeliveryTarget{&target},\n\t\t\t\t\t},\n\t\t\t\t\t\"example.com\": {\n\t\t\t\t\t\ttargets: []module.DeliveryTarget{&target},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdefaultRcpt: &rcptBlock{\n\t\t\t\t\trejectErr: errors.New(\"wtf\"),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tLog: testutils.Logger(t, \"msgpipeline\"),\n\t}\n\n\ttestutils.DoTestDelivery(t, &d, \"sender@example.com\", []string{\"POSTMastER\"})\n\ttestutils.DoTestDelivery(t, &d, \"sender@example.com\", []string{\"SenDeR@EXAMPLE.com\"})\n\ttestutils.DoTestDelivery(t, &d, \"sender@example.com\", []string{\"sender@exAMPle.com\"})\n\tif len(target.Messages) != 3 {\n\t\tt.Fatalf(\"wrong amount of messages received for target, want %d, got %d\", 3, len(target.Messages))\n\t}\n\ttestutils.CheckTestMessage(t, &target, 0, \"sender@example.com\", []string{\"POSTMastER\"})\n\ttestutils.CheckTestMessage(t, &target, 1, \"sender@example.com\", []string{\"SenDeR@EXAMPLE.com\"})\n\ttestutils.CheckTestMessage(t, &target, 2, \"sender@example.com\", []string{\"sender@exAMPle.com\"})\n}\n\nfunc TestMsgPipeline_UnicodeNFC_Rcpt(t *testing.T) {\n\ttarget := testutils.Target{}\n\td := MsgPipeline{\n\t\tmsgpipelineCfg: msgpipelineCfg{\n\t\t\tperSource: map[string]sourceBlock{},\n\t\t\tdefaultSource: sourceBlock{\n\t\t\t\tperRcpt: map[string]*rcptBlock{\n\t\t\t\t\t\"rcpt@é.example.com\": {\n\t\t\t\t\t\ttargets: []module.DeliveryTarget{&target},\n\t\t\t\t\t},\n\t\t\t\t\t\"é.example.com\": {\n\t\t\t\t\t\ttargets: []module.DeliveryTarget{&target},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdefaultRcpt: &rcptBlock{\n\t\t\t\t\trejectErr: errors.New(\"wtf\"),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tLog: testutils.Logger(t, \"msgpipeline\"),\n\t}\n\n\ttestutils.DoTestDelivery(t, &d, \"sender@example.com\", []string{\"rcpt@E\\u0301.EXAMPLE.com\"})\n\ttestutils.DoTestDelivery(t, &d, \"sender@example.com\", []string{\"f@E\\u0301.exAMPle.com\"})\n\tif len(target.Messages) != 2 {\n\t\tt.Fatalf(\"wrong amount of messages received for target, want %d, got %d\", 2, len(target.Messages))\n\t}\n\ttestutils.CheckTestMessage(t, &target, 0, \"sender@example.com\", []string{\"rcpt@E\\u0301.EXAMPLE.com\"})\n\ttestutils.CheckTestMessage(t, &target, 1, \"sender@example.com\", []string{\"f@E\\u0301.exAMPle.com\"})\n}\n\nfunc TestMsgPipeline_MalformedSource(t *testing.T) {\n\ttarget := testutils.Target{}\n\td := MsgPipeline{\n\t\tmsgpipelineCfg: msgpipelineCfg{\n\t\t\tperSource: map[string]sourceBlock{},\n\t\t\tdefaultSource: sourceBlock{\n\t\t\t\tperRcpt: map[string]*rcptBlock{\n\t\t\t\t\t\"postmaster\": {\n\t\t\t\t\t\ttargets: []module.DeliveryTarget{&target},\n\t\t\t\t\t},\n\t\t\t\t\t\"sender@example.com\": {\n\t\t\t\t\t\ttargets: []module.DeliveryTarget{&target},\n\t\t\t\t\t},\n\t\t\t\t\t\"example.com\": {\n\t\t\t\t\t\ttargets: []module.DeliveryTarget{&target},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tLog: testutils.Logger(t, \"msgpipeline\"),\n\t}\n\n\t// Simple checks for violations that can make msgpipeline misbehave.\n\tfor _, addr := range []string{\"not_postmaster_but_no_at_sign\", \"@no_mailbox\", \"no_domain@\"} {\n\t\t_, err := d.StartDelivery(context.Background(), &module.MsgMetadata{ID: \"testing\"}, addr)\n\t\tif err == nil {\n\t\t\tt.Errorf(\"%s is accepted as valid address\", addr)\n\t\t}\n\t}\n}\n\nfunc TestMsgPipeline_TwoRcptToOneTarget(t *testing.T) {\n\ttarget := testutils.Target{}\n\td := MsgPipeline{\n\t\tmsgpipelineCfg: msgpipelineCfg{\n\t\t\tperSource: map[string]sourceBlock{},\n\t\t\tdefaultSource: sourceBlock{\n\t\t\t\tperRcpt: map[string]*rcptBlock{\n\t\t\t\t\t\"example.com\": {\n\t\t\t\t\t\ttargets: []module.DeliveryTarget{&target},\n\t\t\t\t\t},\n\t\t\t\t\t\"example.org\": {\n\t\t\t\t\t\ttargets: []module.DeliveryTarget{&target},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tLog: testutils.Logger(t, \"msgpipeline\"),\n\t}\n\n\ttestutils.DoTestDelivery(t, &d, \"sender@example.com\", []string{\"recipient@example.com\", \"recipient@example.org\"})\n\n\tif len(target.Messages) != 1 {\n\t\tt.Fatalf(\"wrong amount of messages received for target, want %d, got %d\", 1, len(target.Messages))\n\t}\n\ttestutils.CheckTestMessage(t, &target, 0, \"sender@example.com\", []string{\"recipient@example.com\", \"recipient@example.org\"})\n}\n\nfunc TestMsgPipeline_multi_alias(t *testing.T) {\n\ttarget1, target2 := testutils.Target{InstName: \"target1\"}, testutils.Target{InstName: \"target2\"}\n\tmod := testutils.Modifier{\n\t\tRcptTo: map[string][]string{\n\t\t\t\"recipient@example.com\": []string{\n\t\t\t\t\"recipient-1@example.org\",\n\t\t\t\t\"recipient-2@example.net\",\n\t\t\t},\n\t\t},\n\t}\n\td := MsgPipeline{\n\t\tmsgpipelineCfg: msgpipelineCfg{\n\t\t\tperSource: map[string]sourceBlock{},\n\t\t\tdefaultSource: sourceBlock{\n\t\t\t\tmodifiers: modify.Group{\n\t\t\t\t\tModifiers: []module.Modifier{mod},\n\t\t\t\t},\n\t\t\t\tperRcpt: map[string]*rcptBlock{\n\t\t\t\t\t\"example.org\": {\n\t\t\t\t\t\ttargets: []module.DeliveryTarget{&target1},\n\t\t\t\t\t},\n\t\t\t\t\t\"example.net\": {\n\t\t\t\t\t\ttargets: []module.DeliveryTarget{&target2},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tLog: testutils.Logger(t, \"msgpipeline\"),\n\t}\n\n\ttestutils.DoTestDelivery(t, &d, \"sender@example.com\", []string{\"recipient@example.com\"})\n\n\tif len(target1.Messages) != 1 {\n\t\tt.Errorf(\"wrong amount of messages received for target1, want %d, got %d\", 1, len(target1.Messages))\n\t}\n\ttestutils.CheckTestMessage(t, &target1, 0, \"sender@example.com\", []string{\"recipient-1@example.org\"})\n\n\tif len(target2.Messages) != 1 {\n\t\tt.Errorf(\"wrong amount of messages received for target1, want %d, got %d\", 1, len(target2.Messages))\n\t}\n\ttestutils.CheckTestMessage(t, &target2, 0, \"sender@example.com\", []string{\"recipient-2@example.net\"})\n}\n"
  },
  {
    "path": "internal/msgpipeline/objname.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage msgpipeline\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/foxcpp/maddy/framework/module\"\n)\n\n// objectName returns a new that is usable to identify the used external\n// component (module or some stub) in debug logs.\nfunc objectName(x interface{}) string {\n\tmod, ok := x.(module.Module)\n\tif ok {\n\t\treturn mod.Name() + \":\" + mod.InstanceName()\n\t}\n\n\t_, pipeline := x.(*MsgPipeline)\n\tif pipeline {\n\t\treturn \"reroute\"\n\t}\n\n\tstr, ok := x.(fmt.Stringer)\n\tif ok {\n\t\treturn str.String()\n\t}\n\n\treturn fmt.Sprintf(\"%T\", x)\n}\n"
  },
  {
    "path": "internal/msgpipeline/regress_test.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage msgpipeline\n\nimport (\n\t\"testing\"\n\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/internal/testutils\"\n)\n\nfunc TestMsgPipeline_Issue161(t *testing.T) {\n\ttarget := testutils.Target{}\n\tcheck1, check2 := testutils.Check{}, testutils.Check{}\n\td := MsgPipeline{\n\t\tmsgpipelineCfg: msgpipelineCfg{\n\t\t\tglobalChecks: []module.Check{&check1},\n\t\t\tperSource:    map[string]sourceBlock{},\n\t\t\tdefaultSource: sourceBlock{\n\t\t\t\tchecks:  []module.Check{&check2},\n\t\t\t\tperRcpt: map[string]*rcptBlock{},\n\t\t\t\tdefaultRcpt: &rcptBlock{\n\t\t\t\t\ttargets: []module.DeliveryTarget{&target},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tLog: testutils.Logger(t, \"msgpipeline\"),\n\t}\n\n\ttestutils.DoTestDelivery(t, &d, \"whatever@whatever\", []string{\"whatever@whatever\"})\n\n\tif check2.ConnCalls != 1 {\n\t\tt.Errorf(\"CheckConnection called %d times\", check2.ConnCalls)\n\t}\n\tif check2.SenderCalls != 1 {\n\t\tt.Errorf(\"CheckSender called %d times\", check2.SenderCalls)\n\t}\n\tif check2.RcptCalls != 1 {\n\t\tt.Errorf(\"CheckRcpt called %d times\", check2.RcptCalls)\n\t}\n\tif check2.BodyCalls != 1 {\n\t\tt.Errorf(\"CheckBody called %d times\", check2.BodyCalls)\n\t}\n\n\tif check1.UnclosedStates != 0 || check2.UnclosedStates != 0 {\n\t\tt.Fatalf(\"checks state objects leak or double-closed, alive counters: %v, %v\", check1.UnclosedStates, check2.UnclosedStates)\n\t}\n}\n\nfunc TestMsgPipeline_Issue161_2(t *testing.T) {\n\ttarget := testutils.Target{}\n\tcheck1, check2 := testutils.Check{}, testutils.Check{InstName: \"check2\"}\n\td := MsgPipeline{\n\t\tmsgpipelineCfg: msgpipelineCfg{\n\t\t\tglobalChecks: []module.Check{&check1},\n\t\t\tperSource:    map[string]sourceBlock{},\n\t\t\tdefaultSource: sourceBlock{\n\t\t\t\tchecks:  []module.Check{&check1},\n\t\t\t\tperRcpt: map[string]*rcptBlock{},\n\t\t\t\tdefaultRcpt: &rcptBlock{\n\t\t\t\t\tchecks:  []module.Check{&check2},\n\t\t\t\t\ttargets: []module.DeliveryTarget{&target},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tLog: testutils.Logger(t, \"msgpipeline\"),\n\t}\n\n\ttestutils.DoTestDelivery(t, &d, \"whatever@whatever\", []string{\"whatever@whatever\"})\n\n\tif check2.ConnCalls != 1 {\n\t\tt.Errorf(\"CheckConnection called %d times\", check2.ConnCalls)\n\t}\n\tif check2.SenderCalls != 1 {\n\t\tt.Errorf(\"CheckSender called %d times\", check2.SenderCalls)\n\t}\n\tif check2.RcptCalls != 1 {\n\t\tt.Errorf(\"CheckRcpt called %d times\", check2.RcptCalls)\n\t}\n\n\tif check1.UnclosedStates != 0 || check2.UnclosedStates != 0 {\n\t\tt.Fatalf(\"checks state objects leak or double-closed, alive counters: %v, %v\", check1.UnclosedStates, check2.UnclosedStates)\n\t}\n}\n\nfunc TestMsgPipeline_Issue161_3(t *testing.T) {\n\ttarget := testutils.Target{}\n\tcheck1, check2 := testutils.Check{}, testutils.Check{}\n\td := MsgPipeline{\n\t\tmsgpipelineCfg: msgpipelineCfg{\n\t\t\tglobalChecks: []module.Check{&check1, &check2},\n\t\t\tperSource:    map[string]sourceBlock{},\n\t\t\tdefaultSource: sourceBlock{\n\t\t\t\tperRcpt: map[string]*rcptBlock{},\n\t\t\t\tdefaultRcpt: &rcptBlock{\n\t\t\t\t\ttargets: []module.DeliveryTarget{&target},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tLog: testutils.Logger(t, \"msgpipeline\"),\n\t}\n\n\ttestutils.DoTestDelivery(t, &d, \"whatever@whatever\", []string{\"whatever@whatever\"})\n\n\tif check2.ConnCalls != 1 {\n\t\tt.Errorf(\"CheckConnection called %d times\", check2.ConnCalls)\n\t}\n\tif check2.SenderCalls != 1 {\n\t\tt.Errorf(\"CheckSender called %d times\", check2.SenderCalls)\n\t}\n\tif check2.RcptCalls != 1 {\n\t\tt.Errorf(\"CheckRcpt called %d times\", check2.RcptCalls)\n\t}\n\tif check2.BodyCalls != 1 {\n\t\tt.Errorf(\"CheckBody called %d times\", check2.BodyCalls)\n\t}\n\n\tif check1.UnclosedStates != 0 || check2.UnclosedStates != 0 {\n\t\tt.Fatalf(\"checks state objects leak or double-closed, alive counters: %v, %v\", check1.UnclosedStates, check2.UnclosedStates)\n\t}\n}\n"
  },
  {
    "path": "internal/proxy_protocol/proxy_protocol.go",
    "content": "package proxy_protocol\n\nimport (\n\t\"crypto/tls\"\n\t\"net\"\n\t\"strings\"\n\n\t\"github.com/c0va23/go-proxyprotocol\"\n\t\"github.com/foxcpp/maddy/framework/config\"\n\ttls2 \"github.com/foxcpp/maddy/framework/config/tls\"\n\t\"github.com/foxcpp/maddy/framework/log\"\n)\n\ntype ProxyProtocol struct {\n\ttrust     []net.IPNet\n\ttlsConfig *tls.Config\n}\n\nfunc ProxyProtocolDirective(_ *config.Map, node config.Node) (interface{}, error) {\n\tp := ProxyProtocol{}\n\n\tchildM := config.NewMap(nil, node)\n\tvar trustList []string\n\n\tchildM.StringList(\"trust\", false, false, nil, &trustList)\n\tchildM.Custom(\"tls\", true, false, nil, tls2.TLSDirective, &p.tlsConfig)\n\n\tif _, err := childM.Process(); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif len(node.Args) > 0 {\n\t\tif trustList == nil {\n\t\t\ttrustList = make([]string, 0)\n\t\t}\n\t\ttrustList = append(trustList, node.Args...)\n\t}\n\n\tfor _, trust := range trustList {\n\t\tif !strings.Contains(trust, \"/\") {\n\t\t\ttrust += \"/32\"\n\t\t}\n\t\t_, ipNet, err := net.ParseCIDR(trust)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tp.trust = append(p.trust, *ipNet)\n\t}\n\n\treturn &p, nil\n}\n\nfunc NewListener(inner net.Listener, p *ProxyProtocol, logger *log.Logger) net.Listener {\n\tvar listener net.Listener\n\n\tsourceChecker := func(upstream net.Addr) (bool, error) {\n\t\tif tcpAddr, ok := upstream.(*net.TCPAddr); ok {\n\t\t\tif len(p.trust) == 0 {\n\t\t\t\treturn true, nil\n\t\t\t}\n\t\t\tfor _, trusted := range p.trust {\n\t\t\t\tif trusted.Contains(tcpAddr.IP) {\n\t\t\t\t\treturn true, nil\n\t\t\t\t}\n\t\t\t}\n\t\t} else if _, ok := upstream.(*net.UnixAddr); ok {\n\t\t\t// UNIX local socket connection, always trusted\n\t\t\treturn true, nil\n\t\t}\n\n\t\tlogger.Printf(\"connection from untrusted source %s\", upstream)\n\t\treturn false, nil\n\t}\n\n\tproxyListener := proxyprotocol.NewDefaultListener(inner).\n\t\tWithLogger(proxyprotocol.LoggerFunc(logger.Debugf)).\n\t\tWithSourceChecker(sourceChecker)\n\tlistener = &proxyListener\n\n\tif p.tlsConfig != nil {\n\t\tlistener = tls.NewListener(listener, p.tlsConfig)\n\t}\n\n\treturn listener\n}\n"
  },
  {
    "path": "internal/smtpconn/pool/pool.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage pool\n\nimport (\n\t\"context\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/foxcpp/maddy/framework/log\"\n)\n\ntype Conn interface {\n\tUsable() bool\n\tLastUseAt() time.Time\n\tClose() error\n}\n\ntype Config struct {\n\tNew                 func(ctx context.Context, key string) (Conn, error)\n\tMaxKeys             int\n\tMaxConnsPerKey      int\n\tMaxConnLifetimeSec  int64\n\tStaleKeyLifetimeSec int64\n}\n\ntype slot struct {\n\tc chan Conn\n\t// To keep slot size smaller it is just a unix timestamp.\n\tlastUse int64\n}\n\ntype P struct {\n\tcfg      Config\n\tkeys     map[string]slot\n\tkeysLock sync.Mutex\n\n\tcleanupStop chan struct{}\n}\n\nfunc New(cfg Config) *P {\n\tif cfg.New == nil {\n\t\tcfg.New = func(context.Context, string) (Conn, error) {\n\t\t\treturn nil, nil\n\t\t}\n\t}\n\n\tp := &P{\n\t\tcfg:         cfg,\n\t\tkeys:        make(map[string]slot, cfg.MaxKeys),\n\t\tcleanupStop: make(chan struct{}),\n\t}\n\n\tgo p.cleanUpTick(p.cleanupStop)\n\n\treturn p\n}\n\nfunc (p *P) cleanUpTick(stop chan struct{}) {\n\tctx := context.Background()\n\ttick := time.NewTicker(time.Minute)\n\tdefer tick.Stop()\n\n\tfor {\n\t\tselect {\n\t\tcase <-tick.C:\n\t\t\tp.CleanUp(ctx)\n\t\tcase <-stop:\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc (p *P) CleanUp(ctx context.Context) {\n\tp.keysLock.Lock()\n\tdefer p.keysLock.Unlock()\n\n\tfor k, v := range p.keys {\n\t\tif v.lastUse+p.cfg.StaleKeyLifetimeSec > time.Now().Unix() {\n\t\t\tcontinue\n\t\t}\n\n\t\tclose(v.c)\n\t\tfor conn := range v.c {\n\t\t\tgo p.close(conn)\n\t\t}\n\t\tdelete(p.keys, k)\n\t}\n}\n\nfunc (p *P) close(c Conn) {\n\tif err := c.Close(); err != nil {\n\t\tlog.DefaultLogger.Error(\"failed to close pooled connection\", err)\n\t}\n}\n\nfunc (p *P) Get(ctx context.Context, key string) (Conn, error) {\n\tp.keysLock.Lock()\n\n\tbucket, ok := p.keys[key]\n\tif !ok {\n\t\tp.keysLock.Unlock()\n\t\treturn p.cfg.New(ctx, key)\n\t}\n\n\tif time.Now().Unix()-bucket.lastUse > p.cfg.MaxConnLifetimeSec {\n\t\t// Drop bucket.\n\t\tdelete(p.keys, key)\n\t\tclose(bucket.c)\n\n\t\t// Close might take some time, unlock early.\n\t\tp.keysLock.Unlock()\n\n\t\tfor conn := range bucket.c {\n\t\t\tp.close(conn)\n\t\t}\n\n\t\treturn p.cfg.New(ctx, key)\n\t}\n\n\tp.keysLock.Unlock()\n\n\tfor {\n\t\tvar conn Conn\n\t\tselect {\n\t\tcase conn, ok = <-bucket.c:\n\t\t\tif !ok {\n\t\t\t\treturn p.cfg.New(ctx, key)\n\t\t\t}\n\t\tdefault:\n\t\t\treturn p.cfg.New(ctx, key)\n\t\t}\n\n\t\tif !conn.Usable() {\n\t\t\t// Close might take some time, run in parallel.\n\t\t\tgo p.close(conn)\n\t\t\tcontinue\n\t\t}\n\t\tif conn.LastUseAt().Add(time.Duration(p.cfg.MaxConnLifetimeSec) * time.Second).Before(time.Now()) {\n\t\t\tgo func() {\n\t\t\t\tif err := conn.Close(); err != nil {\n\t\t\t\t\tlog.DefaultLogger.Error(\"failed to close pooled connection\", err)\n\t\t\t\t}\n\t\t\t}()\n\t\t\tcontinue\n\t\t}\n\n\t\treturn conn, nil\n\t}\n}\n\nfunc (p *P) Return(key string, c Conn) {\n\tp.keysLock.Lock()\n\tdefer p.keysLock.Unlock()\n\n\tif p.keys == nil {\n\t\treturn\n\t}\n\n\tbucket, ok := p.keys[key]\n\tif !ok {\n\t\t// Garbage-collect stale buckets.\n\t\tif len(p.keys) == p.cfg.MaxKeys {\n\t\t\tfor k, v := range p.keys {\n\t\t\t\tif v.lastUse+p.cfg.StaleKeyLifetimeSec > time.Now().Unix() {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tdelete(p.keys, k)\n\t\t\t\tclose(v.c)\n\n\t\t\t\tfor conn := range v.c {\n\t\t\t\t\tp.close(conn)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tbucket = slot{\n\t\t\tc:       make(chan Conn, p.cfg.MaxConnsPerKey),\n\t\t\tlastUse: time.Now().Unix(),\n\t\t}\n\t\tp.keys[key] = bucket\n\t}\n\n\tselect {\n\tcase bucket.c <- c:\n\t\tbucket.lastUse = time.Now().Unix()\n\tdefault:\n\t\t// Let it go, let it go...\n\t\tgo p.close(c)\n\t}\n}\n\nfunc (p *P) Close() {\n\tp.cleanupStop <- struct{}{}\n\n\tp.keysLock.Lock()\n\tdefer p.keysLock.Unlock()\n\n\tfor k, v := range p.keys {\n\t\tclose(v.c)\n\t\tfor conn := range v.c {\n\t\t\tp.close(conn)\n\t\t}\n\t\tdelete(p.keys, k)\n\t}\n\tp.keys = nil\n}\n"
  },
  {
    "path": "internal/smtpconn/smtpconn.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\n// Package smtpconn contains the code shared between target.smtp and\n// remote modules.\n//\n// It implements the wrapper over the SMTP connection (go-smtp.Client) object\n// with the following features added:\n// - Logging of certain errors (e.g. QUIT command errors)\n// - Wrapping of returned errors using the exterrors package.\n// - SMTPUTF8/IDNA support.\n// - TLS support mode (don't use, attempt, require).\npackage smtpconn\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"runtime/trace\"\n\t\"time\"\n\n\t\"github.com/emersion/go-message/textproto\"\n\t\"github.com/emersion/go-smtp\"\n\t\"github.com/foxcpp/maddy/framework/address\"\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/exterrors\"\n\t\"github.com/foxcpp/maddy/framework/log\"\n)\n\n// The C object represents the SMTP connection and is a wrapper around\n// go-smtp.Client with additional maddy-specific logic.\n//\n// Currently, the C object represents one session and cannot be reused.\ntype C struct {\n\t// Dialer to use to estabilish new network connections. Set to net.Dialer\n\t// DialContext by New.\n\tDialer func(ctx context.Context, network, addr string) (net.Conn, error)\n\n\t// Timeout for most session commands (EHLO, MAIL, RCPT, DATA, STARTTLS).\n\t// Set to 5 mins by New.\n\tCommandTimeout time.Duration\n\n\t// Timeout for the initial TCP connection establishment.\n\tConnectTimeout time.Duration\n\n\t// Timeout for the final dot. Set to 12 mins by New.\n\t// (see go-smtp source for explanation of used defaults).\n\tSubmissionTimeout time.Duration\n\n\t// Hostname to sent in the EHLO/HELO command. Set to\n\t// 'localhost.localdomain' by New. Expected to be encoded in ACE form.\n\tHostname string\n\n\t// tls.Config to use. Can be nil if no special changes are required.\n\tTLSConfig *tls.Config\n\n\t// Logger to use for debug log and certain errors.\n\tLog *log.Logger\n\n\t// Include the remote server address in SMTP status messages in the form\n\t// \"ADDRESS said: ...\"\n\tAddrInSMTPMsg bool\n\n\tconn       net.Conn\n\tserverName string\n\tcl         *smtp.Client\n\trcpts      []string\n\tlmtp       bool\n}\n\n// New creates the new instance of the C object, populating the required fields\n// with resonable default values.\nfunc New() *C {\n\treturn &C{\n\t\tDialer:            (&net.Dialer{}).DialContext,\n\t\tConnectTimeout:    5 * time.Minute,\n\t\tCommandTimeout:    5 * time.Minute,\n\t\tSubmissionTimeout: 12 * time.Minute,\n\t\tTLSConfig:         &tls.Config{},\n\t\tHostname:          \"localhost.localdomain\",\n\t}\n}\n\nfunc (c *C) wrapClientErr(err error, serverName string) error {\n\tif err == nil {\n\t\treturn nil\n\t}\n\n\tswitch err := err.(type) {\n\tcase TLSError:\n\t\treturn err\n\tcase *exterrors.SMTPError:\n\t\treturn err\n\tcase *smtp.SMTPError:\n\t\tmsg := err.Message\n\t\tif c.AddrInSMTPMsg {\n\t\t\tmsg = serverName + \" said: \" + err.Message\n\t\t}\n\n\t\tif err.Code == 552 {\n\t\t\terr.Code = 452\n\t\t\terr.EnhancedCode[0] = 4\n\t\t\tc.Log.Msg(\"SMTP code 552 rewritten to 452 per RFC 5321 Section 4.5.3.1.10\")\n\t\t}\n\n\t\treturn &exterrors.SMTPError{\n\t\t\tCode:         err.Code,\n\t\t\tEnhancedCode: exterrors.EnhancedCode(err.EnhancedCode),\n\t\t\tMessage:      msg,\n\t\t\tMisc: map[string]interface{}{\n\t\t\t\t\"remote_server\": serverName,\n\t\t\t},\n\t\t\tErr: err,\n\t\t}\n\tcase *net.OpError:\n\t\tif _, ok := err.Err.(*net.DNSError); ok {\n\t\t\treason, misc := exterrors.UnwrapDNSErr(err)\n\t\t\tmisc[\"remote_server\"] = err.Addr\n\t\t\tmisc[\"io_op\"] = err.Op\n\t\t\treturn &exterrors.SMTPError{\n\t\t\t\tCode:         exterrors.SMTPCode(err, 450, 550),\n\t\t\t\tEnhancedCode: exterrors.SMTPEnchCode(err, exterrors.EnhancedCode{0, 4, 4}),\n\t\t\t\tMessage:      \"DNS error\",\n\t\t\t\tErr:          err,\n\t\t\t\tReason:       reason,\n\t\t\t\tMisc:         misc,\n\t\t\t}\n\t\t}\n\t\treturn &exterrors.SMTPError{\n\t\t\tCode:         450,\n\t\t\tEnhancedCode: exterrors.EnhancedCode{4, 4, 2},\n\t\t\tMessage:      \"Network I/O error\",\n\t\t\tErr:          err,\n\t\t\tMisc: map[string]interface{}{\n\t\t\t\t\"remote_addr\": err.Addr,\n\t\t\t\t\"io_op\":       err.Op,\n\t\t\t},\n\t\t}\n\tdefault:\n\t\treturn exterrors.WithFields(err, map[string]interface{}{\n\t\t\t\"remote_server\": serverName,\n\t\t})\n\t}\n}\n\n// Connect actually estabilishes the network connection with the remote host,\n// executes HELO/EHLO and optionally STARTTLS command.\nfunc (c *C) Connect(ctx context.Context, endp config.Endpoint, starttls bool, tlsConfig *tls.Config) (didTLS bool, err error) {\n\tdidTLS, cl, conn, err := c.attemptConnect(ctx, false, endp, starttls, tlsConfig)\n\tif err != nil {\n\t\treturn false, c.wrapClientErr(err, endp.Host)\n\t}\n\n\tc.serverName = endp.Host\n\tc.cl = cl\n\tc.conn = conn\n\n\tc.Log.DebugMsg(\"connected\", \"remote_server\", c.serverName,\n\t\t\"local_addr\", c.LocalAddr(), \"remote_addr\", c.RemoteAddr())\n\n\treturn didTLS, nil\n}\n\n// ConnectLMTP estabilishes the network connection with the remote host and\n// sends LHLO command, negotiating LMTP use.\nfunc (c *C) ConnectLMTP(ctx context.Context, endp config.Endpoint, starttls bool, tlsConfig *tls.Config) (didTLS bool, err error) {\n\tdidTLS, cl, conn, err := c.attemptConnect(ctx, true, endp, starttls, tlsConfig)\n\tif err != nil {\n\t\treturn false, c.wrapClientErr(err, endp.Host)\n\t}\n\n\tc.serverName = endp.Host\n\tc.cl = cl\n\tc.conn = conn\n\n\tc.Log.DebugMsg(\"connected\", \"remote_server\", c.serverName,\n\t\t\"local_addr\", c.LocalAddr(), \"remote_addr\", c.RemoteAddr())\n\n\treturn didTLS, nil\n}\n\n// TLSError is returned by Connect to indicate the error during STARTTLS\n// command execution.\n//\n// If the endpoint uses Implicit TLS, TLS errors are threated as connection\n// errors and thus are not returned as TLSError.\ntype TLSError struct {\n\tErr error\n}\n\nfunc (err TLSError) Error() string {\n\treturn \"smtpconn: \" + err.Err.Error()\n}\n\nfunc (err TLSError) Unwrap() error {\n\treturn err.Err\n}\n\nfunc (c *C) LocalAddr() net.Addr {\n\tif c.conn == nil {\n\t\treturn nil\n\t}\n\treturn c.conn.LocalAddr()\n}\n\nfunc (c *C) RemoteAddr() net.Addr {\n\tif c.conn == nil {\n\t\treturn nil\n\t}\n\treturn c.conn.RemoteAddr()\n}\n\nfunc (c *C) closeClient(cl *smtp.Client) {\n\tif err := cl.Close(); err != nil {\n\t\tc.Log.Error(\"client connection close failed\", err)\n\t}\n}\n\nfunc (c *C) attemptConnect(ctx context.Context, lmtp bool, endp config.Endpoint, starttls bool, tlsConfig *tls.Config) (didTLS bool, cl *smtp.Client, conn net.Conn, err error) {\n\tdialCtx, cancel := context.WithTimeout(ctx, c.ConnectTimeout)\n\tconn, err = c.Dialer(dialCtx, endp.Network(), endp.Address())\n\tcancel()\n\tif err != nil {\n\t\treturn false, nil, nil, err\n\t}\n\n\tif endp.IsTLS() {\n\t\tcfg := tlsConfig.Clone()\n\t\tcfg.ServerName = endp.Host\n\t\tconn = tls.Client(conn, cfg)\n\t}\n\n\tc.lmtp = lmtp\n\t// This uses initial greeting timeout of 5 minutes (hardcoded).\n\tif lmtp {\n\t\tcl = smtp.NewClientLMTP(conn)\n\t} else {\n\t\tcl = smtp.NewClient(conn)\n\t}\n\n\tcl.CommandTimeout = c.CommandTimeout\n\tcl.SubmissionTimeout = c.SubmissionTimeout\n\n\t// i18n: hostname is already expected to be in A-labels form.\n\tif err := cl.Hello(c.Hostname); err != nil {\n\t\tc.closeClient(cl)\n\t\treturn false, nil, nil, err\n\t}\n\n\tif !starttls {\n\t\treturn false, cl, conn, nil\n\t}\n\n\tif ok, _ := cl.Extension(\"STARTTLS\"); !ok {\n\t\tif err := cl.Quit(); err != nil {\n\t\t\tc.closeClient(cl)\n\t\t}\n\t\treturn false, nil, nil, fmt.Errorf(\"TLS required but unsupported by downstream\")\n\t}\n\n\tcfg := tlsConfig.Clone()\n\tcfg.ServerName = endp.Host\n\tif err := cl.StartTLS(cfg); err != nil {\n\t\t// After the handshake failure, the connection may be in a bad state.\n\t\t// We attempt to send the proper QUIT command though, in case the error happened\n\t\t// *after* the handshake (e.g. PKI verification fail), we don't log the error in\n\t\t// this case though.\n\t\tif err := cl.Quit(); err != nil {\n\t\t\tc.closeClient(cl)\n\t\t}\n\n\t\treturn false, nil, nil, TLSError{err}\n\t}\n\n\t// Re-do HELO using our hostname instead of localhost.\n\tif err := cl.Hello(c.Hostname); err != nil {\n\t\tc.closeClient(cl)\n\n\t\tvar tlsErr *tls.CertificateVerificationError\n\t\tif errors.As(err, &tlsErr) {\n\t\t\treturn false, nil, nil, TLSError{Err: tlsErr}\n\t\t}\n\n\t\treturn false, nil, nil, err\n\t}\n\n\treturn true, cl, conn, nil\n}\n\n// Mail sends the MAIL FROM command to the remote server.\n//\n// SIZE and REQUIRETLS options are forwarded to the remote server as-is.\n// SMTPUTF8 is forwarded if supported by the remote server, if it is not\n// supported - attempt will be done to convert addresses to the ASCII form, if\n// this is not possible, the corresponding method (Mail or Rcpt) will fail.\nfunc (c *C) Mail(ctx context.Context, from string, opts smtp.MailOptions) error {\n\tdefer trace.StartRegion(ctx, \"smtpconn/MAIL FROM\").End()\n\n\toutOpts := smtp.MailOptions{\n\t\t// Future extensions may add additional fields that should not be\n\t\t// copied blindly. So we copy only fields we know should be handled\n\t\t// this way.\n\n\t\tSize:       opts.Size,\n\t\tRequireTLS: opts.RequireTLS,\n\t}\n\n\t// INTERNATIONALIZATION: Use SMTPUTF8 is possible, attempt to convert addresses otherwise.\n\n\t// There is no way we can accept a message with non-ASCII addresses without SMTPUTF8\n\t// this is enforced by endpoint/smtp.\n\tif opts.UTF8 {\n\t\tif ok, _ := c.cl.Extension(\"SMTPUTF8\"); ok {\n\t\t\toutOpts.UTF8 = true\n\t\t} else {\n\t\t\tvar err error\n\t\t\tfrom, err = address.ToASCII(from)\n\t\t\tif err != nil {\n\t\t\t\treturn &exterrors.SMTPError{\n\t\t\t\t\tCode:         550,\n\t\t\t\t\tEnhancedCode: exterrors.EnhancedCode{5, 6, 7},\n\t\t\t\t\tMessage:      \"SMTPUTF8 is unsupported, cannot convert sender address\",\n\t\t\t\t\tMisc: map[string]interface{}{\n\t\t\t\t\t\t\"remote_server\": c.serverName,\n\t\t\t\t\t},\n\t\t\t\t\tErr: err,\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif err := c.cl.Mail(from, &outOpts); err != nil {\n\t\treturn c.wrapClientErr(err, c.serverName)\n\t}\n\n\treturn nil\n}\n\n// Rcpts returns the list of recipients that were accepted by the remote server.\nfunc (c *C) Rcpts() []string {\n\treturn c.rcpts\n}\n\nfunc (c *C) ServerName() string {\n\treturn c.serverName\n}\n\nfunc (c *C) Client() *smtp.Client {\n\treturn c.cl\n}\n\nfunc (c *C) IsLMTP() bool {\n\treturn c.lmtp\n}\n\n// Rcpt sends the RCPT TO command to the remote server.\n//\n// If the address is non-ASCII and cannot be converted to ASCII and the remote\n// server does not support SMTPUTF8, error will be returned.\nfunc (c *C) Rcpt(ctx context.Context, to string, opts smtp.RcptOptions) error {\n\tdefer trace.StartRegion(ctx, \"smtpconn/RCPT TO\").End()\n\n\toutOpts := &smtp.RcptOptions{\n\t\t// TODO: DSN support\n\t}\n\n\t// If necessary, the extension flag is enabled in StartDelivery.\n\tif ok, _ := c.cl.Extension(\"SMTPUTF8\"); !address.IsASCII(to) && !ok {\n\t\tvar err error\n\t\tto, err = address.ToASCII(to)\n\t\tif err != nil {\n\t\t\treturn &exterrors.SMTPError{\n\t\t\t\tCode:         553,\n\t\t\t\tEnhancedCode: exterrors.EnhancedCode{5, 6, 7},\n\t\t\t\tMessage:      \"SMTPUTF8 is unsupported, cannot convert recipient address\",\n\t\t\t\tMisc: map[string]interface{}{\n\t\t\t\t\t\"remote_server\": c.serverName,\n\t\t\t\t},\n\t\t\t\tErr: err,\n\t\t\t}\n\t\t}\n\t}\n\n\tif err := c.cl.Rcpt(to, outOpts); err != nil {\n\t\treturn c.wrapClientErr(err, c.serverName)\n\t}\n\n\tc.rcpts = append(c.rcpts, to)\n\n\treturn nil\n}\n\ntype lmtpError map[string]*smtp.SMTPError\n\nfunc (l lmtpError) SetStatus(rcptTo string, err *smtp.SMTPError) {\n\tl[rcptTo] = err\n}\n\nfunc (l lmtpError) singleError() *smtp.SMTPError {\n\tnonNils := 0\n\tfor _, e := range l {\n\t\tif e != nil {\n\t\t\tnonNils++\n\t\t}\n\t}\n\tif nonNils == 1 {\n\t\tfor _, err := range l {\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (l lmtpError) Unwrap() error {\n\tif err := l.singleError(); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (l lmtpError) Error() string {\n\tif err := l.singleError(); err != nil {\n\t\treturn err.Error()\n\t}\n\treturn fmt.Sprintf(\"multiple errors reported by LMTP downstream: %v\", map[string]*smtp.SMTPError(l))\n}\n\nfunc (c *C) smtpToLMTPData(ctx context.Context, hdr textproto.Header, body io.Reader) error {\n\tstatusCb := lmtpError{}\n\tif err := c.LMTPData(ctx, hdr, body, statusCb.SetStatus); err != nil {\n\t\treturn err\n\t}\n\thasAnyFailures := false\n\tfor _, err := range statusCb {\n\t\tif err != nil {\n\t\t\thasAnyFailures = true\n\t\t}\n\t}\n\tif hasAnyFailures {\n\t\treturn statusCb\n\t}\n\treturn nil\n}\n\n// Data sends the DATA command to the remote server and then sends the message header\n// and body.\n//\n// If the Data command fails, the connection may be in a unclean state (e.g. in\n// the middle of message data stream). It is not safe to continue using it.\nfunc (c *C) Data(ctx context.Context, hdr textproto.Header, body io.Reader) error {\n\tdefer trace.StartRegion(ctx, \"smtpconn/DATA\").End()\n\n\tif c.IsLMTP() {\n\t\treturn c.smtpToLMTPData(ctx, hdr, body)\n\t}\n\n\twc, err := c.cl.Data()\n\tif err != nil {\n\t\treturn c.wrapClientErr(err, c.serverName)\n\t}\n\n\tif err := textproto.WriteHeader(wc, hdr); err != nil {\n\t\treturn c.wrapClientErr(err, c.serverName)\n\t}\n\n\tif _, err := io.Copy(wc, body); err != nil {\n\t\treturn c.wrapClientErr(err, c.serverName)\n\t}\n\n\tif err := wc.Close(); err != nil {\n\t\treturn c.wrapClientErr(err, c.serverName)\n\t}\n\n\treturn nil\n}\n\nfunc (c *C) LMTPData(ctx context.Context, hdr textproto.Header, body io.Reader, statusCb func(string, *smtp.SMTPError)) error {\n\tdefer trace.StartRegion(ctx, \"smtpconn/LMTPDATA\").End()\n\n\twc, err := c.cl.LMTPData(statusCb)\n\tif err != nil {\n\t\treturn c.wrapClientErr(err, c.serverName)\n\t}\n\n\tif err := textproto.WriteHeader(wc, hdr); err != nil {\n\t\treturn c.wrapClientErr(err, c.serverName)\n\t}\n\n\tif _, err := io.Copy(wc, body); err != nil {\n\t\treturn c.wrapClientErr(err, c.serverName)\n\t}\n\n\tif err := wc.Close(); err != nil {\n\t\treturn c.wrapClientErr(err, c.serverName)\n\t}\n\n\treturn nil\n}\n\nfunc (c *C) Noop() error {\n\tif c.cl == nil {\n\t\treturn errors.New(\"smtpconn: not connected\")\n\t}\n\n\treturn c.cl.Noop()\n}\n\n// Close sends the QUIT command, if it fails - it directly closes the\n// connection.\nfunc (c *C) Close() error {\n\tc.cl.CommandTimeout = 5 * time.Second\n\n\tif err := c.cl.Quit(); err != nil {\n\t\tvar smtpErr *smtp.SMTPError\n\t\tvar netErr *net.OpError\n\t\tif errors.As(err, &smtpErr) && smtpErr.Code == 421 {\n\t\t\t// 421 \"Service not available\" is typically sent\n\t\t\t// when idle timeout happens.\n\t\t\tc.Log.DebugMsg(\"QUIT error\", \"reason\", c.wrapClientErr(err, c.serverName))\n\t\t} else if errors.As(err, &netErr) &&\n\t\t\t(netErr.Timeout() || netErr.Err.Error() == \"write: broken pipe\" || netErr.Err.Error() == \"read: connection reset\") {\n\t\t\t// The case for silently closed connections.\n\t\t\tc.Log.DebugMsg(\"QUIT error\", \"reason\", c.wrapClientErr(err, c.serverName))\n\t\t} else {\n\t\t\tc.Log.Error(\"QUIT error\", c.wrapClientErr(err, c.serverName))\n\t\t}\n\n\t\treturn c.cl.Close()\n\t}\n\n\tc.cl = nil\n\tc.serverName = \"\"\n\n\treturn nil\n}\n\n// DirectClose closes the underlying connection without sending the QUIT\n// command.\nfunc (c *C) DirectClose() error {\n\tcl := c.cl\n\tc.cl = nil\n\tc.serverName = \"\"\n\treturn cl.Close()\n}\n"
  },
  {
    "path": "internal/smtpconn/smtpconn_test.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage smtpconn\n\nimport (\n\t\"flag\"\n\t\"math/rand\"\n\t\"os\"\n\t\"strconv\"\n\t\"testing\"\n)\n\nvar testPort string\n\nfunc TestMain(m *testing.M) {\n\tremoteSmtpPort := flag.String(\"test.smtpport\", \"random\", \"(maddy) SMTP port to use for connections in tests\")\n\tflag.Parse()\n\n\tif *remoteSmtpPort == \"random\" {\n\t\t*remoteSmtpPort = strconv.Itoa(rand.Intn(65536-10000) + 10000)\n\t}\n\n\ttestPort = *remoteSmtpPort\n\tos.Exit(m.Run())\n}\n"
  },
  {
    "path": "internal/smtpconn/smtputf8_test.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage smtpconn\n\nimport (\n\t\"context\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/emersion/go-message/textproto\"\n\t\"github.com/emersion/go-smtp\"\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/exterrors\"\n\t\"github.com/foxcpp/maddy/internal/testutils\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc doTestDelivery(t *testing.T, conn *C, from string, to []string, opts smtp.MailOptions) error {\n\tt.Helper()\n\n\tif err := conn.Mail(context.Background(), from, opts); err != nil {\n\t\treturn err\n\t}\n\tfor _, rcpt := range to {\n\t\tif err := conn.Rcpt(context.Background(), rcpt, smtp.RcptOptions{}); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\thdr := textproto.Header{}\n\thdr.Add(\"B\", \"2\")\n\thdr.Add(\"A\", \"1\")\n\treturn conn.Data(context.Background(), hdr, strings.NewReader(\"foobar\\n\"))\n}\n\nfunc TestSMTPUTF8(t *testing.T) {\n\ttype test struct {\n\t\tclientSender string\n\t\tclientRcpt   string\n\n\t\tserverUTF8   bool\n\t\tserverSender string\n\t\tserverRcpt   string\n\n\t\texpectUTF8 bool\n\t\texpectErr  *exterrors.SMTPError\n\t}\n\tcheck := func(case_ test) {\n\t\tt.Helper()\n\n\t\tbe, srv := testutils.SMTPServer(t, \"127.0.0.1:\"+testPort)\n\t\tsrv.EnableSMTPUTF8 = case_.serverUTF8\n\t\tdefer func() {\n\t\t\trequire.NoError(t, srv.Close())\n\t\t}()\n\t\tdefer testutils.CheckSMTPConnLeak(t, srv)\n\n\t\tc := New()\n\t\tc.Log = testutils.Logger(t, \"target.smtp\")\n\t\tif _, err := c.Connect(context.Background(), config.Endpoint{\n\t\t\tScheme: \"tcp\",\n\t\t\tHost:   \"127.0.0.1\",\n\t\t\tPort:   testPort,\n\t\t}, false, nil); err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tdefer func() {\n\t\t\trequire.NoError(t, c.Close())\n\t\t}()\n\n\t\terr := doTestDelivery(t, c, case_.clientSender, []string{case_.clientRcpt},\n\t\t\tsmtp.MailOptions{UTF8: true})\n\t\tif err != nil {\n\t\t\tif case_.expectErr == nil {\n\t\t\t\tt.Error(\"Unexpected failure\")\n\t\t\t} else {\n\t\t\t\ttestutils.CheckSMTPErr(t, err, case_.expectErr.Code, case_.expectErr.EnhancedCode, case_.expectErr.Message)\n\t\t\t}\n\t\t\treturn\n\t\t} else if case_.expectErr != nil {\n\t\t\tt.Error(\"Unexpected success\")\n\t\t}\n\n\t\tbe.CheckMsg(t, 0, case_.serverSender, []string{case_.serverRcpt})\n\t\tif be.Messages[0].Opts.UTF8 != case_.expectUTF8 {\n\t\t\tt.Errorf(\"expectUTF8 = %v, SMTPUTF8 = %v\", case_.expectErr, be.Messages[0].Opts.UTF8)\n\t\t}\n\t}\n\n\tcheck(test{\n\t\tclientSender: \"test@тест.example.org\",\n\t\tclientRcpt:   \"test@example.invalid\",\n\t\tserverSender: \"test@xn--e1aybc.example.org\",\n\t\tserverRcpt:   \"test@example.invalid\",\n\t})\n\tcheck(test{\n\t\tclientSender: \"test@example.org\",\n\t\tclientRcpt:   \"test@тест.example.invalid\",\n\t\tserverSender: \"test@example.org\",\n\t\tserverRcpt:   \"test@xn--e1aybc.example.invalid\",\n\t})\n\tcheck(test{\n\t\tclientSender: \"тест@example.org\",\n\t\tclientRcpt:   \"test@example.invalid\",\n\t\tserverUTF8:   false,\n\t\texpectErr: &exterrors.SMTPError{\n\t\t\tCode:         550,\n\t\t\tEnhancedCode: exterrors.EnhancedCode{5, 6, 7},\n\t\t\tMessage:      \"SMTPUTF8 is unsupported, cannot convert sender address\",\n\t\t},\n\t})\n\tcheck(test{\n\t\tclientSender: \"test@example.org\",\n\t\tclientRcpt:   \"тест@example.invalid\",\n\t\tserverUTF8:   false,\n\t\texpectErr: &exterrors.SMTPError{\n\t\t\tCode:         553,\n\t\t\tEnhancedCode: exterrors.EnhancedCode{5, 6, 7},\n\t\t\tMessage:      \"SMTPUTF8 is unsupported, cannot convert recipient address\",\n\t\t},\n\t})\n\tcheck(test{\n\t\tclientSender: \"test@тест.org\",\n\t\tclientRcpt:   \"test@example.invalid\",\n\t\tserverSender: \"test@тест.org\",\n\t\tserverRcpt:   \"test@example.invalid\",\n\t\tserverUTF8:   true,\n\t\texpectUTF8:   true,\n\t})\n\tcheck(test{\n\t\tclientSender: \"test@example.org\",\n\t\tclientRcpt:   \"test@тест.example.invalid\",\n\t\tserverSender: \"test@example.org\",\n\t\tserverRcpt:   \"test@тест.example.invalid\",\n\t\tserverUTF8:   true,\n\t\texpectUTF8:   true,\n\t})\n\tcheck(test{\n\t\tclientSender: \"тест@example.org\",\n\t\tclientRcpt:   \"test@example.invalid\",\n\t\tserverSender: \"тест@example.org\",\n\t\tserverRcpt:   \"test@example.invalid\",\n\t\tserverUTF8:   true,\n\t\texpectUTF8:   true,\n\t})\n\tcheck(test{\n\t\tclientSender: \"test@example.org\",\n\t\tclientRcpt:   \"тест@example.invalid\",\n\t\tserverSender: \"test@example.org\",\n\t\tserverRcpt:   \"тест@example.invalid\",\n\t\tserverUTF8:   true,\n\t\texpectUTF8:   true,\n\t})\n}\n"
  },
  {
    "path": "internal/sqlite/is.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2026 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage sqliteprovider\n\nfunc IsSqliteDriver(name string) bool {\n\treturn name == \"sqlite\" || name == \"sqlite3\"\n}\n"
  },
  {
    "path": "internal/sqlite/modernc_sqlite3.go",
    "content": "//go:build (!nosqlite3 && !cgo) || modernc\n\n/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2026 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage sqliteprovider\n\nimport _ \"modernc.org/sqlite\"\n\nconst (\n\tIsAvailable  = true\n\tIsTranspiled = true\n)\n\nfunc MapDriverName(n string) string {\n\tif n == \"sqlite3\" {\n\t\treturn \"sqlite\"\n\t}\n\treturn n\n}\n"
  },
  {
    "path": "internal/sqlite/no_sqlite3.go",
    "content": "//go:build nosqlite3\n\n/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2026 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage sqliteprovider\n\nconst (\n\tIsAvailable  = false\n\tIsTranspiled = false\n)\n\nfunc MapDriverName(n string) string {\n\treturn n\n}\n"
  },
  {
    "path": "internal/sqlite/sqlite3.go",
    "content": "//go:build !nosqlite3 && cgo && !modernc\n\n/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2026 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage sqliteprovider\n\nimport _ \"github.com/mattn/go-sqlite3\"\n\nconst (\n\tIsAvailable  = true\n\tIsTranspiled = false\n)\n\nfunc MapDriverName(n string) string {\n\tif n == \"sqlite\" {\n\t\treturn \"sqlite3\"\n\t}\n\treturn n\n}\n"
  },
  {
    "path": "internal/storage/blob/fs/fs.go",
    "content": "package fs\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n)\n\n// FSStore struct represents directory on FS used to store blobs.\ntype FSStore struct {\n\tinstName string\n\troot     string\n}\n\nfunc New(_ *container.C, _, instName string) (module.Module, error) {\n\treturn &FSStore{instName: instName}, nil\n}\n\nfunc (s *FSStore) Name() string {\n\treturn \"storage.blob.fs\"\n}\n\nfunc (s *FSStore) InstanceName() string {\n\treturn s.instName\n}\n\nfunc (s *FSStore) Configure(inlineArgs []string, cfg *config.Map) error {\n\tswitch len(inlineArgs) {\n\tcase 0:\n\tcase 1:\n\t\ts.root = inlineArgs[0]\n\tdefault:\n\t\treturn fmt.Errorf(\"storage.blob.fs: 1 or 0 arguments expected\")\n\t}\n\n\tcfg.String(\"root\", false, false, s.root, &s.root)\n\tif _, err := cfg.Process(); err != nil {\n\t\treturn err\n\t}\n\n\tif s.root == \"\" {\n\t\treturn config.NodeErr(cfg.Block, \"storage.blob.fs: directory not set\")\n\t}\n\n\tif err := os.MkdirAll(s.root, os.ModeDir|os.ModePerm); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (s *FSStore) Open(_ context.Context, key string) (io.ReadCloser, error) {\n\tf, err := os.Open(filepath.Join(s.root, key))\n\tif err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\treturn nil, module.ErrNoSuchBlob\n\t\t}\n\t\treturn nil, err\n\t}\n\treturn f, nil\n}\n\nfunc (s *FSStore) Create(_ context.Context, key string, blobSize int64) (module.Blob, error) {\n\tf, err := os.Create(filepath.Join(s.root, key))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif blobSize >= 0 {\n\t\tif err := f.Truncate(blobSize); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\treturn f, nil\n}\n\nfunc (s *FSStore) Delete(_ context.Context, keys []string) error {\n\tfor _, key := range keys {\n\t\tif err := os.Remove(filepath.Join(s.root, key)); err != nil {\n\t\t\tif os.IsNotExist(err) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc init() {\n\tvar _ module.BlobStore = &FSStore{}\n\tmodules.Register((&FSStore{}).Name(), New)\n}\n"
  },
  {
    "path": "internal/storage/blob/fs/fs_test.go",
    "content": "package fs\n\nimport (\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/internal/storage/blob\"\n\t\"github.com/foxcpp/maddy/internal/testutils\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestFS(t *testing.T) {\n\tblob.TestStore(t, func() module.BlobStore {\n\t\tdir := testutils.Dir(t)\n\t\treturn &FSStore{instName: \"test\", root: dir}\n\t}, func(store module.BlobStore) {\n\t\trequire.NoError(t, os.RemoveAll(store.(*FSStore).root))\n\t})\n}\n"
  },
  {
    "path": "internal/storage/blob/s3/s3.go",
    "content": "package s3\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/log\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n\t\"github.com/minio/minio-go/v7\"\n\t\"github.com/minio/minio-go/v7/pkg/credentials\"\n)\n\nconst modName = \"storage.blob.s3\"\n\nconst (\n\tcredsTypeFileMinio = \"file_minio\"\n\tcredsTypeFileAWS   = \"file_aws\"\n\tcredsTypeAccessKey = \"access_key\"\n\tcredsTypeIAM       = \"iam\"\n\tcredsTypeDefault   = credsTypeAccessKey\n)\n\ntype Store struct {\n\tinstName string\n\tlog      *log.Logger\n\n\tendpoint string\n\tcl       *minio.Client\n\n\tbucketName   string\n\tobjectPrefix string\n}\n\nfunc New(c *container.C, modName, instName string) (module.Module, error) {\n\treturn &Store{\n\t\tinstName: instName,\n\t\tlog:      c.DefaultLogger.Sublogger(modName),\n\t}, nil\n}\n\nfunc (s *Store) Configure(inlineArgs []string, cfg *config.Map) error {\n\tif len(inlineArgs) != 0 {\n\t\treturn fmt.Errorf(\"%s: expected 0 arguments\", modName)\n\t}\n\n\tvar (\n\t\tsecure          bool\n\t\taccessKeyID     string\n\t\tsecretAccessKey string\n\t\tcredsType       string\n\t\tlocation        string\n\t)\n\tcfg.String(\"endpoint\", false, true, \"\", &s.endpoint)\n\tcfg.Bool(\"secure\", false, true, &secure)\n\tcfg.String(\"access_key\", false, true, \"\", &accessKeyID)\n\tcfg.String(\"secret_key\", false, true, \"\", &secretAccessKey)\n\tcfg.String(\"bucket\", false, true, \"\", &s.bucketName)\n\tcfg.String(\"region\", false, false, \"\", &location)\n\tcfg.String(\"object_prefix\", false, false, \"\", &s.objectPrefix)\n\tcfg.String(\"creds\", false, false, credsTypeDefault, &credsType)\n\n\tif _, err := cfg.Process(); err != nil {\n\t\treturn err\n\t}\n\tif s.endpoint == \"\" {\n\t\treturn fmt.Errorf(\"%s: endpoint not set\", modName)\n\t}\n\n\tvar creds *credentials.Credentials\n\n\tswitch credsType {\n\tcase credsTypeFileMinio:\n\t\tcreds = credentials.NewFileMinioClient(\"\", \"\")\n\tcase credsTypeFileAWS:\n\t\tcreds = credentials.NewFileAWSCredentials(\"\", \"\")\n\tcase credsTypeIAM:\n\t\tcreds = credentials.NewIAM(\"\")\n\tcase credsTypeAccessKey:\n\t\tcreds = credentials.NewStaticV4(accessKeyID, secretAccessKey, \"\")\n\tdefault:\n\t\tcreds = credentials.NewStaticV4(accessKeyID, secretAccessKey, \"\")\n\t}\n\n\tcl, err := minio.New(s.endpoint, &minio.Options{\n\t\tCreds:  creds,\n\t\tSecure: secure,\n\t\tRegion: location,\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"%s: %w\", modName, err)\n\t}\n\n\ts.cl = cl\n\treturn nil\n}\n\nfunc (s *Store) Name() string {\n\treturn modName\n}\n\nfunc (s *Store) InstanceName() string {\n\treturn s.instName\n}\n\ntype s3blob struct {\n\tpw      *io.PipeWriter\n\tdidSync bool\n\terrCh   chan error\n}\n\nfunc (b *s3blob) Sync() error {\n\t// We do this in Sync instead of Close because\n\t// backend may not actually check the error of Close.\n\t// The problematic restriction is that Sync can now be called\n\t// only once.\n\tif b.didSync {\n\t\tpanic(\"storage.blob.s3: Sync called twice for a blob object\")\n\t}\n\n\tif err := b.pw.Close(); err != nil {\n\t\treturn err\n\t}\n\tb.didSync = true\n\treturn <-b.errCh\n}\n\nfunc (b *s3blob) Write(p []byte) (n int, err error) {\n\treturn b.pw.Write(p)\n}\n\nfunc (b *s3blob) Close() error {\n\tif !b.didSync {\n\t\tif err := b.pw.CloseWithError(fmt.Errorf(\"storage.blob.s3: blob closed without Sync\")); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (s *Store) Create(ctx context.Context, key string, blobSize int64) (module.Blob, error) {\n\tpr, pw := io.Pipe()\n\terrCh := make(chan error, 1)\n\n\tgo func() {\n\t\tpartSize := uint64(0)\n\t\tif blobSize == module.UnknownBlobSize {\n\t\t\t// Without this, minio-go will allocate 500 MiB buffer which\n\t\t\t// is a little too much.\n\t\t\t// https://github.com/minio/minio-go/issues/1478\n\t\t\tpartSize = 1 * 1024 * 1024 /* 1 MiB */\n\t\t}\n\t\t_, err := s.cl.PutObject(ctx, s.bucketName, s.objectPrefix+key, pr, blobSize, minio.PutObjectOptions{\n\t\t\tPartSize: partSize,\n\t\t})\n\t\tif err != nil {\n\t\t\tif err := pr.CloseWithError(fmt.Errorf(\"s3 PutObject: %w\", err)); err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\t\t}\n\t\terrCh <- err\n\t}()\n\n\treturn &s3blob{\n\t\tpw:    pw,\n\t\terrCh: errCh,\n\t}, nil\n}\n\nfunc (s *Store) Open(ctx context.Context, key string) (io.ReadCloser, error) {\n\tobj, err := s.cl.GetObject(ctx, s.bucketName, s.objectPrefix+key, minio.GetObjectOptions{})\n\tif err != nil {\n\t\tresp := minio.ToErrorResponse(err)\n\t\tif resp.StatusCode == http.StatusNotFound {\n\t\t\treturn nil, module.ErrNoSuchBlob\n\t\t}\n\t\treturn nil, err\n\t}\n\treturn obj, nil\n}\n\nfunc (s *Store) Delete(ctx context.Context, keys []string) error {\n\tvar lastErr error\n\tfor _, k := range keys {\n\t\tlastErr = s.cl.RemoveObject(ctx, s.bucketName, s.objectPrefix+k, minio.RemoveObjectOptions{})\n\t\tif lastErr != nil {\n\t\t\ts.log.Error(\"failed to delete object\", lastErr, s.objectPrefix+k)\n\t\t}\n\t}\n\treturn lastErr\n}\n\nfunc init() {\n\tvar _ module.BlobStore = &Store{}\n\tmodules.Register(modName, New)\n}\n"
  },
  {
    "path": "internal/storage/blob/s3/s3_test.go",
    "content": "package s3\n\nimport (\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/internal/storage/blob\"\n\t\"github.com/johannesboyne/gofakes3\"\n\t\"github.com/johannesboyne/gofakes3/backend/s3mem\"\n)\n\nfunc TestFS(t *testing.T) {\n\tvar (\n\t\tbackend gofakes3.Backend\n\t\tfaker   *gofakes3.GoFakeS3\n\t\tts      *httptest.Server\n\t)\n\n\tblob.TestStore(t, func() module.BlobStore {\n\t\tbackend = s3mem.New()\n\t\tfaker = gofakes3.New(backend)\n\t\tts = httptest.NewServer(faker.Server())\n\n\t\tif err := backend.CreateBucket(\"maddy-test\"); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\tst := &Store{instName: \"test\"}\n\t\terr := st.Configure(nil, config.NewMap(map[string]interface{}{}, config.Node{\n\t\t\tChildren: []config.Node{\n\t\t\t\t{\n\t\t\t\t\tName: \"endpoint\",\n\t\t\t\t\tArgs: []string{ts.Listener.Addr().String()},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName: \"secure\",\n\t\t\t\t\tArgs: []string{\"false\"},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName: \"access_key\",\n\t\t\t\t\tArgs: []string{\"access-key\"},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName: \"secret_key\",\n\t\t\t\t\tArgs: []string{\"secret-key\"},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName: \"bucket\",\n\t\t\t\t\tArgs: []string{\"maddy-test\"},\n\t\t\t\t},\n\t\t\t},\n\t\t}))\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\treturn st\n\t}, func(store module.BlobStore) {\n\t\tts.Close()\n\n\t\tbackend = s3mem.New()\n\t\tfaker = gofakes3.New(backend)\n\t\tts = httptest.NewServer(faker.Server())\n\t})\n\n\tif ts != nil {\n\t\tts.Close()\n\t}\n}\n"
  },
  {
    "path": "internal/storage/blob/test_blob.go",
    "content": "//go:build cgo && !no_sqlite3\n// +build cgo,!no_sqlite3\n\npackage blob\n\nimport (\n\t\"math/rand\"\n\t\"testing\"\n\n\tbackendtests \"github.com/foxcpp/go-imap-backend-tests\"\n\timapsql \"github.com/foxcpp/go-imap-sql\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\timapsql2 \"github.com/foxcpp/maddy/internal/storage/imapsql\"\n\t\"github.com/foxcpp/maddy/internal/testutils\"\n)\n\ntype testBack struct {\n\tbackendtests.Backend\n\tExtStore module.BlobStore\n}\n\nfunc TestStore(t *testing.T, newStore func() module.BlobStore, cleanStore func(module.BlobStore)) {\n\t// We use go-imap-sql backend and run a subset of\n\t// go-imap-backend-tests related to loading and saving messages.\n\t//\n\t// In the future we should probably switch to using a memory\n\t// backend for this.\n\n\tbackendtests.Whitelist = []string{\n\t\tt.Name() + \"/Mailbox_CreateMessage\",\n\t\tt.Name() + \"/Mailbox_ListMessages_Body\",\n\t\tt.Name() + \"/Mailbox_CopyMessages\",\n\t\tt.Name() + \"/Mailbox_Expunge\",\n\t\tt.Name() + \"/Mailbox_MoveMessages\",\n\t}\n\n\tinitBackend := func() backendtests.Backend {\n\t\trandSrc := rand.NewSource(0)\n\t\tprng := rand.New(randSrc)\n\t\tstore := newStore()\n\n\t\tl := testutils.Logger(t, \"imapsql\")\n\t\tb, err := imapsql.New(\"sqlite3\", \":memory:\",\n\t\t\timapsql2.ExtBlobStore{Base: store}, imapsql.Opts{\n\t\t\t\tPRNG: prng,\n\t\t\t\tLog:  l,\n\t\t\t},\n\t\t)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\treturn testBack{Backend: b, ExtStore: store}\n\t}\n\tcleanBackend := func(bi backendtests.Backend) {\n\t\tb := bi.(testBack)\n\t\tif err := b.Backend.(*imapsql.Backend).Close(); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tcleanStore(b.ExtStore)\n\t}\n\n\tbackendtests.RunTests(t, initBackend, cleanBackend)\n}\n"
  },
  {
    "path": "internal/storage/blob/test_blob_nosqlite.go",
    "content": "//go:build !cgo || no_sqlite3\n// +build !cgo no_sqlite3\n\npackage blob\n\nimport (\n\t\"testing\"\n\n\t\"github.com/foxcpp/maddy/framework/module\"\n)\n\nfunc TestStore(t *testing.T, newStore func() module.BlobStore, cleanStore func(module.BlobStore)) {\n\tt.Skip(\"storage.blob tests require CGo and sqlite3\")\n}\n"
  },
  {
    "path": "internal/storage/imapsql/bench_test.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage imapsql\n\nimport (\n\t\"flag\"\n\t\"strconv\"\n\t\"testing\"\n\t\"time\"\n\n\timapsql \"github.com/foxcpp/go-imap-sql\"\n\t\"github.com/foxcpp/maddy/internal/testutils\"\n)\n\nvar (\n\ttestDB      string\n\ttestDSN     string\n\ttestFsstore string\n)\n\nfunc init() {\n\tflag.StringVar(&testDB, \"sql.testdb\", \"\", \"Database to use for storage/sql benchmarks\")\n\tflag.StringVar(&testDSN, \"sql.testdsn\", \"\", \"DSN to use for storage/sql benchmarks\")\n\tflag.StringVar(&testFsstore, \"sql.testfsstore\", \"\", \"fsstore location to use for storage/sql benchmarks\")\n}\n\nfunc createTestDB(tb testing.TB, compAlgo string) *Storage {\n\tif testDB == \"\" || testDSN == \"\" || testFsstore == \"\" {\n\t\ttb.Skip(\"-sql.testdb, -sql.testdsn and -sql.testfsstore should be specified to run this benchmark\")\n\t}\n\n\tdb, err := imapsql.New(testDB, testDSN, &imapsql.FSStore{Root: testFsstore}, imapsql.Opts{\n\t\tCompressAlgo: compAlgo,\n\t})\n\tif err != nil {\n\t\ttb.Fatal(err)\n\t}\n\treturn &Storage{\n\t\tBack: db,\n\t}\n}\n\nfunc BenchmarkStorage_Delivery(b *testing.B) {\n\trandomKey := \"rcpt-\" + strconv.FormatInt(time.Now().UnixNano(), 10) + \"@example.org\"\n\n\tbe := createTestDB(b, \"\")\n\tif err := be.CreateIMAPAcct(randomKey); err != nil {\n\t\tb.Fatal(err)\n\t}\n\n\ttestutils.BenchDelivery(b, be, \"sender@example.org\", []string{randomKey})\n}\n\nfunc BenchmarkStorage_DeliveryLZ4(b *testing.B) {\n\trandomKey := \"rcpt-\" + strconv.FormatInt(time.Now().UnixNano(), 10) + \"@example.org\"\n\n\tbe := createTestDB(b, \"lz4\")\n\tif err := be.CreateIMAPAcct(randomKey); err != nil {\n\t\tb.Fatal(err)\n\t}\n\n\ttestutils.BenchDelivery(b, be, \"sender@example.org\", []string{randomKey})\n}\n\nfunc BenchmarkStorage_DeliveryZstd(b *testing.B) {\n\trandomKey := \"rcpt-\" + strconv.FormatInt(time.Now().UnixNano(), 10) + \"@example.org\"\n\n\tbe := createTestDB(b, \"zstd\")\n\tif err := be.CreateIMAPAcct(randomKey); err != nil {\n\t\tb.Fatal(err)\n\t}\n\n\ttestutils.BenchDelivery(b, be, \"sender@example.org\", []string{randomKey})\n}\n"
  },
  {
    "path": "internal/storage/imapsql/delivery.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage imapsql\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"runtime/trace\"\n\n\t\"github.com/emersion/go-imap\"\n\t\"github.com/emersion/go-imap/backend\"\n\t\"github.com/emersion/go-message/textproto\"\n\t\"github.com/emersion/go-smtp\"\n\timapsql \"github.com/foxcpp/go-imap-sql\"\n\t\"github.com/foxcpp/maddy/framework/buffer\"\n\t\"github.com/foxcpp/maddy/framework/exterrors\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/internal/target\"\n)\n\ntype addedRcpt struct {\n\trcptTo string\n}\ntype delivery struct {\n\tstore    *Storage\n\tmsgMeta  *module.MsgMetadata\n\td        imapsql.Delivery\n\tmailFrom string\n\n\taddedRcpts map[string]addedRcpt\n}\n\nfunc (d *delivery) String() string {\n\treturn d.store.Name() + \":\" + d.store.InstanceName()\n}\n\nfunc userDoesNotExist(actual error) error {\n\treturn &exterrors.SMTPError{\n\t\tCode:         501,\n\t\tEnhancedCode: exterrors.EnhancedCode{5, 1, 1},\n\t\tMessage:      \"User does not exist\",\n\t\tTargetName:   \"imapsql\",\n\t\tErr:          actual,\n\t}\n}\n\nfunc (d *delivery) AddRcpt(ctx context.Context, rcptTo string, _ smtp.RcptOptions) error {\n\tdefer trace.StartRegion(ctx, \"sql/AddRcpt\").End()\n\n\taccountName, err := d.store.deliveryNormalize(ctx, rcptTo)\n\tif err != nil {\n\t\treturn userDoesNotExist(err)\n\t}\n\n\tif _, ok := d.addedRcpts[accountName]; ok {\n\t\treturn nil\n\t}\n\n\t// This header is added to the message only for that recipient.\n\t// go-imap-sql does certain optimizations to store the message\n\t// with small amount of per-recipient data in a efficient way.\n\tuserHeader := textproto.Header{}\n\tuserHeader.Add(\"Delivered-To\", accountName)\n\n\tif err := d.d.AddRcpt(accountName, userHeader); err != nil {\n\t\tif errors.Is(err, imapsql.ErrUserDoesntExists) || errors.Is(err, backend.ErrNoSuchMailbox) {\n\t\t\treturn userDoesNotExist(err)\n\t\t}\n\t\tvar serializationError imapsql.SerializationError\n\t\tif errors.As(err, &serializationError) {\n\t\t\treturn &exterrors.SMTPError{\n\t\t\t\tCode:         453,\n\t\t\t\tEnhancedCode: exterrors.EnhancedCode{4, 3, 2},\n\t\t\t\tMessage:      \"Internal server error, try again later\",\n\t\t\t\tTargetName:   \"imapsql\",\n\t\t\t\tErr:          err,\n\t\t\t}\n\t\t}\n\t\treturn err\n\t}\n\n\td.addedRcpts[accountName] = addedRcpt{\n\t\trcptTo: rcptTo,\n\t}\n\treturn nil\n}\n\nfunc (d *delivery) Body(ctx context.Context, header textproto.Header, body buffer.Buffer) error {\n\tdefer trace.StartRegion(ctx, \"sql/Body\").End()\n\n\tif !d.msgMeta.Quarantine && d.store.filters != nil {\n\t\tfor rcpt, rcptData := range d.addedRcpts {\n\t\t\tfolder, flags, err := d.store.filters.IMAPFilter(rcpt, rcptData.rcptTo, d.msgMeta, header, body)\n\t\t\tif err != nil {\n\t\t\t\td.store.log.Error(\"IMAPFilter failed\", err, \"rcpt\", rcpt)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\td.d.UserMailbox(rcpt, folder, flags)\n\t\t}\n\t}\n\n\tif d.msgMeta.Quarantine {\n\t\tif err := d.d.SpecialMailbox(imap.JunkAttr, d.store.junkMbox); err != nil {\n\t\t\tvar serializationError imapsql.SerializationError\n\t\t\tif errors.As(err, &serializationError) {\n\t\t\t\treturn &exterrors.SMTPError{\n\t\t\t\t\tCode:         453,\n\t\t\t\t\tEnhancedCode: exterrors.EnhancedCode{4, 3, 2},\n\t\t\t\t\tMessage:      \"Internal server error, try again later\",\n\t\t\t\t\tTargetName:   \"imapsql\",\n\t\t\t\t\tErr:          err,\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\t}\n\n\theader = header.Copy()\n\theader.Add(\"Return-Path\", \"<\"+target.SanitizeForHeader(d.mailFrom)+\">\")\n\terr := d.d.BodyParsed(header, body.Len(), body)\n\tvar serializationError imapsql.SerializationError\n\tif errors.As(err, &serializationError) {\n\t\treturn &exterrors.SMTPError{\n\t\t\tCode:         453,\n\t\t\tEnhancedCode: exterrors.EnhancedCode{4, 3, 2},\n\t\t\tMessage:      \"Internal server error, try again later\",\n\t\t\tTargetName:   \"imapsql\",\n\t\t\tErr:          err,\n\t\t}\n\t}\n\treturn err\n}\n\nfunc (d *delivery) Abort(ctx context.Context) error {\n\tdefer trace.StartRegion(ctx, \"sql/Abort\").End()\n\n\treturn d.d.Abort()\n}\n\nfunc (d *delivery) Commit(ctx context.Context) error {\n\tdefer trace.StartRegion(ctx, \"sql/Commit\").End()\n\n\treturn d.d.Commit()\n}\n\nfunc (store *Storage) StartDelivery(ctx context.Context, msgMeta *module.MsgMetadata, mailFrom string) (module.Delivery, error) {\n\tdefer trace.StartRegion(ctx, \"sql/StartDelivery\").End()\n\n\treturn &delivery{\n\t\tstore:      store,\n\t\tmsgMeta:    msgMeta,\n\t\tmailFrom:   mailFrom,\n\t\td:          store.Back.NewDelivery(),\n\t\taddedRcpts: map[string]addedRcpt{},\n\t}, nil\n}\n"
  },
  {
    "path": "internal/storage/imapsql/external_blob_store.go",
    "content": "package imapsql\n\nimport (\n\t\"context\"\n\t\"io\"\n\n\timapsql \"github.com/foxcpp/go-imap-sql\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n)\n\ntype ExtBlob struct {\n\tio.ReadCloser\n}\n\nfunc (e ExtBlob) Sync() error {\n\tpanic(\"not implemented\")\n}\n\nfunc (e ExtBlob) Write(p []byte) (n int, err error) {\n\tpanic(\"not implemented\")\n}\n\ntype WriteExtBlob struct {\n\tmodule.Blob\n}\n\nfunc (w WriteExtBlob) Read(p []byte) (n int, err error) {\n\tpanic(\"not implemented\")\n}\n\ntype ExtBlobStore struct {\n\tBase module.BlobStore\n}\n\nfunc (e ExtBlobStore) Create(key string, objSize int64) (imapsql.ExtStoreObj, error) {\n\tblob, err := e.Base.Create(context.TODO(), key, objSize)\n\tif err != nil {\n\t\treturn nil, imapsql.ExternalError{\n\t\t\tNonExistent: err == module.ErrNoSuchBlob,\n\t\t\tKey:         key,\n\t\t\tErr:         err,\n\t\t}\n\t}\n\treturn WriteExtBlob{Blob: blob}, nil\n}\n\nfunc (e ExtBlobStore) Open(key string) (imapsql.ExtStoreObj, error) {\n\tblob, err := e.Base.Open(context.TODO(), key)\n\tif err != nil {\n\t\treturn nil, imapsql.ExternalError{\n\t\t\tNonExistent: err == module.ErrNoSuchBlob,\n\t\t\tKey:         key,\n\t\t\tErr:         err,\n\t\t}\n\t}\n\treturn ExtBlob{ReadCloser: blob}, nil\n}\n\nfunc (e ExtBlobStore) Delete(keys []string) error {\n\terr := e.Base.Delete(context.TODO(), keys)\n\tif err != nil {\n\t\treturn imapsql.ExternalError{\n\t\t\tKey: \"\",\n\t\t\tErr: err,\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/storage/imapsql/imapsql.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\n// Package imapsql implements SQL-based storage module\n// using go-imap-sql library (github.com/foxcpp/go-imap-sql).\n//\n// Interfaces implemented:\n// - module.StorageBackend\n// - module.PlainAuth\n// - module.DeliveryTarget\npackage imapsql\n\nimport (\n\t\"context\"\n\t\"crypto/sha1\"\n\t\"database/sql\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"runtime/debug\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/emersion/go-imap\"\n\tsortthread \"github.com/emersion/go-imap-sortthread\"\n\t\"github.com/emersion/go-imap/backend\"\n\tmess \"github.com/foxcpp/go-imap-mess\"\n\timapsql \"github.com/foxcpp/go-imap-sql\"\n\t\"github.com/foxcpp/maddy/framework/config\"\n\tmodconfig \"github.com/foxcpp/maddy/framework/config/module\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/dns\"\n\t\"github.com/foxcpp/maddy/framework/log\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n\t\"github.com/foxcpp/maddy/internal/authz\"\n\tsqliteprovider \"github.com/foxcpp/maddy/internal/sqlite\"\n\t\"github.com/foxcpp/maddy/internal/updatepipe\"\n\t\"github.com/foxcpp/maddy/internal/updatepipe/pubsub\"\n\n\t_ \"github.com/go-sql-driver/mysql\"\n\t_ \"github.com/lib/pq\"\n)\n\nconst modName = \"storage.imapsql\"\n\ntype Storage struct {\n\tBack     *imapsql.Backend\n\tinstName string\n\tlog      *log.Logger\n\n\tjunkMbox string\n\n\tdriver    string\n\tdsn       []string\n\tblobStore module.BlobStore\n\topts      *imapsql.Opts\n\n\tresolver dns.Resolver\n\n\tupdPipe      updatepipe.P\n\tupdPushStop  chan struct{}\n\toutboundUpds chan mess.Update\n\n\tfilters module.IMAPFilter\n\n\tdeliveryMap       module.Table\n\tdeliveryNormalize func(context.Context, string) (string, error)\n\tauthMap           module.Table\n\tauthNormalize     func(context.Context, string) (string, error)\n}\n\nfunc (store *Storage) Name() string {\n\treturn modName\n}\n\nfunc (store *Storage) InstanceName() string {\n\treturn store.instName\n}\n\nfunc New(c *container.C, modName, instName string) (module.Module, error) {\n\tstore := &Storage{\n\t\tinstName: instName,\n\t\tlog:      c.DefaultLogger.Sublogger(modName),\n\t\tresolver: dns.DefaultResolver(),\n\t}\n\treturn store, nil\n}\n\nfunc (store *Storage) Configure(inlineArgs []string, cfg *config.Map) error {\n\tif len(inlineArgs) != 0 {\n\t\tif len(inlineArgs) == 1 {\n\t\t\treturn errors.New(\"imapsql: expected at least 2 arguments\")\n\t\t}\n\n\t\tstore.driver = inlineArgs[0]\n\t\tstore.dsn = inlineArgs[1:]\n\t}\n\n\tvar (\n\t\tdriver            string\n\t\tdsn               []string\n\t\tappendlimitVal    int64 = -1\n\t\tcompression       []string\n\t\tauthNormalize     string\n\t\tdeliveryNormalize string\n\n\t\tblobStore module.BlobStore\n\t)\n\n\topts := &imapsql.Opts{}\n\tcfg.String(\"driver\", false, false, store.driver, &driver)\n\tcfg.StringList(\"dsn\", false, false, store.dsn, &dsn)\n\tcfg.Callback(\"fsstore\", func(m *config.Map, node config.Node) error {\n\t\tstore.log.Msg(\"'fsstore' directive is deprecated, use 'msg_store fs' instead\")\n\t\treturn modconfig.ModuleFromNode(\"storage.blob\", append([]string{\"fs\"}, node.Args...),\n\t\t\tnode, m.Globals, &blobStore)\n\t})\n\tcfg.Custom(\"msg_store\", false, false, func() (interface{}, error) {\n\t\tvar store module.BlobStore\n\t\terr := modconfig.ModuleFromNode(\"storage.blob\", []string{\"fs\", \"messages\"},\n\t\t\tconfig.Node{}, nil, &store)\n\t\treturn store, err\n\t}, func(m *config.Map, node config.Node) (interface{}, error) {\n\t\tvar store module.BlobStore\n\t\terr := modconfig.ModuleFromNode(\"storage.blob\", node.Args,\n\t\t\tnode, m.Globals, &store)\n\t\treturn store, err\n\t}, &blobStore)\n\tcfg.StringList(\"compression\", false, false, []string{\"off\"}, &compression)\n\tcfg.DataSize(\"appendlimit\", false, false, 32*1024*1024, &appendlimitVal)\n\tcfg.Bool(\"debug\", true, false, &store.log.Debug)\n\tcfg.Int(\"sqlite3_cache_size\", false, false, 0, &opts.CacheSize)\n\tcfg.Int(\"sqlite3_busy_timeout\", false, false, 5000, &opts.BusyTimeout)\n\tcfg.Bool(\"disable_recent\", false, true, &opts.DisableRecent)\n\tcfg.String(\"junk_mailbox\", false, false, \"Junk\", &store.junkMbox)\n\tcfg.Custom(\"imap_filter\", false, false, func() (interface{}, error) {\n\t\treturn nil, nil\n\t}, func(m *config.Map, node config.Node) (interface{}, error) {\n\t\tvar filter module.IMAPFilter\n\t\terr := modconfig.GroupFromNode(\"imap_filters\", node.Args, node, m.Globals, &filter)\n\t\treturn filter, err\n\t}, &store.filters)\n\tcfg.Custom(\"auth_map\", false, false, func() (interface{}, error) {\n\t\treturn nil, nil\n\t}, modconfig.TableDirective, &store.authMap)\n\tcfg.String(\"auth_normalize\", false, false, \"auto\", &authNormalize)\n\tcfg.Custom(\"delivery_map\", false, false, func() (interface{}, error) {\n\t\treturn nil, nil\n\t}, modconfig.TableDirective, &store.deliveryMap)\n\tcfg.String(\"delivery_normalize\", false, false, \"precis_casefold_email\", &deliveryNormalize)\n\n\tif _, err := cfg.Process(); err != nil {\n\t\treturn err\n\t}\n\n\tif dsn == nil {\n\t\treturn errors.New(\"imapsql: dsn is required\")\n\t}\n\tif driver == \"\" {\n\t\treturn errors.New(\"imapsql: driver is required\")\n\t}\n\n\tif sqliteprovider.IsSqliteDriver(driver) {\n\t\tif sqliteprovider.IsTranspiled {\n\t\t\tstore.log.Println(\"using transpiled SQLite (modernc.org/sqlite)\")\n\t\t} else if sqliteprovider.IsAvailable {\n\t\t\tstore.log.Debugln(\"using cgo SQLite\")\n\t\t} else {\n\t\t\treturn errors.New(\"imapsql: SQLite is not supported, recompile without no_sqlite3 tag set\")\n\t\t}\n\t}\n\tdriver = sqliteprovider.MapDriverName(driver)\n\n\tdeliveryNormFunc, ok := authz.NormalizeFuncs[deliveryNormalize]\n\tif !ok {\n\t\treturn errors.New(\"imapsql: unknown normalization function: \" + deliveryNormalize)\n\t}\n\tstore.deliveryNormalize = func(ctx context.Context, s string) (string, error) {\n\t\treturn deliveryNormFunc(s)\n\t}\n\tif store.deliveryMap != nil {\n\t\tstore.deliveryNormalize = func(ctx context.Context, email string) (string, error) {\n\t\t\temail, err := deliveryNormFunc(email)\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\t\t\tmapped, ok, err := store.deliveryMap.Lookup(ctx, email)\n\t\t\tif err != nil || !ok {\n\t\t\t\treturn \"\", userDoesNotExist(err)\n\t\t\t}\n\t\t\treturn mapped, nil\n\t\t}\n\t}\n\n\tif authNormalize != \"auto\" {\n\t\tstore.log.Msg(\"auth_normalize in storage.imapsql is deprecated and will be removed in the next release, use storage_map in imap config instead\")\n\t}\n\tauthNormFunc, ok := authz.NormalizeFuncs[authNormalize]\n\tif !ok {\n\t\treturn errors.New(\"imapsql: unknown normalization function: \" + authNormalize)\n\t}\n\tstore.authNormalize = func(ctx context.Context, s string) (string, error) {\n\t\treturn authNormFunc(s)\n\t}\n\tif store.authMap != nil {\n\t\tstore.log.Msg(\"auth_map in storage.imapsql is deprecated and will be removed in the next release, use storage_map in imap config instead\")\n\t\tstore.authNormalize = func(ctx context.Context, username string) (string, error) {\n\t\t\tusername, err := authNormFunc(username)\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\t\t\tmapped, ok, err := store.authMap.Lookup(ctx, username)\n\t\t\tif err != nil || !ok {\n\t\t\t\treturn \"\", userDoesNotExist(err)\n\t\t\t}\n\t\t\treturn mapped, nil\n\t\t}\n\t}\n\n\topts.Log = store.log\n\n\tif appendlimitVal == -1 {\n\t\topts.MaxMsgBytes = nil\n\t} else {\n\t\t// int is 32-bit on some platforms, so cut off values we can't actually\n\t\t// use.\n\t\tif int64(uint32(appendlimitVal)) != appendlimitVal {\n\t\t\treturn errors.New(\"imapsql: appendlimit value is too big\")\n\t\t}\n\t\topts.MaxMsgBytes = new(uint32)\n\t\t*opts.MaxMsgBytes = uint32(appendlimitVal)\n\t}\n\n\tif len(compression) != 0 {\n\t\tswitch compression[0] {\n\t\tcase \"zstd\", \"lz4\":\n\t\t\topts.CompressAlgo = compression[0]\n\t\t\tif len(compression) == 2 {\n\t\t\t\topts.CompressAlgoParams = compression[1]\n\t\t\t\tif _, err := strconv.Atoi(compression[1]); err != nil {\n\t\t\t\t\treturn errors.New(\"imapsql: first argument for lz4 and zstd is compression level\")\n\t\t\t\t}\n\t\t\t}\n\t\t\tif len(compression) > 2 {\n\t\t\t\treturn errors.New(\"imapsql: expected at most 2 arguments\")\n\t\t\t}\n\t\tcase \"off\":\n\t\t\tif len(compression) > 1 {\n\t\t\t\treturn errors.New(\"imapsql: expected at most 1 arguments\")\n\t\t\t}\n\t\tdefault:\n\t\t\treturn errors.New(\"imapsql: unknown compression algorithm\")\n\t\t}\n\t}\n\n\tdriverFound := false\n\tfor _, d := range sql.Drivers() {\n\t\tif d == driver {\n\t\t\tdriverFound = true\n\t\t\tbreak\n\t\t}\n\t}\n\tif !driverFound {\n\t\treturn fmt.Errorf(\"imapsql: unknown driver %q\", driver)\n\t}\n\n\tstore.driver = driver\n\tstore.dsn = dsn\n\tstore.blobStore = blobStore\n\tstore.opts = opts\n\tstore.log.Debugln(\"go-imap-sql version\", imapsql.VersionStr)\n\n\treturn nil\n}\n\nfunc (store *Storage) Start() error {\n\tdsnStr := strings.Join(store.dsn, \" \")\n\tvar err error\n\tstore.Back, err = imapsql.New(store.driver, dsnStr, ExtBlobStore{Base: store.blobStore}, *store.opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"imapsql: %s\", err)\n\t}\n\treturn nil\n}\n\nfunc (store *Storage) EnableUpdatePipe(mode updatepipe.BackendMode) error {\n\tif store.updPipe != nil {\n\t\treturn nil\n\t}\n\n\tswitch store.driver {\n\tcase \"sqlite3\", \"sqlite\":\n\t\tdbId := sha1.Sum([]byte(strings.Join(store.dsn, \" \")))\n\t\tsockPath := filepath.Join(\n\t\t\tconfig.RuntimeDirectory,\n\t\t\tfmt.Sprintf(\"sql-%s.sock\", hex.EncodeToString(dbId[:])))\n\t\tstore.log.DebugMsg(\"using unix socket for external updates\", \"path\", sockPath)\n\t\tstore.updPipe = &updatepipe.UnixSockPipe{\n\t\t\tSockPath: sockPath,\n\t\t\tLog:      store.log.Sublogger(\"updpipe\"),\n\t\t}\n\tcase \"postgres\":\n\t\tstore.log.DebugMsg(\"using PostgreSQL broker for external updates\")\n\t\tps, err := pubsub.NewPQ(strings.Join(store.dsn, \" \"))\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"enable_update_pipe: %w\", err)\n\t\t}\n\t\tps.Log = store.log.Sublogger(\"updpipe/pubsub\")\n\t\tpipe := &updatepipe.PubSubPipe{\n\t\t\tPubSub: ps,\n\t\t\tLog:    store.log.Sublogger(\"updpipe\"),\n\t\t}\n\t\tstore.Back.UpdateManager().ExternalUnsubscribe = pipe.Unsubscribe\n\t\tstore.Back.UpdateManager().ExternalSubscribe = pipe.Subscribe\n\t\tstore.updPipe = pipe\n\tdefault:\n\t\treturn errors.New(\"imapsql: driver does not have an update pipe implementation\")\n\t}\n\n\tinbound := make(chan mess.Update, 32)\n\toutbound := make(chan mess.Update, 10)\n\tstore.outboundUpds = outbound\n\n\tif mode == updatepipe.ModeReplicate {\n\t\tif err := store.updPipe.Listen(inbound); err != nil {\n\t\t\tstore.updPipe = nil\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif err := store.updPipe.InitPush(); err != nil {\n\t\tstore.updPipe = nil\n\t\treturn err\n\t}\n\n\tstore.Back.UpdateManager().SetExternalSink(outbound)\n\n\tstore.updPushStop = make(chan struct{}, 1)\n\tgo func() {\n\t\tdefer func() {\n\t\t\t// Ensure we sent all outbound updates.\n\t\t\tfor upd := range outbound {\n\t\t\t\tif err := store.updPipe.Push(upd); err != nil {\n\t\t\t\t\tstore.log.Error(\"IMAP update pipe push failed\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t\tstore.updPushStop <- struct{}{}\n\n\t\t\tif err := recover(); err != nil {\n\t\t\t\tstack := debug.Stack()\n\t\t\t\tlog.Printf(\"panic during imapsql update push: %v\\n%s\", err, stack)\n\t\t\t}\n\t\t}()\n\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase u := <-inbound:\n\t\t\t\tstore.log.DebugMsg(\"external update received\", \"type\", u.Type, \"key\", u.Key)\n\t\t\t\tstore.Back.UpdateManager().ExternalUpdate(u)\n\t\t\tcase u, ok := <-outbound:\n\t\t\t\tif !ok {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tstore.log.DebugMsg(\"sending external update\", \"type\", u.Type, \"key\", u.Key)\n\t\t\t\tif err := store.updPipe.Push(u); err != nil {\n\t\t\t\t\tstore.log.Error(\"IMAP update pipe push failed\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}()\n\n\treturn nil\n}\n\nfunc (store *Storage) I18NLevel() int {\n\treturn 1\n}\n\nfunc (store *Storage) IMAPExtensions() []string {\n\treturn []string{\"APPENDLIMIT\", \"MOVE\", \"CHILDREN\", \"SPECIAL-USE\", \"I18NLEVEL=1\", \"SORT\", \"THREAD=ORDEREDSUBJECT\"}\n}\n\nfunc (store *Storage) CreateMessageLimit() *uint32 {\n\treturn store.Back.CreateMessageLimit()\n}\n\nfunc (store *Storage) GetOrCreateIMAPAcct(username string) (backend.User, error) {\n\taccountName, err := store.authNormalize(context.TODO(), username)\n\tif err != nil {\n\t\treturn nil, backend.ErrInvalidCredentials\n\t}\n\n\treturn store.Back.GetOrCreateUser(accountName)\n}\n\nfunc (store *Storage) Lookup(ctx context.Context, key string) (string, bool, error) {\n\taccountName, err := store.authNormalize(ctx, key)\n\tif err != nil {\n\t\treturn \"\", false, nil\n\t}\n\n\tusr, err := store.Back.GetUser(accountName)\n\tif err != nil {\n\t\tif errors.Is(err, imapsql.ErrUserDoesntExists) {\n\t\t\treturn \"\", false, nil\n\t\t}\n\t\treturn \"\", false, err\n\t}\n\tif err := usr.Logout(); err != nil {\n\t\tstore.log.Error(\"logout failed\", err, \"username\", accountName)\n\t}\n\n\treturn \"\", true, nil\n}\n\nfunc (store *Storage) Stop() error {\n\t// Stop backend from generating new updates.\n\tif err := store.Back.Close(); err != nil {\n\t\tstore.log.Error(\"close backend failed\", err)\n\t}\n\n\t// Wait for 'updates replicate' goroutine to actually stop so we will send\n\t// all updates before shutting down (this is especially important for\n\t// maddy subcommands).\n\tif store.updPipe != nil {\n\t\tclose(store.outboundUpds)\n\t\t<-store.updPushStop\n\n\t\tif err := store.updPipe.Close(); err != nil {\n\t\t\tstore.log.Error(\"updatepipe close failed\", err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (store *Storage) Login(_ *imap.ConnInfo, usenrame, password string) (backend.User, error) {\n\tpanic(\"This method should not be called and is added only to satisfy backend.Backend interface\")\n}\n\nfunc (store *Storage) SupportedThreadAlgorithms() []sortthread.ThreadAlgorithm {\n\treturn []sortthread.ThreadAlgorithm{sortthread.OrderedSubject}\n}\n\nfunc init() {\n\tmodules.Register(\"storage.imapsql\", New)\n\tmodules.Register(\"target.imapsql\", New)\n}\n"
  },
  {
    "path": "internal/storage/imapsql/maddyctl.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage imapsql\n\nimport (\n\t\"github.com/emersion/go-imap/backend\"\n)\n\n// These methods wrap corresponding go-imap-sql methods, but also apply\n// maddy-specific credentials rules.\n\nfunc (store *Storage) ListIMAPAccts() ([]string, error) {\n\treturn store.Back.ListUsers()\n}\n\nfunc (store *Storage) CreateIMAPAcct(accountName string) error {\n\treturn store.Back.CreateUser(accountName)\n}\n\nfunc (store *Storage) DeleteIMAPAcct(accountName string) error {\n\treturn store.Back.DeleteUser(accountName)\n}\n\nfunc (store *Storage) GetIMAPAcct(accountName string) (backend.User, error) {\n\treturn store.Back.GetUser(accountName)\n}\n"
  },
  {
    "path": "internal/table/chain.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage table\n\nimport (\n\t\"context\"\n\n\t\"github.com/foxcpp/maddy/framework/config\"\n\tmodconfig \"github.com/foxcpp/maddy/framework/config/module\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n)\n\ntype Chain struct {\n\tmodName  string\n\tinstName string\n\n\tchain    []module.Table\n\toptional []bool\n}\n\nfunc NewChain(_ *container.C, modName, instName string) (module.Module, error) {\n\treturn &Chain{\n\t\tmodName:  modName,\n\t\tinstName: instName,\n\t}, nil\n}\n\nfunc (s *Chain) Configure(inlineArgs []string, cfg *config.Map) error {\n\tcfg.Callback(\"step\", func(m *config.Map, node config.Node) error {\n\t\tvar tbl module.Table\n\t\terr := modconfig.ModuleFromNode(\"table\", node.Args, node, m.Globals, &tbl)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\ts.chain = append(s.chain, tbl)\n\t\ts.optional = append(s.optional, false)\n\t\treturn nil\n\t})\n\tcfg.Callback(\"optional_step\", func(m *config.Map, node config.Node) error {\n\t\tvar tbl module.Table\n\t\terr := modconfig.ModuleFromNode(\"table\", node.Args, node, m.Globals, &tbl)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\ts.chain = append(s.chain, tbl)\n\t\ts.optional = append(s.optional, true)\n\t\treturn nil\n\t})\n\n\t_, err := cfg.Process()\n\treturn err\n}\n\nfunc (s *Chain) Name() string {\n\treturn s.modName\n}\n\nfunc (s *Chain) InstanceName() string {\n\treturn s.instName\n}\n\nfunc (s *Chain) Lookup(ctx context.Context, key string) (string, bool, error) {\n\tnewVal, err := s.LookupMulti(ctx, key)\n\tif err != nil {\n\t\treturn \"\", false, err\n\t}\n\tif len(newVal) == 0 {\n\t\treturn \"\", false, nil\n\t}\n\n\treturn newVal[0], true, nil\n}\n\nfunc (s *Chain) LookupMulti(ctx context.Context, key string) ([]string, error) {\n\tresult := []string{key}\nSTEP:\n\tfor i, step := range s.chain {\n\t\tnewResult := []string{}\n\t\tfor _, key = range result {\n\t\t\tif step_multi, ok := step.(module.MultiTable); ok {\n\t\t\t\tval, err := step_multi.LookupMulti(ctx, key)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn []string{}, err\n\t\t\t\t}\n\t\t\t\tif len(val) == 0 {\n\t\t\t\t\tif s.optional[i] {\n\t\t\t\t\t\tcontinue STEP\n\t\t\t\t\t}\n\t\t\t\t\treturn []string{}, nil\n\t\t\t\t}\n\t\t\t\tnewResult = append(newResult, val...)\n\t\t\t} else {\n\t\t\t\tval, ok, err := step.Lookup(ctx, key)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn []string{}, err\n\t\t\t\t}\n\t\t\t\tif !ok {\n\t\t\t\t\tif s.optional[i] {\n\t\t\t\t\t\tcontinue STEP\n\t\t\t\t\t}\n\t\t\t\t\treturn []string{}, nil\n\t\t\t\t}\n\t\t\t\tnewResult = append(newResult, val)\n\t\t\t}\n\t\t}\n\t\tresult = newResult\n\t}\n\treturn result, nil\n}\n\nfunc init() {\n\tmodules.Register(\"table.chain\", NewChain)\n}\n"
  },
  {
    "path": "internal/table/email_localpart.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage table\n\nimport (\n\t\"context\"\n\n\t\"github.com/foxcpp/maddy/framework/address\"\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n)\n\ntype EmailLocalpart struct {\n\tmodName       string\n\tinstName      string\n\tallowNonEmail bool\n}\n\nfunc NewEmailLocalpart(_ *container.C, modName, instName string) (module.Module, error) {\n\treturn &EmailLocalpart{\n\t\tmodName:       modName,\n\t\tinstName:      instName,\n\t\tallowNonEmail: modName == \"table.email_localpart_optional\",\n\t}, nil\n}\n\nfunc (s *EmailLocalpart) Configure(inlineArgs []string, cfg *config.Map) error {\n\treturn nil\n}\n\nfunc (s *EmailLocalpart) Name() string {\n\treturn s.modName\n}\n\nfunc (s *EmailLocalpart) InstanceName() string {\n\treturn s.modName\n}\n\nfunc (s *EmailLocalpart) Lookup(ctx context.Context, key string) (string, bool, error) {\n\tmbox, _, err := address.Split(key)\n\tif err != nil {\n\t\tif s.allowNonEmail {\n\t\t\treturn key, true, nil\n\t\t}\n\t\t// Invalid email, no local part mapping.\n\t\treturn \"\", false, nil\n\t}\n\treturn mbox, true, nil\n}\n\nfunc init() {\n\tmodules.Register(\"table.email_localpart\", NewEmailLocalpart)\n\tmodules.Register(\"table.email_localpart_optional\", NewEmailLocalpart)\n}\n"
  },
  {
    "path": "internal/table/email_with_domain.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage table\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/foxcpp/maddy/framework/address\"\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/log\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n)\n\ntype EmailWithDomain struct {\n\tmodName  string\n\tinstName string\n\tdomains  []string\n\tlog      *log.Logger\n}\n\nfunc NewEmailWithDomain(c *container.C, modName, instName string) (module.Module, error) {\n\treturn &EmailWithDomain{\n\t\tmodName:  modName,\n\t\tinstName: instName,\n\t\tlog:      c.DefaultLogger.Sublogger(modName),\n\t}, nil\n}\n\nfunc (s *EmailWithDomain) Configure(inlineArgs []string, cfg *config.Map) error {\n\ts.domains = inlineArgs\n\n\tfor _, d := range s.domains {\n\t\tif !address.ValidDomain(d) {\n\t\t\treturn fmt.Errorf(\"%s: invalid domain: %s\", s.modName, d)\n\t\t}\n\t}\n\tif len(s.domains) == 0 {\n\t\treturn fmt.Errorf(\"%s: at least one domain is required\", s.modName)\n\t}\n\n\treturn nil\n}\n\nfunc (s *EmailWithDomain) Name() string {\n\treturn s.modName\n}\n\nfunc (s *EmailWithDomain) InstanceName() string {\n\treturn s.modName\n}\n\nfunc (s *EmailWithDomain) Lookup(ctx context.Context, key string) (string, bool, error) {\n\tquotedMbox := address.QuoteMbox(key)\n\n\tif len(s.domains) == 0 {\n\t\ts.log.Msg(\"only first domain is used when expanding key\", \"key\", key, \"domain\", s.domains[0])\n\t}\n\n\treturn quotedMbox + \"@\" + s.domains[0], true, nil\n}\n\nfunc (s *EmailWithDomain) LookupMulti(ctx context.Context, key string) ([]string, error) {\n\tquotedMbox := address.QuoteMbox(key)\n\temails := make([]string, len(s.domains))\n\tfor i, domain := range s.domains {\n\t\temails[i] = quotedMbox + \"@\" + domain\n\t}\n\treturn emails, nil\n}\n\nfunc init() {\n\tmodules.Register(\"table.email_with_domain\", NewEmailWithDomain)\n}\n"
  },
  {
    "path": "internal/table/file.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage table\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"runtime/debug\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/log\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n)\n\nconst FileModName = \"table.file\"\n\ntype File struct {\n\tinstName string\n\tfile     string\n\n\tm      map[string][]string\n\tmLck   sync.RWMutex\n\tmStamp time.Time\n\n\tstopReloader chan struct{}\n\tforceReload  chan struct{}\n\n\tlog *log.Logger\n}\n\nfunc NewFile(c *container.C, modName, instName string) (module.Module, error) {\n\tm := &File{\n\t\tinstName:     instName,\n\t\tm:            make(map[string][]string),\n\t\tstopReloader: make(chan struct{}),\n\t\tforceReload:  make(chan struct{}),\n\t\tlog:          c.DefaultLogger.Sublogger(modName),\n\t}\n\n\treturn m, nil\n}\n\nfunc (f *File) Name() string {\n\treturn FileModName\n}\n\nfunc (f *File) InstanceName() string {\n\treturn f.instName\n}\n\nfunc (f *File) Configure(inlineArgs []string, cfg *config.Map) error {\n\tswitch len(inlineArgs) {\n\tcase 1:\n\t\tf.file = inlineArgs[0]\n\tcase 0:\n\tdefault:\n\t\treturn fmt.Errorf(\"%s: cannot use multiple files with single %s, use %s multiple times to do so\", FileModName, FileModName, FileModName)\n\t}\n\n\tvar file string\n\tcfg.Bool(\"debug\", true, false, &f.log.Debug)\n\tcfg.String(\"file\", false, false, \"\", &file)\n\tif _, err := cfg.Process(); err != nil {\n\t\treturn err\n\t}\n\n\tif file != \"\" {\n\t\tif f.file != \"\" {\n\t\t\treturn fmt.Errorf(\"%s: file path specified both in directive and in argument, do it once\", FileModName)\n\t\t}\n\t\tf.file = file\n\t}\n\n\tif err := readFile(f.file, f.m); err != nil {\n\t\tif !os.IsNotExist(err) {\n\t\t\treturn err\n\t\t}\n\t\tf.log.Printf(\"ignoring non-existent file: %s\", f.file)\n\t}\n\n\treturn nil\n}\n\nfunc (f *File) Start() error {\n\tgo f.reloader()\n\treturn nil\n}\n\nfunc (f *File) Reload() error {\n\tf.forceReload <- struct{}{}\n\treturn nil\n}\n\nfunc (f *File) Stop() error {\n\tf.stopReloader <- struct{}{}\n\t<-f.stopReloader\n\treturn nil\n}\n\nvar reloadInterval = 15 * time.Second\n\nfunc (f *File) reloader() {\n\tdefer func() {\n\t\tif err := recover(); err != nil {\n\t\t\tstack := debug.Stack()\n\t\t\tlog.Printf(\"panic during m reload: %v\\n%s\", err, stack)\n\t\t}\n\t}()\n\n\tt := time.NewTicker(reloadInterval)\n\tdefer t.Stop()\n\n\tfor {\n\t\tselect {\n\t\tcase <-t.C:\n\t\t\tf.reload()\n\n\t\tcase <-f.forceReload:\n\t\t\tf.reload()\n\n\t\tcase <-f.stopReloader:\n\t\t\tf.stopReloader <- struct{}{}\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc (f *File) reload() {\n\tinfo, err := os.Stat(f.file)\n\tif err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\tf.mLck.Lock()\n\t\t\tf.m = map[string][]string{}\n\t\t\tf.mLck.Unlock()\n\t\t\treturn\n\t\t}\n\t\tf.log.Error(\"os stat\", err)\n\t}\n\tif info.ModTime().Before(f.mStamp) || time.Since(info.ModTime()) < (reloadInterval/2) {\n\t\treturn // reload not necessary\n\t}\n\n\tf.log.Debugf(\"reloading\")\n\n\tnewm := make(map[string][]string, len(f.m)+5)\n\tif err := readFile(f.file, newm); err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\tf.log.Printf(\"ignoring non-existent file: %s\", f.file)\n\t\t\treturn\n\t\t}\n\n\t\tf.log.Println(err)\n\t\treturn\n\t}\n\t// after reading we need to check whether file has changed in between\n\tinfo2, err := os.Stat(f.file)\n\tif err != nil {\n\t\tf.log.Println(err)\n\t\treturn\n\t}\n\n\tif !info2.ModTime().Equal(info.ModTime()) {\n\t\t// file has changed in the meantime\n\t\treturn\n\t}\n\n\tf.mLck.Lock()\n\tf.m = newm\n\tf.mStamp = info.ModTime()\n\tf.mLck.Unlock()\n}\n\nfunc readFile(path string, out map[string][]string) error {\n\tf, err := os.Open(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tscnr := bufio.NewScanner(f)\n\tlineCounter := 0\n\n\tparseErr := func(text string) error {\n\t\treturn fmt.Errorf(\"%s:%d: %s\", path, lineCounter, text)\n\t}\n\n\tfor scnr.Scan() {\n\t\tlineCounter++\n\t\tif strings.HasPrefix(scnr.Text(), \"#\") {\n\t\t\tcontinue\n\t\t}\n\n\t\ttext := strings.TrimSpace(scnr.Text())\n\t\tif text == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tparts := strings.SplitN(text, \":\", 2)\n\t\tif len(parts) == 1 {\n\t\t\tparts = append(parts, \"\")\n\t\t}\n\n\t\tfrom := strings.TrimSpace(parts[0])\n\t\tif len(from) == 0 {\n\t\t\treturn parseErr(\"empty address before colon\")\n\t\t}\n\n\t\tfor _, to := range strings.Split(parts[1], \",\") {\n\t\t\tto := strings.TrimSpace(to)\n\t\t\tout[from] = append(out[from], to)\n\t\t}\n\t}\n\treturn scnr.Err()\n}\n\nfunc (f *File) Lookup(_ context.Context, val string) (string, bool, error) {\n\t// The existing map is never modified, instead it is replaced with a new\n\t// one if reload is performed.\n\tf.mLck.RLock()\n\tusedFile := f.m\n\tf.mLck.RUnlock()\n\n\tnewVal, ok := usedFile[val]\n\n\tif len(newVal) == 0 {\n\t\treturn \"\", false, nil\n\t}\n\n\treturn newVal[0], ok, nil\n}\n\nfunc (f *File) LookupMulti(_ context.Context, val string) ([]string, error) {\n\tf.mLck.RLock()\n\tusedFile := f.m\n\tf.mLck.RUnlock()\n\n\treturn usedFile[val], nil\n}\n\nfunc init() {\n\tmodules.Register(FileModName, NewFile)\n}\n"
  },
  {
    "path": "internal/table/file_test.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage table\n\nimport (\n\t\"os\"\n\t\"reflect\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/internal/testutils\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestReadFile(t *testing.T) {\n\ttest := func(file string, expected map[string][]string) {\n\t\tt.Helper()\n\n\t\tf, err := os.CreateTemp(\"\", \"maddy-tests-\")\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tdefer func(name string) {\n\t\t\terr := os.Remove(name)\n\t\t\tif err != nil {\n\t\t\t\tt.Log(err)\n\t\t\t}\n\t\t}(f.Name())\n\t\tdefer func(f *os.File) {\n\t\t\terr := f.Close()\n\t\t\tif err != nil {\n\t\t\t\tt.Log(err)\n\t\t\t}\n\t\t}(f)\n\t\tif _, err := f.WriteString(file); err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tactual := map[string][]string{}\n\t\terr = readFile(f.Name(), actual)\n\t\tif expected == nil {\n\t\t\tif err == nil {\n\t\t\t\tt.Errorf(\"expected failure, got %+v\", actual)\n\t\t\t}\n\t\t\treturn\n\t\t}\n\t\tif err != nil {\n\t\t\tt.Errorf(\"unexpected failure: %v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tif !reflect.DeepEqual(actual, expected) {\n\t\t\tt.Errorf(\"wrong results\\n want %+v\\n got %+v\", expected, actual)\n\t\t}\n\t}\n\n\ttest(\"a: b\", map[string][]string{\"a\": {\"b\"}})\n\ttest(\"a@example.org: b@example.com\", map[string][]string{\"a@example.org\": {\"b@example.com\"}})\n\ttest(`\"a @ a\"@example.org: b@example.com`, map[string][]string{`\"a @ a\"@example.org`: {\"b@example.com\"}})\n\ttest(`a@example.org: \"b @ b\"@example.com`, map[string][]string{`a@example.org`: {`\"b @ b\"@example.com`}})\n\ttest(`\"a @ a\": \"b @ b\"`, map[string][]string{`\"a @ a\"`: {`\"b @ b\"`}})\n\ttest(\"a: b, c\", map[string][]string{\"a\": {\"b\", \"c\"}})\n\ttest(\"a: b\\na: c\", map[string][]string{\"a\": {\"b\", \"c\"}})\n\ttest(\": b\", nil)\n\ttest(\":\", nil)\n\ttest(\"aaa\", map[string][]string{\"aaa\": {\"\"}})\n\ttest(\": b\", nil)\n\ttest(\"     testing@example.com   :  arbitrary-whitespace@example.org   \",\n\t\tmap[string][]string{\"testing@example.com\": {\"arbitrary-whitespace@example.org\"}})\n\ttest(`# skip comments\na: b`, map[string][]string{\"a\": {\"b\"}})\n\ttest(`# and empty lines\n\na: b`, map[string][]string{\"a\": {\"b\"}})\n\ttest(\"# with whitespace too\\n    \\na: b\", map[string][]string{\"a\": {\"b\"}})\n\ttest(\"a: b\\na: c\", map[string][]string{\"a\": {\"b\", \"c\"}})\n}\n\nfunc TestFileReload(t *testing.T) {\n\tt.Parallel()\n\n\tconst file = `cat: dog`\n\n\tf, err := os.CreateTemp(\"\", \"maddy-tests-\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer func(name string) {\n\t\terr := os.Remove(name)\n\t\tif err != nil {\n\t\t\tt.Log(err)\n\t\t}\n\t}(f.Name())\n\tif _, err := f.WriteString(file); err != nil {\n\t\t_ = f.Close()\n\t\tt.Fatal(err)\n\t}\n\terr = f.Close()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tmod, err := NewFile(container.New(), \"\", \"\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tm := mod.(*File)\n\tif err := m.Start(); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tm.log = testutils.Logger(t, \"file_map\")\n\tdefer func() {\n\t\tassert.NoError(t, m.Stop())\n\t}()\n\n\tif err := mod.Configure([]string{f.Name()}, &config.Map{Block: config.Node{}}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// ensure it is correctly loaded at first time.\n\tm.mLck.RLock()\n\tif m.m[\"cat\"] == nil {\n\t\tt.Fatalf(\"wrong content loaded, new m were not loaded, %v\", m.m)\n\t}\n\tm.mLck.RUnlock()\n\n\tfor i := 0; i < 100; i++ {\n\t\t// try to provoke race condition on file writing\n\t\tif i%2 == 0 {\n\t\t\tif err := os.WriteFile(f.Name(), []byte(\"dog: cat\"), os.ModePerm); err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t}\n\t\ttime.Sleep(reloadInterval + 5*time.Millisecond)\n\t\tm.mLck.RLock()\n\t\tif m.m[\"dog\"] == nil {\n\t\t\tt.Fatalf(\"wrong content loaded, new m were not loaded, %v\", m.m)\n\t\t}\n\t\tm.mLck.RUnlock()\n\t}\n}\n\nfunc TestFileReload_Broken(t *testing.T) {\n\tt.Parallel()\n\n\tconst file = `cat: dog`\n\n\tf, err := os.CreateTemp(\"\", \"maddy-tests-\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer func(name string) {\n\t\terr := os.Remove(name)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t}(f.Name())\n\tif _, err := f.WriteString(file); err != nil {\n\t\trequire.NoError(t, f.Close())\n\t\tt.Fatal(err)\n\t}\n\trequire.NoError(t, f.Close())\n\n\tmod, err := NewFile(container.New(), \"\", \"\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tm := mod.(*File)\n\tif err := m.Start(); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tm.log = testutils.Logger(t, FileModName)\n\tdefer func(m *File) {\n\t\terr := m.Stop()\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t}(m)\n\n\tif err := mod.Configure([]string{f.Name()}, &config.Map{Block: config.Node{}}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tf2, err := os.OpenFile(f.Name(), os.O_WRONLY|os.O_SYNC, os.ModePerm)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif _, err := f2.WriteString(\":\"); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer func(f2 *os.File) {\n\t\terr := f2.Close()\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t}(f2)\n\n\ttime.Sleep(3 * reloadInterval)\n\n\tm.mLck.RLock()\n\tdefer m.mLck.RUnlock()\n\tif m.m[\"cat\"] == nil {\n\t\tt.Fatal(\"New m were loaded or map changed\", m.m)\n\t}\n}\n\nfunc TestFileReload_Removed(t *testing.T) {\n\tt.Parallel()\n\n\tconst file = `cat: dog`\n\n\tf, err := os.CreateTemp(\"\", \"maddy-tests-\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif _, err := f.WriteString(file); err != nil {\n\t\t_ = f.Close()\n\t\tt.Fatal(err)\n\t}\n\terr = f.Close()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tmod, err := NewFile(container.New(), \"\", \"\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tm := mod.(*File)\n\tif err := m.Start(); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tm.log = testutils.Logger(t, FileModName)\n\tdefer func(m *File) {\n\t\terr := m.Stop()\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t}(m)\n\n\tif err := mod.Configure([]string{f.Name()}, &config.Map{Block: config.Node{}}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\terr = os.Remove(f.Name())\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\ttime.Sleep(3 * reloadInterval)\n\n\tm.mLck.RLock()\n\tdefer m.mLck.RUnlock()\n\tif m.m[\"cat\"] != nil {\n\t\tt.Fatal(\"Old m are still loaded\")\n\t}\n}\n\nfunc init() {\n\treloadInterval = 10 * time.Millisecond\n}\n"
  },
  {
    "path": "internal/table/identity.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage table\n\nimport (\n\t\"context\"\n\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n)\n\ntype Identity struct {\n\tmodName  string\n\tinstName string\n}\n\nfunc NewIdentity(_ *container.C, modName, instName string) (module.Module, error) {\n\treturn &Identity{\n\t\tmodName:  modName,\n\t\tinstName: instName,\n\t}, nil\n}\n\nfunc (s *Identity) Configure(inlineArgs []string, cfg *config.Map) error {\n\treturn nil\n}\n\nfunc (s *Identity) Name() string {\n\treturn s.modName\n}\n\nfunc (s *Identity) InstanceName() string {\n\treturn s.modName\n}\n\nfunc (s *Identity) Lookup(_ context.Context, key string) (string, bool, error) {\n\treturn key, true, nil\n}\n\nfunc init() {\n\tmodules.Register(\"table.identity\", NewIdentity)\n}\n"
  },
  {
    "path": "internal/table/regexp.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage table\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n)\n\ntype Regexp struct {\n\tmodName  string\n\tinstName string\n\n\tre           *regexp.Regexp\n\treplacements []string\n\n\texpandPlaceholders bool\n}\n\nfunc NewRegexp(_ *container.C, modName, instName string) (module.Module, error) {\n\treturn &Regexp{\n\t\tmodName:  modName,\n\t\tinstName: instName,\n\t}, nil\n}\n\nfunc (r *Regexp) Configure(inlineArgs []string, cfg *config.Map) error {\n\tvar (\n\t\tfullMatch       bool\n\t\tcaseInsensitive bool\n\t)\n\tcfg.Bool(\"full_match\", false, true, &fullMatch)\n\tcfg.Bool(\"case_insensitive\", false, true, &caseInsensitive)\n\tcfg.Bool(\"expand_replaceholders\", false, true, &r.expandPlaceholders)\n\tif _, err := cfg.Process(); err != nil {\n\t\treturn err\n\t}\n\n\tregex := inlineArgs[0]\n\tif len(inlineArgs) > 1 {\n\t\tr.replacements = inlineArgs[1:]\n\t}\n\n\tif fullMatch {\n\t\tif !strings.HasPrefix(regex, \"^\") {\n\t\t\tregex = \"^\" + regex\n\t\t}\n\t\tif !strings.HasSuffix(regex, \"$\") {\n\t\t\tregex = regex + \"$\"\n\t\t}\n\t}\n\n\tif caseInsensitive {\n\t\tregex = \"(?i)\" + regex\n\t}\n\n\tvar err error\n\tr.re, err = regexp.Compile(regex)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"%s: %v\", r.modName, err)\n\t}\n\treturn nil\n}\n\nfunc (r *Regexp) Name() string {\n\treturn r.modName\n}\n\nfunc (r *Regexp) InstanceName() string {\n\treturn r.modName\n}\n\nfunc (r *Regexp) LookupMulti(_ context.Context, key string) ([]string, error) {\n\tmatches := r.re.FindStringSubmatchIndex(key)\n\tif matches == nil {\n\t\treturn []string{}, nil\n\t}\n\n\tresult := []string{}\n\tfor _, replacement := range r.replacements {\n\t\tif !r.expandPlaceholders {\n\t\t\tresult = append(result, replacement)\n\t\t} else {\n\t\t\tresult = append(result, string(r.re.ExpandString([]byte{}, replacement, key, matches)))\n\t\t}\n\t}\n\treturn result, nil\n}\n\nfunc (r *Regexp) Lookup(ctx context.Context, key string) (string, bool, error) {\n\tnewVal, err := r.LookupMulti(ctx, key)\n\tif err != nil {\n\t\treturn \"\", false, err\n\t}\n\tif len(newVal) == 0 {\n\t\treturn \"\", false, nil\n\t}\n\n\treturn newVal[0], true, nil\n}\n\nfunc init() {\n\tmodules.Register(\"table.regexp\", NewRegexp)\n}\n"
  },
  {
    "path": "internal/table/sql_query.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage table\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/log\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n\tsqliteprovider \"github.com/foxcpp/maddy/internal/sqlite\"\n\t_ \"github.com/lib/pq\"\n)\n\ntype SQL struct {\n\tmodName  string\n\tinstName string\n\tprepare  func() error\n\n\tnamedArgs bool\n\n\tdb     *sql.DB\n\tlookup *sql.Stmt\n\tadd    *sql.Stmt\n\tlist   *sql.Stmt\n\tset    *sql.Stmt\n\tdel    *sql.Stmt\n}\n\nfunc NewSQL(_ *container.C, modName, instName string) (module.Module, error) {\n\treturn &SQL{\n\t\tmodName:  modName,\n\t\tinstName: instName,\n\t}, nil\n}\n\nfunc (s *SQL) Name() string {\n\treturn s.modName\n}\n\nfunc (s *SQL) InstanceName() string {\n\treturn s.instName\n}\n\nfunc (s *SQL) Configure(inlineArgs []string, cfg *config.Map) error {\n\tvar (\n\t\tdriver      string\n\t\tinitQueries []string\n\t\tdsnParts    []string\n\t\tlookupQuery string\n\n\t\taddQuery    string\n\t\tlistQuery   string\n\t\tremoveQuery string\n\t\tsetQuery    string\n\t)\n\tcfg.StringList(\"init\", false, false, nil, &initQueries)\n\tcfg.String(\"driver\", false, true, \"\", &driver)\n\tcfg.StringList(\"dsn\", false, true, nil, &dsnParts)\n\tcfg.Bool(\"named_args\", false, false, &s.namedArgs)\n\n\tcfg.String(\"lookup\", false, true, \"\", &lookupQuery)\n\n\tcfg.String(\"add\", false, false, \"\", &addQuery)\n\tcfg.String(\"list\", false, false, \"\", &listQuery)\n\tcfg.String(\"del\", false, false, \"\", &removeQuery)\n\tcfg.String(\"set\", false, false, \"\", &setQuery)\n\tif _, err := cfg.Process(); err != nil {\n\t\treturn err\n\t}\n\n\tif driver == \"postgres\" && s.namedArgs {\n\t\treturn config.NodeErr(cfg.Block, \"PostgreSQL driver does not support named_args\")\n\t}\n\tdriver = sqliteprovider.MapDriverName(driver)\n\n\tdb, err := sql.Open(driver, strings.Join(dsnParts, \" \"))\n\tif err != nil {\n\t\treturn config.NodeErr(cfg.Block, \"failed to open db: %v\", err)\n\t}\n\ts.db = db\n\ts.prepare = func() error {\n\t\tfor _, init := range initQueries {\n\t\t\tif _, err := db.Exec(init); err != nil {\n\t\t\t\treturn config.NodeErr(cfg.Block, \"init query failed: %v\", err)\n\t\t\t}\n\t\t}\n\n\t\ts.lookup, err = db.Prepare(lookupQuery)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to prepare lookup query: %v\", err)\n\t\t}\n\t\tif addQuery != \"\" {\n\t\t\ts.add, err = db.Prepare(addQuery)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to prepare add query: %v\", err)\n\t\t\t}\n\t\t}\n\t\tif listQuery != \"\" {\n\t\t\ts.list, err = db.Prepare(listQuery)\n\t\t\tif err != nil {\n\t\t\t\treturn config.NodeErr(cfg.Block, \"failed to prepare list query: %v\", err)\n\t\t\t}\n\t\t}\n\t\tif setQuery != \"\" {\n\t\t\ts.set, err = db.Prepare(setQuery)\n\t\t\tif err != nil {\n\t\t\t\treturn config.NodeErr(cfg.Block, \"failed to prepare set query: %v\", err)\n\t\t\t}\n\t\t}\n\t\tif removeQuery != \"\" {\n\t\t\ts.del, err = db.Prepare(removeQuery)\n\t\t\tif err != nil {\n\t\t\t\treturn config.NodeErr(cfg.Block, \"failed to prepare del query: %v\", err)\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\n\treturn s.prepare()\n}\n\nfunc (s *SQL) Start() error {\n\treturn nil\n}\n\nfunc (s *SQL) Stop() error {\n\tif err := s.lookup.Close(); err != nil {\n\t\tlog.DefaultLogger.Error(\"lookup query close failed\", err)\n\t}\n\treturn s.db.Close()\n}\n\nfunc (s *SQL) Lookup(ctx context.Context, val string) (string, bool, error) {\n\tvar (\n\t\trepl string\n\t\trow  *sql.Row\n\t)\n\tif s.namedArgs {\n\t\trow = s.lookup.QueryRowContext(ctx, sql.Named(\"key\", val))\n\t} else {\n\t\trow = s.lookup.QueryRowContext(ctx, val)\n\t}\n\tif err := row.Scan(&repl); err != nil {\n\t\tif errors.Is(err, sql.ErrNoRows) {\n\t\t\treturn \"\", false, nil\n\t\t}\n\t\treturn \"\", false, fmt.Errorf(\"%s: lookup %s: %w\", s.modName, val, err)\n\t}\n\treturn repl, true, nil\n}\n\nfunc (s *SQL) LookupMulti(ctx context.Context, val string) ([]string, error) {\n\tvar (\n\t\trepl []string\n\t\trows *sql.Rows\n\t\terr  error\n\t)\n\tif s.namedArgs {\n\t\trows, err = s.lookup.QueryContext(ctx, sql.Named(\"key\", val))\n\t} else {\n\t\trows, err = s.lookup.QueryContext(ctx, val)\n\t}\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"%s; lookup %s: %w\", s.modName, val, err)\n\t}\n\tfor rows.Next() {\n\t\tvar res string\n\t\tif err := rows.Scan(&res); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"%s; lookup %s: %w\", s.modName, val, err)\n\t\t}\n\t\trepl = append(repl, res)\n\t}\n\tif err := rows.Err(); err != nil {\n\t\treturn nil, fmt.Errorf(\"%s; lookup %s: %w\", s.modName, val, err)\n\t}\n\treturn repl, nil\n}\n\nfunc (s *SQL) Keys() ([]string, error) {\n\tif s.list == nil {\n\t\treturn nil, fmt.Errorf(\"%s: table is not mutable (no 'list' query)\", s.modName)\n\t}\n\n\trows, err := s.list.Query()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"%s: list: %w\", s.modName, err)\n\t}\n\tdefer func() {\n\t\t_ = rows.Close()\n\t}()\n\tvar list []string\n\tfor rows.Next() {\n\t\tvar key string\n\t\tif err := rows.Scan(&key); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"%s: list: %w\", s.modName, err)\n\t\t}\n\t\tlist = append(list, key)\n\t}\n\tif err := rows.Err(); err != nil {\n\t\treturn nil, fmt.Errorf(\"%s: list: %w\", s.modName, err)\n\t}\n\treturn list, nil\n}\n\nfunc (s *SQL) RemoveKey(k string) error {\n\tif s.del == nil {\n\t\treturn fmt.Errorf(\"%s: table is not mutable (no 'del' query)\", s.modName)\n\t}\n\n\tvar err error\n\tif s.namedArgs {\n\t\t_, err = s.del.Exec(sql.Named(\"key\", k))\n\t} else {\n\t\t_, err = s.del.Exec(k)\n\t}\n\tif err != nil {\n\t\treturn fmt.Errorf(\"%s: del %s: %w\", s.modName, k, err)\n\t}\n\treturn nil\n}\n\nfunc (s *SQL) SetKey(k, v string) error {\n\tif s.set == nil {\n\t\treturn fmt.Errorf(\"%s: table is not mutable (no 'set' query)\", s.modName)\n\t}\n\tif s.add == nil {\n\t\treturn fmt.Errorf(\"%s: table is not mutable (no 'add' query)\", s.modName)\n\t}\n\n\tvar args []interface{}\n\tif s.namedArgs {\n\t\targs = []interface{}{sql.Named(\"key\", k), sql.Named(\"value\", v)}\n\t} else {\n\t\targs = []interface{}{k, v}\n\t}\n\n\tif _, err := s.add.Exec(args...); err != nil {\n\t\tif _, err := s.set.Exec(args...); err != nil {\n\t\t\treturn fmt.Errorf(\"%s: add %s: %w\", s.modName, k, err)\n\t\t}\n\t\treturn nil\n\t}\n\treturn nil\n}\n\nfunc init() {\n\tmodules.Register(\"table.sql_query\", NewSQL)\n}\n"
  },
  {
    "path": "internal/table/sql_query_test.go",
    "content": "//go:build !nosqlite3 && cgo\n// +build !nosqlite3,cgo\n\n/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage table\n\nimport (\n\t\"context\"\n\t\"path/filepath\"\n\t\"reflect\"\n\t\"testing\"\n\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/internal/testutils\"\n)\n\nfunc TestSQL(t *testing.T) {\n\tpath := testutils.Dir(t)\n\tmod, err := NewSQL(container.New(), \"sql_table\", \"\")\n\tif err != nil {\n\t\tt.Fatal(\"Module create failed:\", err)\n\t}\n\ttbl := mod.(*SQL)\n\terr = tbl.Configure(nil, config.NewMap(nil, config.Node{\n\t\tChildren: []config.Node{\n\t\t\t{\n\t\t\t\tName: \"driver\",\n\t\t\t\tArgs: []string{\"sqlite3\"},\n\t\t\t},\n\t\t\t{\n\t\t\t\tName: \"dsn\",\n\t\t\t\tArgs: []string{filepath.Join(path, \"test.db\")},\n\t\t\t},\n\t\t\t{\n\t\t\t\tName: \"init\",\n\t\t\t\tArgs: []string{\n\t\t\t\t\t\"CREATE TABLE testTbl (key TEXT, value TEXT)\",\n\t\t\t\t\t\"INSERT INTO testTbl VALUES ('user1', 'user1a')\",\n\t\t\t\t\t\"INSERT INTO testTbl VALUES ('user1', 'user1b')\",\n\t\t\t\t\t\"INSERT INTO testTbl VALUES ('user3', NULL)\",\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tName: \"lookup\",\n\t\t\t\tArgs: []string{\"SELECT value FROM testTbl WHERE key = $key\"},\n\t\t\t},\n\t\t},\n\t}))\n\tif err != nil {\n\t\tt.Fatal(\"Init failed:\", err)\n\t}\n\tif err := tbl.Start(); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tcheck := func(key, res string, ok, fail bool) {\n\t\tt.Helper()\n\n\t\tactualRes, actualOk, err := tbl.Lookup(context.Background(), key)\n\t\tif actualRes != res {\n\t\t\tt.Errorf(\"Result mismatch: want %s, got %s\", res, actualRes)\n\t\t}\n\t\tif actualOk != ok {\n\t\t\tt.Errorf(\"OK mismatch: want %v, got %v\", actualOk, ok)\n\t\t}\n\t\tif (err != nil) != fail {\n\t\t\tt.Errorf(\"Error mismatch: want failure = %v, got %v\", fail, err)\n\t\t}\n\t}\n\n\tcheck(\"user1\", \"user1a\", true, false)\n\tcheck(\"user2\", \"\", false, false)\n\tcheck(\"user3\", \"\", false, true)\n\n\tvals, err := tbl.LookupMulti(context.Background(), \"user1\")\n\tif err != nil {\n\t\tt.Error(\"Unexpected error:\", err)\n\t}\n\tif !reflect.DeepEqual(vals, []string{\"user1a\", \"user1b\"}) {\n\t\tt.Error(\"Wrong result of LookupMulti:\", vals)\n\t}\n}\n"
  },
  {
    "path": "internal/table/sql_table.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage table\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n\t_ \"github.com/lib/pq\"\n)\n\ntype SQLTable struct {\n\tmodName  string\n\tinstName string\n\n\twrapped *SQL\n}\n\nfunc NewSQLTable(_ *container.C, modName, instName string) (module.Module, error) {\n\treturn &SQLTable{\n\t\tmodName:  modName,\n\t\tinstName: instName,\n\n\t\twrapped: &SQL{\n\t\t\tmodName:  modName,\n\t\t\tinstName: instName,\n\t\t},\n\t}, nil\n}\n\nfunc (s *SQLTable) Name() string {\n\treturn s.modName\n}\n\nfunc (s *SQLTable) InstanceName() string {\n\treturn s.instName\n}\n\nfunc (s *SQLTable) Configure(inlineArgs []string, cfg *config.Map) error {\n\tvar (\n\t\tdriver      string\n\t\tdsnParts    []string\n\t\ttableName   string\n\t\tkeyColumn   string\n\t\tvalueColumn string\n\t)\n\tcfg.String(\"driver\", false, true, \"\", &driver)\n\tcfg.StringList(\"dsn\", false, true, nil, &dsnParts)\n\tcfg.String(\"table_name\", false, true, \"\", &tableName)\n\tcfg.String(\"key_column\", false, false, \"key\", &keyColumn)\n\tcfg.String(\"value_column\", false, false, \"value\", &valueColumn)\n\tif _, err := cfg.Process(); err != nil {\n\t\treturn err\n\t}\n\n\t// sql_table module literally wraps the sql_query module by generating a\n\t// configuration block for it.\n\n\tvar (\n\t\tuseNamedArgs string\n\n\t\tlookupQuery string\n\t\taddQuery    string\n\t\tlistQuery   string\n\t\tsetQuery    string\n\t\tdelQuery    string\n\t)\n\tif driver == \"sqlite3\" {\n\t\tuseNamedArgs = \"yes\"\n\t\tlookupQuery = fmt.Sprintf(\"SELECT %s FROM %s WHERE %s = :key\", valueColumn, tableName, keyColumn)\n\t\taddQuery = fmt.Sprintf(\"INSERT INTO %s(%s, %s) VALUES(:key, :value)\", tableName, keyColumn, valueColumn)\n\t\tlistQuery = fmt.Sprintf(\"SELECT %s from %s\", keyColumn, tableName)\n\t\tsetQuery = fmt.Sprintf(\"UPDATE %s SET %s = :value WHERE %s = :key\", tableName, valueColumn, keyColumn)\n\t\tdelQuery = fmt.Sprintf(\"DELETE FROM %s WHERE %s = :key\", tableName, keyColumn)\n\t} else {\n\t\tuseNamedArgs = \"no\"\n\t\tlookupQuery = fmt.Sprintf(\"SELECT %s FROM %s WHERE %s = $1\", valueColumn, tableName, keyColumn)\n\t\taddQuery = fmt.Sprintf(\"INSERT INTO %s(%s, %s) VALUES($1, $2)\", tableName, keyColumn, valueColumn)\n\t\tlistQuery = fmt.Sprintf(\"SELECT %s from %s\", keyColumn, tableName)\n\t\tsetQuery = fmt.Sprintf(\"UPDATE %s SET %s = $2 WHERE %s = $1\", tableName, valueColumn, keyColumn)\n\t\tdelQuery = fmt.Sprintf(\"DELETE FROM %s WHERE %s = $1\", tableName, keyColumn)\n\t}\n\n\treturn s.wrapped.Configure(nil, config.NewMap(cfg.Globals, config.Node{\n\t\tChildren: []config.Node{\n\t\t\t{\n\t\t\t\tName: \"driver\",\n\t\t\t\tArgs: []string{driver},\n\t\t\t},\n\t\t\t{\n\t\t\t\tName: \"dsn\",\n\t\t\t\tArgs: dsnParts,\n\t\t\t},\n\t\t\t{\n\t\t\t\tName: \"named_args\",\n\t\t\t\tArgs: []string{useNamedArgs},\n\t\t\t},\n\t\t\t{\n\t\t\t\tName: \"lookup\",\n\t\t\t\tArgs: []string{lookupQuery},\n\t\t\t},\n\t\t\t{\n\t\t\t\tName: \"add\",\n\t\t\t\tArgs: []string{addQuery},\n\t\t\t},\n\t\t\t{\n\t\t\t\tName: \"list\",\n\t\t\t\tArgs: []string{listQuery},\n\t\t\t},\n\t\t\t{\n\t\t\t\tName: \"set\",\n\t\t\t\tArgs: []string{setQuery},\n\t\t\t},\n\t\t\t{\n\t\t\t\tName: \"del\",\n\t\t\t\tArgs: []string{delQuery},\n\t\t\t},\n\t\t\t{\n\t\t\t\tName: \"init\",\n\t\t\t\tArgs: []string{fmt.Sprintf(`CREATE TABLE IF NOT EXISTS %s (\n\t\t\t\t\t%s TEXT PRIMARY KEY NOT NULL,\n\t\t\t\t\t%s TEXT NOT NULL\n\t\t\t\t)`, tableName, keyColumn, valueColumn)},\n\t\t\t},\n\t\t},\n\t}))\n}\n\nfunc (s *SQLTable) Start() error { return s.wrapped.Start() }\n\nfunc (s *SQLTable) Stop() error {\n\treturn s.wrapped.Stop()\n}\n\nfunc (s *SQLTable) Lookup(ctx context.Context, val string) (string, bool, error) {\n\treturn s.wrapped.Lookup(ctx, val)\n}\n\nfunc (s *SQLTable) LookupMulti(ctx context.Context, val string) ([]string, error) {\n\treturn s.wrapped.LookupMulti(ctx, val)\n}\n\nfunc (s *SQLTable) Keys() ([]string, error) {\n\treturn s.wrapped.Keys()\n}\n\nfunc (s *SQLTable) RemoveKey(k string) error {\n\treturn s.wrapped.RemoveKey(k)\n}\n\nfunc (s *SQLTable) SetKey(k, v string) error {\n\treturn s.wrapped.SetKey(k, v)\n}\n\nfunc init() {\n\tmodules.Register(\"table.sql_table\", NewSQLTable)\n}\n"
  },
  {
    "path": "internal/table/static.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage table\n\nimport (\n\t\"context\"\n\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n)\n\ntype Static struct {\n\tmodName  string\n\tinstName string\n\n\tm map[string][]string\n}\n\nfunc NewStatic(_ *container.C, modName, instName string) (module.Module, error) {\n\treturn &Static{\n\t\tmodName:  modName,\n\t\tinstName: instName,\n\t\tm:        map[string][]string{},\n\t}, nil\n}\n\nfunc (s *Static) Configure(inlineArgs []string, cfg *config.Map) error {\n\tcfg.Callback(\"entry\", func(_ *config.Map, node config.Node) error {\n\t\tif len(node.Args) < 2 {\n\t\t\treturn config.NodeErr(node, \"expected at least one value\")\n\t\t}\n\t\ts.m[node.Args[0]] = node.Args[1:]\n\t\treturn nil\n\t})\n\t_, err := cfg.Process()\n\treturn err\n}\n\nfunc (s *Static) Name() string {\n\treturn s.modName\n}\n\nfunc (s *Static) InstanceName() string {\n\treturn s.modName\n}\n\nfunc (s *Static) Lookup(ctx context.Context, key string) (string, bool, error) {\n\tval := s.m[key]\n\tif len(val) == 0 {\n\t\treturn \"\", false, nil\n\t}\n\treturn val[0], true, nil\n}\n\nfunc (s *Static) LookupMulti(ctx context.Context, key string) ([]string, error) {\n\treturn s.m[key], nil\n}\n\nfunc init() {\n\tmodules.Register(\"table.static\", NewStatic)\n}\n"
  },
  {
    "path": "internal/target/delivery.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage target\n\nimport (\n\t\"github.com/foxcpp/maddy/framework/log\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n)\n\nfunc DeliveryLogger(parent *log.Logger, msgMeta *module.MsgMetadata) *log.Logger {\n\tl := parent.Sublogger(\"\")\n\tfields := make(map[string]interface{}, len(l.Fields)+1)\n\tfor k, v := range l.Fields {\n\t\tfields[k] = v\n\t}\n\tfields[\"msg_id\"] = msgMeta.ID\n\tl.Fields = fields\n\treturn l\n}\n"
  },
  {
    "path": "internal/target/queue/metrics.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage queue\n\nimport \"github.com/prometheus/client_golang/prometheus\"\n\nvar queuedMsgs = prometheus.NewGaugeVec(\n\tprometheus.GaugeOpts{\n\t\tNamespace: \"maddy\",\n\t\tSubsystem: \"queue\",\n\t\tName:      \"length\",\n\t\tHelp:      \"Amount of queued messages\",\n\t},\n\t[]string{\"module\", \"location\"},\n)\n\nfunc init() {\n\tprometheus.MustRegister(queuedMsgs)\n}\n"
  },
  {
    "path": "internal/target/queue/queue.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\n/*\nPackage queue implements module which keeps messages on disk and tries delivery\nto the configured target (usually remote) multiple times until all recipients\nare succeeded.\n\nInterfaces implemented:\n- module.DeliveryTarget\n\nImplementation summary follows.\n\nAll scheduled deliveries are attempted to the configured DeliveryTarget.\nAll metadata is preserved on disk.\n\nFailure status is determined on per-recipient basis:\n  - Delivery.StartDelivery fail handled as a failure for all recipients.\n  - Delivery.AddRcpt fail handled as a failure for the corresponding recipient.\n  - Delivery.Body fail handled as a failure for all recipients.\n  - If Delivery implements PartialDelivery, then\n    PartialDelivery.BodyNonAtomic is used instead. Failures are determined based\n    on StatusCollector.SetStatus calls done by target in this case.\n\nFor each failure check is done to see if it is a permanent failure\nor a temporary one. This is done using exterrors.IsTemporaryOrUnspec.\nThat is, errors are assumed to be temporary by default.\nAll errors are converted to SMTPError then due to a storage limitations.\n\nIf there are any *temporary* failed recipients, delivery will be retried\nafter delay *only for these* recipients.\n\nLast error for each recipient is saved for reporting in NDN. A NDN is generated\nif there are any failed recipients left after\nlast attempt to deliver the message.\n\nAmount of attempts for each message is limited to a certain configured number.\nAfter last attempt, all recipients that are still temporary failing are assumed\nto be permanently failed.\n*/\npackage queue\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"math\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"runtime/debug\"\n\t\"runtime/trace\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/emersion/go-message/textproto\"\n\t\"github.com/emersion/go-smtp\"\n\t\"github.com/foxcpp/maddy/framework/buffer\"\n\t\"github.com/foxcpp/maddy/framework/config\"\n\tmodconfig \"github.com/foxcpp/maddy/framework/config/module\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/exterrors\"\n\t\"github.com/foxcpp/maddy/framework/log\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n\t\"github.com/foxcpp/maddy/internal/dsn\"\n\t\"github.com/foxcpp/maddy/internal/msgpipeline\"\n\t\"github.com/foxcpp/maddy/internal/target\"\n)\n\n// partialError describes state of partially successful message delivery.\ntype partialError struct {\n\t// Underlying error objects for each recipient.\n\tErrs map[string]error\n\n\t// Fields can be accessed without holding this lock, but only after\n\t// target.BodyNonAtomic/Body returns.\n\tstatusLock *sync.Mutex\n}\n\n// SetStatus implements module.StatusCollector so partialError can be\n// passed directly to PartialDelivery.BodyNonAtomic.\nfunc (pe *partialError) SetStatus(rcptTo string, err error) {\n\tlog.Debugf(\"PartialError.SetStatus(%s, %v)\", rcptTo, err)\n\tif err == nil {\n\t\treturn\n\t}\n\tpe.statusLock.Lock()\n\tdefer pe.statusLock.Unlock()\n\tpe.Errs[rcptTo] = err\n}\n\nfunc (pe *partialError) Error() string {\n\treturn fmt.Sprintf(\"delivery failed for some recipients: %v\", pe.Errs)\n}\n\n// dontRecover controls the behavior of panic handlers, if it is set to true -\n// they are disabled and so tests will panic to avoid masking bugs.\nvar dontRecover = false\n\ntype Queue struct {\n\tname             string\n\tlocation         string\n\thostname         string\n\tautogenMsgDomain string\n\twheel            *TimeWheel[queueSlot]\n\n\tdsnPipeline module.DeliveryTarget\n\n\t// Retry delay is calculated using the following formula:\n\t// initialRetryTime * retryTimeScale ^ (TriesCount - 1)\n\tinitialRetryTime time.Duration\n\tretryTimeScale   float64\n\tmaxTries         int\n\tmaxParallelism   int\n\n\t// If any delivery is scheduled in less than postInitDelay\n\t// after Init, its delay will be increased by postInitDelay.\n\t//\n\t// Say, if postInitDelay is 10 secs.\n\t// Then if some message is scheduled to delivered 5 seconds\n\t// after init, it will be actually delivered 15 seconds\n\t// after start-up.\n\t//\n\t// This delay is added to make that if maddy is killed shortly\n\t// after start-up for whatever reason it will not affect the queue.\n\tpostInitDelay time.Duration\n\n\tlog    *log.Logger\n\tTarget module.DeliveryTarget\n\n\tdeliveryWg sync.WaitGroup\n\t// Buffered channel used to restrict count of deliveries attempted\n\t// in parallel.\n\tdeliverySemaphore chan struct{}\n}\n\ntype QueueMetadata struct {\n\tMsgMeta *module.MsgMetadata\n\tFrom    string\n\n\t// Recipients that should be tried next.\n\t// May or may not be equal to partialError.TemporaryFailed.\n\tTo []string\n\n\t// Information about previous failures.\n\t// Preserved to be included in a bounce message.\n\tFailedRcpts          []string\n\tTemporaryFailedRcpts []string\n\t// All errors are converted to SMTPError we can serialize and\n\t// also it is directly usable for bounce messages.\n\tRcptErrs map[string]*smtp.SMTPError\n\n\t// Amount of times delivery *already tried*.\n\tTriesCount map[string]int\n\n\tFirstAttempt time.Time\n\tLastAttempt  time.Time\n}\n\ntype queueSlot struct {\n\tID string\n\n\t// If nil - Hdr and Body are invalid, all values should be read from\n\t// disk.\n\tMeta *QueueMetadata\n\tHdr  *textproto.Header\n\tBody buffer.Buffer\n}\n\nfunc New(c *container.C, modName, instName string) (module.Module, error) {\n\tq := &Queue{\n\t\tname:             instName,\n\t\tinitialRetryTime: 15 * time.Minute,\n\t\tretryTimeScale:   1.25,\n\t\tpostInitDelay:    10 * time.Second,\n\t\tlog:              c.DefaultLogger.Sublogger(modName),\n\t}\n\treturn q, nil\n}\n\nfunc (q *Queue) Configure(inlineArgs []string, cfg *config.Map) error {\n\tswitch len(inlineArgs) {\n\tcase 0:\n\t\t// Not inline definition.\n\tcase 1:\n\t\tq.location = inlineArgs[0]\n\tdefault:\n\t\treturn errors.New(\"queue: wrong amount of inline arguments\")\n\t}\n\n\tcfg.Bool(\"debug\", true, false, &q.log.Debug)\n\tcfg.Int(\"max_tries\", false, false, 20, &q.maxTries)\n\tcfg.Int(\"max_parallelism\", false, false, 16, &q.maxParallelism)\n\tcfg.Duration(\"post_init_delay\", false, false, q.postInitDelay, &q.postInitDelay)\n\tcfg.Duration(\"initial_retry_time\", false, false, q.initialRetryTime, &q.initialRetryTime)\n\tcfg.Float(\"retry_time_scale\", false, false, q.retryTimeScale, &q.retryTimeScale)\n\tcfg.String(\"location\", false, false, q.location, &q.location)\n\tcfg.Custom(\"target\", false, true, nil, modconfig.DeliveryDirective, &q.Target)\n\tcfg.String(\"hostname\", true, true, \"\", &q.hostname)\n\tcfg.String(\"autogenerated_msg_domain\", true, false, \"\", &q.autogenMsgDomain)\n\tcfg.Custom(\"bounce\", false, false, nil, func(m *config.Map, node config.Node) (interface{}, error) {\n\t\treturn msgpipeline.New(m.Globals, node.Children)\n\t}, &q.dsnPipeline)\n\tif _, err := cfg.Process(); err != nil {\n\t\treturn err\n\t}\n\n\tif q.dsnPipeline != nil {\n\t\tif q.autogenMsgDomain == \"\" {\n\t\t\treturn errors.New(\"queue: autogenerated_msg_domain is required if bounce {} is specified\")\n\t\t}\n\n\t\tq.dsnPipeline.(*msgpipeline.MsgPipeline).Hostname = q.hostname\n\t\tq.dsnPipeline.(*msgpipeline.MsgPipeline).Log = q.log.Sublogger(\"pipeline\")\n\t}\n\tif q.location == \"\" && q.name == \"\" {\n\t\treturn errors.New(\"queue: need explicit location directive or inline argument if defined inline\")\n\t}\n\tif q.location == \"\" {\n\t\tq.location = filepath.Join(config.StateDirectory, q.name)\n\t}\n\n\t// TODO: Check location write permissions.\n\tif err := os.MkdirAll(q.location, os.ModePerm); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (q *Queue) Start() error {\n\treturn q.start(q.maxParallelism)\n}\n\nfunc (q *Queue) start(maxParallelism int) error {\n\tq.wheel = NewTimeWheel[queueSlot](q.dispatch)\n\tq.deliverySemaphore = make(chan struct{}, maxParallelism)\n\n\tif err := q.readDiskQueue(); err != nil {\n\t\treturn err\n\t}\n\n\tq.log.Debugf(\"delivery target: %T\", q.Target)\n\n\treturn nil\n}\n\nfunc (q *Queue) EarlyStop() error {\n\t// We must ensure queue state is consistent on disk before we proceed\n\t// with configuration reload.\n\tq.wheel.Close()\n\tq.deliveryWg.Wait()\n\treturn nil\n}\n\nfunc (q *Queue) Stop() error {\n\treturn q.EarlyStop()\n}\n\n// discardBroken changes the name of metadata file to have .meta_broken\n// extension.\n//\n// Further attempts to deliver (due to a timewheel) it will fail due to\n// non-existent meta-data file.\n//\n// No error handling is done since this function is called from panic handler.\nfunc (q *Queue) discardBroken(id string) {\n\terr := os.Rename(filepath.Join(q.location, id+\".meta\"), filepath.Join(q.location, id+\".meta_broken\"))\n\tif err != nil {\n\t\t// Note: Global logger is used in case there is something wrong with Queue.Log.\n\t\tlog.Printf(\"can't mark the queue message as broken: %v\", err)\n\t}\n}\n\nfunc (q *Queue) dispatch(ctx context.Context, value TimeSlot[queueSlot]) {\n\tslot := value.Value\n\n\tq.log.Debugln(\"starting delivery for\", slot.ID)\n\n\tq.deliveryWg.Add(1)\n\tgo func() {\n\t\tq.log.Debugln(\"waiting on delivery semaphore for\", slot.ID)\n\t\tq.deliverySemaphore <- struct{}{}\n\t\tdefer func() {\n\t\t\t<-q.deliverySemaphore\n\t\t\tq.deliveryWg.Done()\n\n\t\t\tif dontRecover {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif err := recover(); err != nil {\n\t\t\t\tstack := debug.Stack()\n\t\t\t\tlog.Printf(\"panic during queue dispatch %s: %v\\n%s\", slot.ID, err, stack)\n\t\t\t\tq.discardBroken(slot.ID)\n\t\t\t}\n\t\t}()\n\n\t\tq.log.Debugln(\"delivery semaphore acquired for\", slot.ID)\n\t\tvar (\n\t\t\tmeta *QueueMetadata\n\t\t\thdr  textproto.Header\n\t\t\tbody buffer.Buffer\n\t\t)\n\t\tif slot.Meta == nil {\n\t\t\tvar err error\n\t\t\tmeta, hdr, body, err = q.openMessage(slot.ID)\n\t\t\tif err != nil {\n\t\t\t\tq.log.Error(\"read message\", err, slot.ID)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif meta == nil {\n\t\t\t\tpanic(\"wtf\")\n\t\t\t}\n\t\t} else {\n\t\t\tmeta = slot.Meta\n\t\t\thdr = *slot.Hdr\n\t\t\tbody = slot.Body\n\t\t}\n\n\t\tq.tryDelivery(ctx, meta, hdr, body)\n\t}()\n}\n\nfunc toSMTPErr(err error) *smtp.SMTPError {\n\tif err == nil {\n\t\treturn nil\n\t}\n\n\tres := &smtp.SMTPError{\n\t\tCode:         554,\n\t\tEnhancedCode: smtp.EnhancedCode{5, 0, 0},\n\t\tMessage:      \"Internal server error\",\n\t}\n\n\tif exterrors.IsTemporaryOrUnspec(err) {\n\t\tres.Code = 451\n\t\tres.EnhancedCode = smtp.EnhancedCode{4, 0, 0}\n\t}\n\n\tctxInfo := exterrors.Fields(err)\n\tctxCode, ok := ctxInfo[\"smtp_code\"].(int)\n\tif ok {\n\t\tres.Code = ctxCode\n\t}\n\tctxEnchCode, ok := ctxInfo[\"smtp_enchcode\"].(smtp.EnhancedCode)\n\tif ok {\n\t\tres.EnhancedCode = ctxEnchCode\n\t}\n\tctxMsg, ok := ctxInfo[\"smtp_msg\"].(string)\n\tif ok {\n\t\tres.Message = ctxMsg\n\t}\n\n\tif smtpErr, ok := err.(*smtp.SMTPError); ok {\n\t\tlog.Printf(\"plain SMTP error returned, this is deprecated\")\n\t\tres.Code = smtpErr.Code\n\t\tres.EnhancedCode = smtpErr.EnhancedCode\n\t\tres.Message = smtpErr.Message\n\t}\n\n\treturn res\n}\n\nfunc (q *Queue) tryDelivery(ctx context.Context, meta *QueueMetadata, header textproto.Header, body buffer.Buffer) {\n\tdl := target.DeliveryLogger(q.log, meta.MsgMeta)\n\n\tpartialErr := q.deliver(ctx, meta, header, body)\n\tdl.Debugf(\"errors: %v\", partialErr.Errs)\n\n\t// While iterating the list of recipients we also pick the smallest tries count\n\t// and use it to calculate the delay for the next attempt.\n\tsmallestTriesCount := 999999\n\n\tif meta.TriesCount == nil {\n\t\tmeta.TriesCount = make(map[string]int)\n\t}\n\n\t// Check attempted recipients and corresponding errors.\n\t// Split list into two parts: recipients that should be retried (newRcpts)\n\t// and recipients DSN will be generated for.\n\tnewRcpts := make([]string, 0, len(partialErr.Errs))\n\tfailedRcpts := make([]string, 0, len(partialErr.Errs))\n\tfor _, rcpt := range meta.To {\n\t\trcptErr, ok := partialErr.Errs[rcpt]\n\t\tif !ok {\n\t\t\tdl.Msg(\"delivered\", \"rcpt\", rcpt, \"attempt\", meta.TriesCount[rcpt]+1)\n\t\t\tcontinue\n\t\t}\n\n\t\t// Save last error (either temporary or permanent) for reporting in the DSN.\n\t\tdl.Error(\"delivery attempt failed\", rcptErr, \"rcpt\", rcpt)\n\t\tmeta.RcptErrs[rcpt] = toSMTPErr(rcptErr)\n\n\t\ttemporary := exterrors.IsTemporaryOrUnspec(rcptErr)\n\t\tif !temporary || meta.TriesCount[rcpt]+1 >= q.maxTries {\n\t\t\tdelete(meta.TriesCount, rcpt)\n\t\t\tdl.Msg(\"not delivered, permanent error\", \"rcpt\", rcpt)\n\t\t\tfailedRcpts = append(failedRcpts, rcpt)\n\t\t\tcontinue\n\t\t}\n\n\t\t// Temporary error, increase tries counter and requeue.\n\t\tmeta.TriesCount[rcpt]++\n\t\tnewRcpts = append(newRcpts, rcpt)\n\n\t\t// See smallestTriesCount comment.\n\t\tif count := meta.TriesCount[rcpt]; count < smallestTriesCount {\n\t\t\tsmallestTriesCount = count\n\t\t}\n\t}\n\n\t// Generate DSN for recipients that failed permanently this time.\n\tif len(failedRcpts) != 0 {\n\t\tq.emitDSN(meta, header, failedRcpts)\n\t}\n\t// No recipients to try, either all failed or all succeeded.\n\tif len(newRcpts) == 0 {\n\t\tq.removeFromDisk(meta.MsgMeta)\n\t\treturn\n\t}\n\n\tmeta.To = newRcpts\n\tmeta.LastAttempt = time.Now()\n\n\tif err := q.updateMetadataOnDisk(meta); err != nil {\n\t\tdl.Error(\"meta-data update\", err)\n\t}\n\n\tnextTryTime := time.Now()\n\t// Delay between retries grows exponentally, the formula is:\n\t// initialRetryTime * retryTimeScale ^ (smallestTriesCount - 1)\n\tdl.Debugf(\"delay: %v * %v ^ (%v - 1)\", q.initialRetryTime, q.retryTimeScale, smallestTriesCount)\n\tscaleFactor := time.Duration(math.Pow(q.retryTimeScale, float64(smallestTriesCount-1)))\n\tnextTryTime = nextTryTime.Add(q.initialRetryTime * scaleFactor)\n\tdl.Msg(\"will retry\",\n\t\t\"attempts_count\", meta.TriesCount,\n\t\t\"next_try_delay\", time.Until(nextTryTime),\n\t\t\"rcpts\", meta.To)\n\n\tq.wheel.Add(nextTryTime, queueSlot{\n\t\tID: meta.MsgMeta.ID,\n\n\t\t// Do not keep (meta-)data in memory to reduce usage.  At this point,\n\t\t// it is safe on disk and next try will reread it.\n\t\tMeta: nil,\n\t\tHdr:  nil,\n\t\tBody: nil,\n\t})\n}\n\nfunc (q *Queue) deliver(ctx context.Context, meta *QueueMetadata, header textproto.Header, body buffer.Buffer) partialError {\n\tdl := target.DeliveryLogger(q.log, meta.MsgMeta)\n\tperr := partialError{\n\t\tErrs:       map[string]error{},\n\t\tstatusLock: new(sync.Mutex),\n\t}\n\n\tmsgMeta := meta.MsgMeta.DeepCopy()\n\tmsgMeta.ID = msgMeta.ID + \"-\" + strconv.FormatInt(time.Now().Unix(), 16)\n\tdl.Debugf(\"using message ID = %s\", msgMeta.ID)\n\n\tmsgCtx, msgTask := trace.NewTask(ctx, \"Queue delivery\")\n\tdefer msgTask.End()\n\n\tmailCtx, mailTask := trace.NewTask(msgCtx, \"MAIL FROM\")\n\tdelivery, err := q.Target.StartDelivery(mailCtx, msgMeta, meta.From)\n\tmailTask.End()\n\tif err != nil {\n\t\tdl.Debugf(\"target.StartDelivery failed: %v\", err)\n\t\tfor _, rcpt := range meta.To {\n\t\t\tperr.Errs[rcpt] = err\n\t\t}\n\t\treturn perr\n\t}\n\tdl.Debugf(\"target.StartDelivery OK\")\n\n\t// Check in case delivery implementation is actually\n\t// context-unaware.\n\tif err := mailCtx.Err(); err != nil {\n\t\tfor _, rcpt := range meta.To {\n\t\t\tperr.Errs[rcpt] = err\n\t\t}\n\t\treturn perr\n\t}\n\n\tvar acceptedRcpts []string\n\tfor _, rcpt := range meta.To {\n\t\trcptCtx, rcptTask := trace.NewTask(msgCtx, \"RCPT TO\")\n\t\tif err := delivery.AddRcpt(rcptCtx, rcpt, smtp.RcptOptions{} /* TODO: DSN support */); err != nil {\n\t\t\tdl.Debugf(\"delivery.AddRcpt %s failed: %v\", rcpt, err)\n\t\t\tperr.Errs[rcpt] = err\n\t\t} else {\n\t\t\tdl.Debugf(\"delivery.AddRcpt %s OK\", rcpt)\n\t\t\tacceptedRcpts = append(acceptedRcpts, rcpt)\n\t\t}\n\t\trcptTask.End()\n\n\t\t// Check in case delivery implementation is actually\n\t\t// context-unaware.\n\t\tif err := mailCtx.Err(); err != nil {\n\t\t\tfor _, rcpt := range meta.To {\n\t\t\t\tperr.Errs[rcpt] = err\n\t\t\t}\n\t\t\treturn perr\n\t\t}\n\t}\n\n\tif len(acceptedRcpts) == 0 {\n\t\tdl.Debugf(\"delivery.Abort (no accepted recipients)\")\n\t\tif err := delivery.Abort(msgCtx); err != nil {\n\t\t\tdl.Error(\"delivery.Abort failed\", err)\n\t\t}\n\t\treturn perr\n\t}\n\n\texpandToPartialErr := func(err error) {\n\t\tfor _, rcpt := range acceptedRcpts {\n\t\t\tperr.Errs[rcpt] = err\n\t\t}\n\t}\n\n\t// At this point, it is too late to abort delivery. We should complete\n\t// it or fail it consistently.\n\tmsgCtx = context.WithoutCancel(msgCtx)\n\n\tbodyCtx, bodyTask := trace.NewTask(msgCtx, \"DATA\")\n\tdefer bodyTask.End()\n\n\tpartDelivery, ok := delivery.(module.PartialDelivery)\n\tif ok {\n\t\tdl.Debugf(\"using delivery.BodyNonAtomic\")\n\t\tpartDelivery.BodyNonAtomic(bodyCtx, &perr, header, body)\n\t} else {\n\t\tif err := delivery.Body(bodyCtx, header, body); err != nil {\n\t\t\tdl.Debugf(\"delivery.Body failed: %v\", err)\n\t\t\texpandToPartialErr(err)\n\t\t}\n\t\tdl.Debugf(\"delivery.Body OK\")\n\t}\n\n\tallFailed := true\n\tfor _, rcpt := range acceptedRcpts {\n\t\tif perr.Errs[rcpt] == nil {\n\t\t\tallFailed = false\n\t\t}\n\t}\n\tif allFailed {\n\t\t// No recipients succeeded.\n\t\tdl.Debugf(\"delivery.Abort (all recipients failed)\")\n\t\tif err := delivery.Abort(bodyCtx); err != nil {\n\t\t\tdl.Msg(\"delivery.Abort failed\", err)\n\t\t}\n\t\treturn perr\n\t}\n\n\tif err := delivery.Commit(bodyCtx); err != nil {\n\t\tdl.Debugf(\"delivery.Commit failed: %v\", err)\n\t\texpandToPartialErr(err)\n\t}\n\tdl.Debugf(\"delivery.Commit OK\")\n\n\treturn perr\n}\n\ntype queueDelivery struct {\n\tq    *Queue\n\tmeta *QueueMetadata\n\n\theader textproto.Header\n\tbody   buffer.Buffer\n}\n\nfunc (qd *queueDelivery) AddRcpt(ctx context.Context, rcptTo string, _ smtp.RcptOptions) error {\n\tqd.meta.To = append(qd.meta.To, rcptTo)\n\treturn nil\n}\n\nfunc (qd *queueDelivery) Body(ctx context.Context, header textproto.Header, body buffer.Buffer) error {\n\tdefer trace.StartRegion(ctx, \"queue/Body\").End()\n\n\t// Body buffer initially passed to us may not be valid after \"delivery\" to queue completes.\n\t// storeNewMessage returns a new buffer object created from message blob stored on disk.\n\tstoredBody, err := qd.q.storeNewMessage(qd.meta, header, body)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tqd.body = storedBody\n\tqd.header = header\n\treturn nil\n}\n\nfunc (qd *queueDelivery) Abort(ctx context.Context) error {\n\tdefer trace.StartRegion(ctx, \"queue/Abort\").End()\n\n\tif qd.body != nil {\n\t\tqd.q.removeFromDisk(qd.meta.MsgMeta)\n\t}\n\treturn nil\n}\n\nfunc (qd *queueDelivery) Commit(ctx context.Context) error {\n\tdefer trace.StartRegion(ctx, \"queue/Commit\").End()\n\n\tif qd.meta == nil {\n\t\tpanic(\"queue: double Commit\")\n\t}\n\n\tqd.q.wheel.Add(time.Time{}, queueSlot{\n\t\tID:   qd.meta.MsgMeta.ID,\n\t\tMeta: qd.meta,\n\t\tHdr:  &qd.header,\n\t\tBody: qd.body,\n\t})\n\tqd.meta = nil\n\tqd.body = nil\n\treturn nil\n}\n\nfunc (q *Queue) StartDelivery(ctx context.Context, msgMeta *module.MsgMetadata, mailFrom string) (module.Delivery, error) {\n\tmeta := &QueueMetadata{\n\t\tMsgMeta:      msgMeta,\n\t\tFrom:         mailFrom,\n\t\tRcptErrs:     map[string]*smtp.SMTPError{},\n\t\tFirstAttempt: time.Now(),\n\t\tLastAttempt:  time.Now(),\n\t}\n\treturn &queueDelivery{q: q, meta: meta}, nil\n}\n\nfunc (q *Queue) removeFromDisk(msgMeta *module.MsgMetadata) {\n\tid := msgMeta.ID\n\tdl := target.DeliveryLogger(q.log, msgMeta)\n\n\t// Order is important.\n\t// If we remove header and body but can't remove meta now - readDiskQueue\n\t// will detect and report it.\n\theaderPath := filepath.Join(q.location, id+\".header\")\n\tif err := os.Remove(headerPath); err != nil {\n\t\tdl.Error(\"failed to remove header from disk\", err)\n\t}\n\tbodyPath := filepath.Join(q.location, id+\".body\")\n\tif err := os.Remove(bodyPath); err != nil {\n\t\tdl.Error(\"failed to remove body from disk\", err)\n\t}\n\tmetaPath := filepath.Join(q.location, id+\".meta\")\n\tif err := os.Remove(metaPath); err != nil {\n\t\tdl.Error(\"failed to remove meta-data from disk\", err)\n\t}\n\n\tqueuedMsgs.WithLabelValues(q.name, q.location).Dec()\n\n\tdl.Debugf(\"removed message from disk\")\n}\n\nfunc (q *Queue) readDiskQueue() error {\n\tdirInfo, err := os.ReadDir(q.location)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// TODO(GH #209): Rewrite this function to pass all sub-tests in TestQueueDelivery_DeserializationCleanUp/NoMeta.\n\n\tloadedCount := 0\n\tfor _, entry := range dirInfo {\n\t\t// We start loading from meta-data files and then check whether ID.header and ID.body exist.\n\t\t// This allows us to properly detect dangling body files.\n\t\tif entry.IsDir() || !strings.HasSuffix(entry.Name(), \".meta\") {\n\t\t\tcontinue\n\t\t}\n\t\tid := entry.Name()[:len(entry.Name())-5]\n\n\t\tmeta, err := q.readMessageMeta(id)\n\t\tif err != nil {\n\t\t\tq.log.Printf(\"failed to read meta-data, skipping: %v (msg ID = %s)\", err, id)\n\t\t\tcontinue\n\t\t}\n\n\t\t// Check header file existence.\n\t\tif _, err := os.Stat(filepath.Join(q.location, id+\".header\")); err != nil {\n\t\t\tif os.IsNotExist(err) {\n\t\t\t\tq.log.Printf(\"header file doesn't exist for msg ID = %s\", id)\n\t\t\t\tq.tryRemoveDanglingFile(id + \".meta\")\n\t\t\t\tq.tryRemoveDanglingFile(id + \".body\")\n\t\t\t} else {\n\t\t\t\tq.log.Printf(\"skipping nonstat'able header file: %v (msg ID = %s)\", err, id)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\t// Check body file existence.\n\t\tif _, err := os.Stat(filepath.Join(q.location, id+\".body\")); err != nil {\n\t\t\tif os.IsNotExist(err) {\n\t\t\t\tq.log.Printf(\"body file doesn't exist for msg ID = %s\", id)\n\t\t\t\tq.tryRemoveDanglingFile(id + \".meta\")\n\t\t\t\tq.tryRemoveDanglingFile(id + \".header\")\n\t\t\t} else {\n\t\t\t\tq.log.Printf(\"skipping nonstat'able body file: %v (msg ID = %s)\", err, id)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tsmallestTriesCount := 999999\n\t\tfor _, count := range meta.TriesCount {\n\t\t\tif smallestTriesCount > count {\n\t\t\t\tsmallestTriesCount = count\n\t\t\t}\n\t\t}\n\t\tnextTryTime := meta.LastAttempt\n\t\tscaleFactor := time.Duration(math.Pow(q.retryTimeScale, float64(smallestTriesCount-1)))\n\t\tnextTryTime = nextTryTime.Add(q.initialRetryTime * scaleFactor)\n\n\t\tif time.Until(nextTryTime) < q.postInitDelay {\n\t\t\tnextTryTime = time.Now().Add(q.postInitDelay)\n\t\t}\n\n\t\tq.log.Debugf(\"will try to deliver (msg ID = %s) in %v (%v)\", id, time.Until(nextTryTime), nextTryTime)\n\t\tq.wheel.Add(nextTryTime, queueSlot{\n\t\t\tID: id,\n\t\t})\n\t\tloadedCount++\n\n\t\tqueuedMsgs.WithLabelValues(q.name, q.location).Inc()\n\t}\n\n\tif loadedCount != 0 {\n\t\tq.log.Printf(\"loaded %d saved queue entries\", loadedCount)\n\t}\n\n\treturn nil\n}\n\nfunc (q *Queue) storeNewMessage(meta *QueueMetadata, header textproto.Header, body buffer.Buffer) (buffer.Buffer, error) {\n\tid := meta.MsgMeta.ID\n\n\theaderPath := filepath.Join(q.location, id+\".header\")\n\theaderFile, err := os.Create(headerPath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer func() {\n\t\tif err := headerFile.Close(); err != nil {\n\t\t\tq.log.Error(\"header file close failed\", err)\n\t\t}\n\t}()\n\n\tif err := textproto.WriteHeader(headerFile, header); err != nil {\n\t\tq.tryRemoveDanglingFile(id + \".header\")\n\t\treturn nil, err\n\t}\n\n\tbodyReader, err := body.Open()\n\tif err != nil {\n\t\tq.tryRemoveDanglingFile(id + \".header\")\n\t\treturn nil, err\n\t}\n\tdefer func() {\n\t\tif err := bodyReader.Close(); err != nil {\n\t\t\tq.log.Error(\"bodyReader close failed\", err)\n\t\t}\n\t}()\n\n\tbodyPath := filepath.Join(q.location, id+\".body\")\n\tbodyFile, err := os.Create(bodyPath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer func() {\n\t\tif err := bodyFile.Close(); err != nil {\n\t\t\tq.log.Error(\"body file close failed\", err)\n\t\t}\n\t}()\n\n\tif _, err := io.Copy(bodyFile, bodyReader); err != nil {\n\t\tq.tryRemoveDanglingFile(id + \".body\")\n\t\tq.tryRemoveDanglingFile(id + \".header\")\n\t\treturn nil, err\n\t}\n\n\tif err := q.updateMetadataOnDisk(meta); err != nil {\n\t\tq.tryRemoveDanglingFile(id + \".body\")\n\t\tq.tryRemoveDanglingFile(id + \".header\")\n\t\treturn nil, err\n\t}\n\n\tif err := headerFile.Sync(); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err := bodyFile.Sync(); err != nil {\n\t\treturn nil, err\n\t}\n\n\tqueuedMsgs.WithLabelValues(q.name, q.location).Inc()\n\n\treturn buffer.FileBuffer{Path: bodyPath, LenHint: body.Len()}, nil\n}\n\nfunc (q *Queue) updateMetadataOnDisk(meta *QueueMetadata) error {\n\tmetaPath := filepath.Join(q.location, meta.MsgMeta.ID+\".meta\")\n\n\tvar file *os.File\n\tvar err error\n\tif runtime.GOOS == \"windows\" {\n\t\tfile, err = os.Create(metaPath)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t} else {\n\t\tfile, err = os.Create(metaPath + \".new\")\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tdefer func() {\n\t\tif err := file.Close(); err != nil {\n\t\t\tq.log.Error(\"metadata file close failed\", err)\n\t\t}\n\t}()\n\n\tmetaCopy := *meta\n\tmetaCopy.MsgMeta = meta.MsgMeta.DeepCopy()\n\tmetaCopy.MsgMeta.Conn = nil\n\n\tif err := json.NewEncoder(file).Encode(metaCopy); err != nil {\n\t\treturn err\n\t}\n\n\tif err := file.Sync(); err != nil {\n\t\treturn err\n\t}\n\n\tif runtime.GOOS != \"windows\" {\n\t\tif err := os.Rename(metaPath+\".new\", metaPath); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (q *Queue) readMessageMeta(id string) (*QueueMetadata, error) {\n\tmetaPath := filepath.Join(q.location, id+\".meta\")\n\tfile, err := os.Open(metaPath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer func() {\n\t\tif err := file.Close(); err != nil {\n\t\t\tq.log.Error(\"metadata file close failed\", err)\n\t\t}\n\t}()\n\n\tmeta := &QueueMetadata{}\n\n\tmeta.MsgMeta = &module.MsgMetadata{}\n\n\t// There is a couple of problems we have to solve before we would be able to\n\t// serialize ConnState.\n\t// 1. future.Future can't be serialized.\n\t// 2. net.Addr can't be deserialized because we don't know the concrete type.\n\n\tif err := json.NewDecoder(file).Decode(meta); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn meta, nil\n}\n\ntype BufferedReadCloser struct {\n\t*bufio.Reader\n\tio.Closer\n}\n\nfunc (q *Queue) tryRemoveDanglingFile(name string) {\n\tif err := os.Remove(filepath.Join(q.location, name)); err != nil {\n\t\tq.log.Error(\"dangling file remove failed\", err)\n\t\treturn\n\t}\n\tq.log.Printf(\"removed dangling file %s\", name)\n}\n\nfunc (q *Queue) openMessage(id string) (*QueueMetadata, textproto.Header, buffer.Buffer, error) {\n\tmeta, err := q.readMessageMeta(id)\n\tif err != nil {\n\t\treturn nil, textproto.Header{}, nil, err\n\t}\n\n\tbodyPath := filepath.Join(q.location, id+\".body\")\n\t_, err = os.Stat(bodyPath)\n\tif err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\tq.tryRemoveDanglingFile(id + \".meta\")\n\t\t}\n\t\treturn nil, textproto.Header{}, nil, err\n\t}\n\tbody := buffer.FileBuffer{Path: bodyPath}\n\n\theaderPath := filepath.Join(q.location, id+\".header\")\n\theaderFile, err := os.Open(headerPath)\n\tif err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\tq.tryRemoveDanglingFile(id + \".meta\")\n\t\t\tq.tryRemoveDanglingFile(id + \".body\")\n\t\t}\n\t\treturn nil, textproto.Header{}, nil, err\n\t}\n\n\tbufferedHeader := bufio.NewReader(headerFile)\n\theader, err := textproto.ReadHeader(bufferedHeader)\n\tif err != nil {\n\t\treturn nil, textproto.Header{}, nil, err\n\t}\n\n\treturn meta, header, body, nil\n}\n\nfunc (q *Queue) InstanceName() string {\n\treturn q.name\n}\n\nfunc (q *Queue) Name() string {\n\treturn \"queue\"\n}\n\nfunc (q *Queue) emitDSN(meta *QueueMetadata, header textproto.Header, failedRcpts []string) {\n\t// If, apparently, we have no DSN msgpipeline configured - do nothing.\n\tif q.dsnPipeline == nil {\n\t\treturn\n\t}\n\n\t// Null return-path, used in DSNs.\n\tif meta.MsgMeta.OriginalFrom == \"\" {\n\t\treturn\n\t}\n\n\tdsnID, err := module.GenerateMsgID()\n\tif err != nil {\n\t\tq.log.Error(\"rand.Rand error\", err)\n\t\treturn\n\t}\n\n\tdsnEnvelope := dsn.Envelope{\n\t\tMsgID: \"<\" + dsnID + \"@\" + q.autogenMsgDomain + \">\",\n\t\tFrom:  \"MAILER-DAEMON@\" + q.autogenMsgDomain,\n\t\tTo:    meta.MsgMeta.OriginalFrom,\n\t}\n\tmtaInfo := dsn.ReportingMTAInfo{\n\t\tReportingMTA:    q.hostname,\n\t\tXSender:         meta.From,\n\t\tXMessageID:      meta.MsgMeta.ID,\n\t\tArrivalDate:     meta.FirstAttempt,\n\t\tLastAttemptDate: meta.LastAttempt,\n\t}\n\tif !meta.MsgMeta.DontTraceSender && meta.MsgMeta.Conn != nil {\n\t\tmtaInfo.ReceivedFromMTA = meta.MsgMeta.Conn.Hostname\n\t}\n\n\trcptInfo := make([]dsn.RecipientInfo, 0, len(meta.RcptErrs))\n\tfor _, rcpt := range failedRcpts {\n\t\trcptErr := meta.RcptErrs[rcpt]\n\t\t// rcptErr is stored in RcptErrs using the effective recipient address,\n\t\t// not the original one.\n\n\t\toriginalRcpt := meta.MsgMeta.OriginalRcpts[rcpt]\n\t\tif originalRcpt != \"\" {\n\t\t\trcpt = originalRcpt\n\t\t}\n\n\t\trcptInfo = append(rcptInfo, dsn.RecipientInfo{\n\t\t\tFinalRecipient: rcpt,\n\t\t\tAction:         dsn.ActionFailed,\n\t\t\tStatus:         rcptErr.EnhancedCode,\n\t\t\tDiagnosticCode: rcptErr,\n\t\t})\n\t}\n\n\tvar dsnBodyBlob bytes.Buffer\n\tdl := target.DeliveryLogger(q.log, meta.MsgMeta)\n\tdsnHeader, err := dsn.GenerateDSN(meta.MsgMeta.SMTPOpts.UTF8, dsnEnvelope, mtaInfo, rcptInfo, header, &dsnBodyBlob)\n\tif err != nil {\n\t\tdl.Error(\"failed to generate fail DSN\", err)\n\t\treturn\n\t}\n\tdsnBody := buffer.MemoryBuffer{Slice: dsnBodyBlob.Bytes()}\n\n\tdsnMeta := &module.MsgMetadata{\n\t\tID: dsnID,\n\t\tSMTPOpts: smtp.MailOptions{\n\t\t\tUTF8:       meta.MsgMeta.SMTPOpts.UTF8,\n\t\t\tRequireTLS: meta.MsgMeta.SMTPOpts.RequireTLS,\n\t\t},\n\t}\n\tdl.Msg(\"generated failed DSN\", \"dsn_id\", dsnID)\n\n\tmsgCtx, msgTask := trace.NewTask(context.Background(), \"DSN Delivery\")\n\tdefer msgTask.End()\n\n\tmailCtx, mailTask := trace.NewTask(msgCtx, \"MAIL FROM\")\n\tdsnDelivery, err := q.dsnPipeline.StartDelivery(mailCtx, dsnMeta, \"\")\n\tmailTask.End()\n\tif err != nil {\n\t\tdl.Error(\"failed to enqueue DSN\", err, \"dsn_id\", dsnID)\n\t\treturn\n\t}\n\n\tdefer func() {\n\t\tif err != nil {\n\t\t\tdl.Error(\"failed to enqueue DSN\", err, \"dsn_id\", dsnID)\n\t\t\tif err := dsnDelivery.Abort(msgCtx); err != nil {\n\t\t\t\tdl.Error(\"failed to abort DSN delivery\", err, \"dsn_id\", dsnID)\n\t\t\t}\n\t\t}\n\t}()\n\n\trcptCtx, rcptTask := trace.NewTask(msgCtx, \"RCPT TO\")\n\tif err = dsnDelivery.AddRcpt(rcptCtx, meta.From, smtp.RcptOptions{}); err != nil {\n\t\trcptTask.End()\n\t\treturn\n\t}\n\trcptTask.End()\n\n\tbodyCtx, bodyTask := trace.NewTask(msgCtx, \"DATA\")\n\tif err = dsnDelivery.Body(bodyCtx, dsnHeader, dsnBody); err != nil {\n\t\tbodyTask.End()\n\t\treturn\n\t}\n\tif err = dsnDelivery.Commit(bodyCtx); err != nil {\n\t\tbodyTask.End()\n\t\treturn\n\t}\n\tbodyTask.End()\n}\n\nfunc init() {\n\tmodules.Register(\"target.queue\", New)\n}\n"
  },
  {
    "path": "internal/target/queue/queue_test.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage queue\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/sha1\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"reflect\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/emersion/go-message/textproto\"\n\t\"github.com/emersion/go-smtp\"\n\t\"github.com/foxcpp/maddy/framework/buffer\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/exterrors\"\n\t\"github.com/foxcpp/maddy/framework/log\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/internal/testutils\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// newTestQueue returns properly initialized Queue object usable for testing.\n//\n// See newTestQueueDir to create testing queue from an existing directory.\n// It is called responsibility to remove queue directory created by this function.\nfunc newTestQueue(t *testing.T, target module.DeliveryTarget) *Queue {\n\treturn newTestQueueDir(t, target, t.TempDir())\n}\n\nfunc cleanQueue(t *testing.T, q *Queue) {\n\tt.Log(\"--- queue.Close\")\n\tif err := q.Stop(); err != nil {\n\t\tt.Fatal(\"queue.Close:\", err)\n\t}\n}\n\nfunc newTestQueueDir(t *testing.T, target module.DeliveryTarget, dir string) *Queue {\n\tmod, _ := New(container.New(), \"\", \"queue\")\n\tq := mod.(*Queue)\n\tq.initialRetryTime = 0\n\tq.retryTimeScale = 1\n\tq.postInitDelay = 0\n\tq.maxTries = 5\n\tq.location = dir\n\tq.Target = target\n\n\tif testing.Verbose() {\n\t\tq.log = testutils.Logger(t, \"queue\")\n\t} else {\n\t\tq.log = &log.NopLogger\n\t}\n\n\tif err := q.start(1); err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn q\n}\n\n// unreliableTarget is a module.DeliveryTarget implementation that stores\n// messages to a slice and sometimes fails with the specified error.\ntype unreliableTarget struct {\n\tcommitted chan testutils.Msg\n\taborted   chan testutils.Msg\n\n\t// Amount of completed deliveries (both failed and succeeded)\n\tpassedMessages int\n\n\t// To make unreliableTarget fail Commit for N-th delivery, set N-1-th\n\t// element of this slice to wanted error object. If slice is\n\t// nil/empty or N is bigger than its size - delivery will succeed.\n\tbodyFailures        []error\n\tbodyFailuresPartial []map[string]error\n\trcptFailures        []map[string]error\n}\n\ntype unreliableTargetDelivery struct {\n\tut  *unreliableTarget\n\tmsg testutils.Msg\n}\n\ntype unreliableTargetDeliveryPartial struct {\n\t*unreliableTargetDelivery\n}\n\nfunc (utd *unreliableTargetDelivery) AddRcpt(ctx context.Context, rcptTo string, _ smtp.RcptOptions) error {\n\tif len(utd.ut.rcptFailures) > utd.ut.passedMessages {\n\t\trcptErrs := utd.ut.rcptFailures[utd.ut.passedMessages]\n\t\tif err := rcptErrs[rcptTo]; err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tutd.msg.RcptTo = append(utd.msg.RcptTo, rcptTo)\n\treturn nil\n}\n\nfunc (utd *unreliableTargetDelivery) Body(ctx context.Context, header textproto.Header, body buffer.Buffer) error {\n\tif utd.ut.bodyFailuresPartial != nil {\n\t\treturn errors.New(\"partial failure occurred, no additional information available\")\n\t}\n\n\tr, _ := body.Open()\n\tutd.msg.Body, _ = io.ReadAll(r)\n\n\tif len(utd.ut.bodyFailures) > utd.ut.passedMessages {\n\t\treturn utd.ut.bodyFailures[utd.ut.passedMessages]\n\t}\n\n\treturn nil\n}\n\nfunc (utd *unreliableTargetDeliveryPartial) BodyNonAtomic(ctx context.Context, c module.StatusCollector, header textproto.Header, body buffer.Buffer) {\n\tr, _ := body.Open()\n\tutd.msg.Body, _ = io.ReadAll(r)\n\n\tif len(utd.ut.bodyFailuresPartial) > utd.ut.passedMessages {\n\t\tfor rcpt, err := range utd.ut.bodyFailuresPartial[utd.ut.passedMessages] {\n\t\t\tc.SetStatus(rcpt, err)\n\t\t}\n\t}\n}\n\nfunc (utd *unreliableTargetDelivery) Abort(ctx context.Context) error {\n\tutd.ut.passedMessages++\n\tif utd.ut.aborted != nil {\n\t\tutd.ut.aborted <- utd.msg\n\t}\n\treturn nil\n}\n\nfunc (utd *unreliableTargetDelivery) Commit(ctx context.Context) error {\n\tutd.ut.passedMessages++\n\tif utd.ut.committed != nil {\n\t\tutd.ut.committed <- utd.msg\n\t}\n\treturn nil\n}\n\nfunc (ut *unreliableTarget) StartDelivery(ctx context.Context, msgMeta *module.MsgMetadata, mailFrom string) (module.Delivery, error) {\n\tif ut.bodyFailuresPartial != nil {\n\t\treturn &unreliableTargetDeliveryPartial{\n\t\t\t&unreliableTargetDelivery{\n\t\t\t\tut: ut,\n\t\t\t\tmsg: testutils.Msg{\n\t\t\t\t\tMsgMeta:  msgMeta,\n\t\t\t\t\tMailFrom: mailFrom,\n\t\t\t\t},\n\t\t\t},\n\t\t}, nil\n\t}\n\treturn &unreliableTargetDelivery{\n\t\tut: ut,\n\t\tmsg: testutils.Msg{\n\t\t\tMsgMeta:  msgMeta,\n\t\t\tMailFrom: mailFrom,\n\t\t},\n\t}, nil\n}\n\nfunc readMsgChanTimeout(t *testing.T, ch <-chan testutils.Msg, timeout time.Duration) *testutils.Msg {\n\tt.Helper()\n\ttimer := time.NewTimer(timeout)\n\tselect {\n\tcase msg := <-ch:\n\t\treturn &msg\n\tcase <-timer.C:\n\t\tt.Fatal(\"chan read timed out\")\n\t\treturn nil\n\t}\n}\n\nfunc checkQueueDir(t *testing.T, q *Queue, expectedIDs []string) {\n\tt.Helper()\n\t// We use the map to lookups and also to mark messages we found\n\t// we can report missing entries.\n\texpectedMap := make(map[string]bool, len(expectedIDs))\n\tfor _, id := range expectedIDs {\n\t\texpectedMap[id] = false\n\t}\n\n\tdir, err := os.ReadDir(q.location)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to read queue directory: %v\", err)\n\t}\n\n\t// Queue implementation uses file names in the following format:\n\t// DELIVERY_ID.SOMETHING\n\tfor _, file := range dir {\n\t\tif file.IsDir() {\n\t\t\tt.Fatalf(\"queue should not create subdirectories in the store, but there is %s dir in it\", file.Name())\n\t\t}\n\n\t\tnameParts := strings.Split(file.Name(), \".\")\n\t\tif len(nameParts) != 2 {\n\t\t\tt.Fatalf(\"did the queue files name format changed? got %s\", file.Name())\n\t\t}\n\n\t\t_, ok := expectedMap[nameParts[0]]\n\t\tif !ok {\n\t\t\tt.Errorf(\"message with unexpected Msg ID %s is stored in queue store\", nameParts[0])\n\t\t\tcontinue\n\t\t}\n\n\t\texpectedMap[nameParts[0]] = true\n\t}\n\n\tfor id, found := range expectedMap {\n\t\tif !found {\n\t\t\tt.Errorf(\"expected message with Msg ID %s is missing from queue store\", id)\n\t\t}\n\t}\n}\n\nfunc TestQueueDelivery(t *testing.T) {\n\tt.Parallel()\n\n\tdt := unreliableTarget{committed: make(chan testutils.Msg, 10)}\n\tq := newTestQueue(t, &dt)\n\tdefer cleanQueue(t, q)\n\n\ttestutils.DoTestDelivery(t, q, \"tester@example.com\", []string{\"tester1@example.org\", \"tester2@example.org\"})\n\n\t// Wait for the delivery to complete and stop processing.\n\tmsg := readMsgChanTimeout(t, dt.committed, 5*time.Second)\n\trequire.NoError(t, q.Stop())\n\n\ttestutils.CheckMsgID(t, msg, \"tester@example.com\", []string{\"tester1@example.org\", \"tester2@example.org\"}, \"\")\n\n\t// There should be no queued messages.\n\tcheckQueueDir(t, q, []string{})\n}\n\nfunc TestQueueDelivery_PermanentFail_NonPartial(t *testing.T) {\n\tt.Parallel()\n\n\tdt := unreliableTarget{\n\t\tbodyFailures: []error{\n\t\t\texterrors.WithTemporary(errors.New(\"you shall not pass\"), false),\n\t\t},\n\t\taborted: make(chan testutils.Msg, 10),\n\t}\n\tq := newTestQueue(t, &dt)\n\tdefer cleanQueue(t, q)\n\n\ttestutils.DoTestDelivery(t, q, \"tester@example.com\", []string{\"tester1@example.org\", \"tester2@example.org\"})\n\n\t// Queue will abort a delivery if it fails for all recipients.\n\treadMsgChanTimeout(t, dt.aborted, 5*time.Second)\n\trequire.NoError(t, q.Stop())\n\n\t// Delivery is failed permanently, hence no retry should be rescheduled.\n\tcheckQueueDir(t, q, []string{})\n}\n\nfunc TestQueueDelivery_PermanentFail_Partial(t *testing.T) {\n\tt.Parallel()\n\n\tdt := unreliableTarget{\n\t\tbodyFailuresPartial: []map[string]error{\n\t\t\t{\n\t\t\t\t\"tester1@example.org\": exterrors.WithTemporary(errors.New(\"you shall not pass\"), false),\n\t\t\t\t\"tester2@example.org\": exterrors.WithTemporary(errors.New(\"you shall not pass\"), false),\n\t\t\t},\n\t\t},\n\t\taborted: make(chan testutils.Msg, 10),\n\t}\n\tq := newTestQueue(t, &dt)\n\tdefer cleanQueue(t, q)\n\n\ttestutils.DoTestDelivery(t, q, \"tester@example.com\", []string{\"tester1@example.org\", \"tester2@example.org\"})\n\n\t// This this is similar to the previous test, but checks PartialDelivery processing logic.\n\t// Here delivery fails for recipients too, but this is reported using PartialDelivery.\n\n\treadMsgChanTimeout(t, dt.aborted, 5*time.Second)\n\trequire.NoError(t, q.Stop())\n\tcheckQueueDir(t, q, []string{})\n}\n\nfunc TestQueueDelivery_TemporaryFail(t *testing.T) {\n\tt.Parallel()\n\n\tdt := unreliableTarget{\n\t\tbodyFailures: []error{\n\t\t\texterrors.WithTemporary(errors.New(\"you shall not pass\"), true),\n\t\t},\n\t\taborted:   make(chan testutils.Msg, 10),\n\t\tcommitted: make(chan testutils.Msg, 10),\n\t}\n\tq := newTestQueue(t, &dt)\n\tdefer cleanQueue(t, q)\n\n\ttestutils.DoTestDelivery(t, q, \"tester@example.com\", []string{\"tester1@example.org\", \"tester2@example.org\"})\n\n\t// Delivery should be aborted, because it failed for all recipients.\n\treadMsgChanTimeout(t, dt.aborted, 5*time.Second)\n\n\t// Second retry, should work fine.\n\tmsg := readMsgChanTimeout(t, dt.committed, 5*time.Second)\n\ttestutils.CheckMsgID(t, msg, \"tester@example.com\", []string{\"tester1@example.org\", \"tester2@example.org\"}, \"\")\n\n\trequire.NoError(t, q.Stop())\n\t// No more retries scheduled, queue storage is clear.\n\tdefer checkQueueDir(t, q, []string{})\n}\n\nfunc TestQueueDelivery_TemporaryFail_Partial(t *testing.T) {\n\tt.Parallel()\n\n\tdt := unreliableTarget{\n\t\tbodyFailuresPartial: []map[string]error{\n\t\t\t{\n\t\t\t\t\"tester2@example.org\": exterrors.WithTemporary(errors.New(\"go away\"), true),\n\t\t\t},\n\t\t},\n\t\taborted:   make(chan testutils.Msg, 10),\n\t\tcommitted: make(chan testutils.Msg, 10),\n\t}\n\tq := newTestQueue(t, &dt)\n\tdefer cleanQueue(t, q)\n\n\ttestutils.DoTestDelivery(t, q, \"tester@example.com\", []string{\"tester1@example.org\", \"tester2@example.org\"})\n\n\t// Committed, tester1@example.org - ok.\n\tmsg := readMsgChanTimeout(t, dt.committed, 5000*time.Second)\n\t// Side note: unreliableTarget adds recipients to the msg object even if they were rejected\n\t// later using a partial error. So slice below is all recipients that were submitted by\n\t// the queue.\n\ttestutils.CheckMsgID(t, msg, \"tester@example.com\", []string{\"tester1@example.org\", \"tester2@example.org\"}, \"\")\n\n\t// committed #2, tester2@example.org - ok\n\tmsg = readMsgChanTimeout(t, dt.committed, 5000*time.Second)\n\ttestutils.CheckMsgID(t, msg, \"tester@example.com\", []string{\"tester2@example.org\"}, \"\")\n\n\trequire.NoError(t, q.Stop())\n\t// No more retries scheduled, queue storage is clear.\n\tcheckQueueDir(t, q, []string{})\n}\n\nfunc TestQueueDelivery_MultipleAttempts(t *testing.T) {\n\tt.Parallel()\n\n\tdt := unreliableTarget{\n\t\tbodyFailuresPartial: []map[string]error{\n\t\t\t{\n\t\t\t\t\"tester1@example.org\": exterrors.WithTemporary(errors.New(\"you shall not pass 1\"), false),\n\t\t\t\t\"tester2@example.org\": exterrors.WithTemporary(errors.New(\"you shall not pass 2\"), true),\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"tester2@example.org\": exterrors.WithTemporary(errors.New(\"you shall not pass 3\"), true),\n\t\t\t},\n\t\t},\n\t\tcommitted: make(chan testutils.Msg, 10),\n\t\taborted:   make(chan testutils.Msg, 10),\n\t}\n\tq := newTestQueue(t, &dt)\n\tdefer cleanQueue(t, q)\n\n\ttestutils.DoTestDelivery(t, q, \"tester@example.com\", []string{\"tester1@example.org\", \"tester2@example.org\", \"tester3@example.org\"})\n\n\t// Committed because delivery to tester3@example.org is succeeded.\n\tmsg := readMsgChanTimeout(t, dt.committed, 5*time.Second)\n\t// Side note: This slice contains all recipients submitted by the queue, even if\n\t// they were rejected later using partialError.\n\ttestutils.CheckMsgID(t, msg, \"tester@example.com\", []string{\"tester1@example.org\", \"tester2@example.org\", \"tester3@example.org\"}, \"\")\n\n\t// tester1 is failed permanently, should not be retried.\n\t// tester2 is failed temporary, should be retried.\n\treadMsgChanTimeout(t, dt.aborted, 5*time.Second)\n\n\t// Third attempt... tester2 delivered.\n\tmsg = readMsgChanTimeout(t, dt.committed, 5*time.Second)\n\ttestutils.CheckMsgID(t, msg, \"tester@example.com\", []string{\"tester2@example.org\"}, \"\")\n\n\trequire.NoError(t, q.Stop())\n\t// No more retries should be scheduled.\n\tcheckQueueDir(t, q, []string{})\n}\n\nfunc TestQueueDelivery_PermanentRcptReject(t *testing.T) {\n\tt.Parallel()\n\n\tdt := unreliableTarget{\n\t\trcptFailures: []map[string]error{\n\t\t\t{\n\t\t\t\t\"tester1@example.org\": exterrors.WithTemporary(errors.New(\"go away\"), false),\n\t\t\t},\n\t\t},\n\t\tcommitted: make(chan testutils.Msg, 10),\n\t}\n\tq := newTestQueue(t, &dt)\n\tdefer cleanQueue(t, q)\n\n\ttestutils.DoTestDelivery(t, q, \"tester@example.org\", []string{\"tester1@example.org\", \"tester2@example.org\"})\n\n\t// Committed, tester2@example.org succeeded.\n\tmsg := readMsgChanTimeout(t, dt.committed, 5*time.Second)\n\ttestutils.CheckMsgID(t, msg, \"tester@example.org\", []string{\"tester2@example.org\"}, \"\")\n\n\trequire.NoError(t, q.Stop())\n\t// No more retries should be scheduled.\n\tcheckQueueDir(t, q, []string{})\n}\n\nfunc TestQueueDelivery_TemporaryRcptReject(t *testing.T) {\n\tt.Parallel()\n\n\tdt := unreliableTarget{\n\t\trcptFailures: []map[string]error{\n\t\t\t{\n\t\t\t\t\"tester1@example.org\": exterrors.WithTemporary(errors.New(\"go away\"), true),\n\t\t\t},\n\t\t},\n\t\tcommitted: make(chan testutils.Msg, 10),\n\t}\n\tq := newTestQueue(t, &dt)\n\tdefer cleanQueue(t, q)\n\n\t// First attempt:\n\t//  tester1 - temp. fail\n\t//  tester2 - ok\n\t// Second attempt:\n\t//  tester1 - ok\n\ttestutils.DoTestDelivery(t, q, \"tester@example.com\", []string{\"tester1@example.org\", \"tester2@example.org\"})\n\n\tmsg := readMsgChanTimeout(t, dt.committed, 5*time.Second)\n\t// Unlike previous tests where unreliableTarget rejected recipients by partialError, here they are rejected\n\t// by AddRcpt directly, so they are NOT saved by the target.\n\ttestutils.CheckMsgID(t, msg, \"tester@example.com\", []string{\"tester2@example.org\"}, \"\")\n\n\tmsg = readMsgChanTimeout(t, dt.committed, 5*time.Second)\n\ttestutils.CheckMsgID(t, msg, \"tester@example.com\", []string{\"tester1@example.org\"}, \"\")\n\n\trequire.NoError(t, q.Stop())\n\t// No more retries should be scheduled.\n\tcheckQueueDir(t, q, []string{})\n}\n\nfunc TestQueueDelivery_SerializationRoundtrip(t *testing.T) {\n\tt.Parallel()\n\n\tdt := unreliableTarget{\n\t\trcptFailures: []map[string]error{\n\t\t\t{\n\t\t\t\t\"tester1@example.org\": exterrors.WithTemporary(errors.New(\"go away\"), true),\n\t\t\t},\n\t\t},\n\t\tcommitted: make(chan testutils.Msg, 10),\n\t}\n\tq := newTestQueue(t, &dt)\n\tdefer cleanQueue(t, q)\n\n\t// This is the most tricky test because it is racy and I have no idea what can be done to avoid it.\n\t// It relies on us calling Close before queue msgpipeline decides to retry delivery.\n\t// Hence retry delay is increased from 0ms used in other tests to make it reliable.\n\tq.initialRetryTime = 1 * time.Second\n\n\t// To make sure we will not time out due to post-init delay.\n\tq.postInitDelay = 0\n\n\tdeliveryID := testutils.DoTestDelivery(t, q, \"tester@example.com\", []string{\"tester1@example.org\", \"tester2@example.org\"})\n\n\t// Standard partial delivery, retry will be scheduled for tester1@example.org.\n\tmsg := readMsgChanTimeout(t, dt.committed, 5*time.Second)\n\ttestutils.CheckMsgID(t, msg, \"tester@example.com\", []string{\"tester2@example.org\"}, \"\")\n\n\t// Then stop it.\n\trequire.NoError(t, q.Stop())\n\n\t// Make sure it is saved.\n\tcheckQueueDir(t, q, []string{deliveryID})\n\n\t// Then reinit it.\n\tq = newTestQueueDir(t, &dt, q.location)\n\n\t// Wait for retry and check it.\n\tmsg = readMsgChanTimeout(t, dt.committed, 5*time.Second)\n\ttestutils.CheckMsgID(t, msg, \"tester@example.com\", []string{\"tester1@example.org\"}, \"\")\n\n\t// Close it again.\n\trequire.NoError(t, q.Stop())\n\t// No more retries should be scheduled.\n\tcheckQueueDir(t, q, []string{})\n}\n\nfunc TestQueueDelivery_DeserlizationCleanUp(t *testing.T) {\n\tt.Parallel()\n\n\ttest := func(t *testing.T, fileSuffix string) {\n\t\tdt := unreliableTarget{\n\t\t\trcptFailures: []map[string]error{\n\t\t\t\t{\n\t\t\t\t\t\"tester1@example.org\": exterrors.WithTemporary(errors.New(\"go away\"), true),\n\t\t\t\t},\n\t\t\t},\n\t\t\tcommitted: make(chan testutils.Msg, 10),\n\t\t}\n\t\tq := newTestQueue(t, &dt)\n\t\tdefer cleanQueue(t, q)\n\n\t\t// This is the most tricky test because it is racy and I have no idea what can be done to avoid it.\n\t\t// It relies on us calling Close before queue msgpipeline decides to retry delivery.\n\t\t// Hence retry delay is increased from 0ms used in other tests to make it reliable.\n\t\tq.initialRetryTime = 1 * time.Second\n\n\t\t// To make sure we will not time out due to post-init delay.\n\t\tq.postInitDelay = 0\n\n\t\tdeliveryID := testutils.DoTestDelivery(t, q, \"tester@example.com\", []string{\"tester1@example.org\", \"tester2@example.org\"})\n\n\t\t// Standard partial delivery, retry will be scheduled for tester1@example.org.\n\t\tmsg := readMsgChanTimeout(t, dt.committed, 5*time.Second)\n\t\ttestutils.CheckMsgID(t, msg, \"tester@example.com\", []string{\"tester2@example.org\"}, \"\")\n\n\t\trequire.NoError(t, q.Stop())\n\n\t\tif err := os.Remove(filepath.Join(q.location, deliveryID+fileSuffix)); err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\t// Dangling files should be removed during load.\n\t\tq = newTestQueueDir(t, &dt, q.location)\n\t\trequire.NoError(t, q.Stop())\n\n\t\t// Nothing should be left.\n\t\tcheckQueueDir(t, q, []string{})\n\t}\n\n\tt.Run(\"NoMeta\", func(t *testing.T) {\n\t\tt.Skip(\"Not implemented\")\n\t\ttest(t, \".meta\")\n\t})\n\tt.Run(\"NoBody\", func(t *testing.T) {\n\t\ttest(t, \".body\")\n\t})\n\tt.Run(\"NoHeader\", func(t *testing.T) {\n\t\ttest(t, \".header\")\n\t})\n}\n\nfunc TestQueueDelivery_AbortIfNoRecipients(t *testing.T) {\n\tt.Parallel()\n\n\tdt := unreliableTarget{\n\t\trcptFailures: []map[string]error{\n\t\t\t{\n\t\t\t\t\"tester1@example.org\": exterrors.WithTemporary(errors.New(\"go away\"), true),\n\t\t\t\t\"tester2@example.org\": exterrors.WithTemporary(errors.New(\"go away\"), true),\n\t\t\t},\n\t\t},\n\t\tcommitted: make(chan testutils.Msg, 10),\n\t\taborted:   make(chan testutils.Msg, 10),\n\t}\n\tq := newTestQueue(t, &dt)\n\tdefer cleanQueue(t, q)\n\n\ttestutils.DoTestDelivery(t, q, \"tester@example.com\", []string{\"tester1@example.org\", \"tester2@example.org\"})\n\treadMsgChanTimeout(t, dt.aborted, 5*time.Second)\n}\n\nfunc TestQueueDelivery_AbortNoDangling(t *testing.T) {\n\tt.Parallel()\n\n\tdt := unreliableTarget{\n\t\trcptFailures: []map[string]error{\n\t\t\t{\n\t\t\t\t\"tester1@example.org\": exterrors.WithTemporary(errors.New(\"go away\"), true),\n\t\t\t\t\"tester2@example.org\": exterrors.WithTemporary(errors.New(\"go away\"), true),\n\t\t\t},\n\t\t},\n\t\tcommitted: make(chan testutils.Msg, 10),\n\t\taborted:   make(chan testutils.Msg, 10),\n\t}\n\tq := newTestQueue(t, &dt)\n\tdefer cleanQueue(t, q)\n\n\t// Copied from testutils.DoTestDelivery.\n\tIDRaw := sha1.Sum([]byte(t.Name()))\n\tencodedID := hex.EncodeToString(IDRaw[:])\n\n\tbody := buffer.MemoryBuffer{Slice: []byte(\"foobar\\r\\n\")}\n\tctx := module.MsgMetadata{\n\t\tDontTraceSender: true,\n\t\tID:              encodedID,\n\t}\n\tdelivery, err := q.StartDelivery(context.Background(), &ctx, \"test3@example.org\")\n\tif err != nil {\n\t\tt.Fatalf(\"unexpected StartDelivery err: %v\", err)\n\t}\n\tfor _, rcpt := range [...]string{\"test@example.org\", \"test2@example.org\"} {\n\t\tif err := delivery.AddRcpt(context.Background(), rcpt, smtp.RcptOptions{}); err != nil {\n\t\t\tt.Fatalf(\"unexpected AddRcpt err for %s: %v\", rcpt, err)\n\t\t}\n\t}\n\tif err := delivery.Body(context.Background(), textproto.Header{}, body); err != nil {\n\t\tt.Fatalf(\"unexpected Body err: %v\", err)\n\t}\n\tif err := delivery.Abort(context.Background()); err != nil {\n\t\tt.Fatalf(\"unexpected Abort err: %v\", err)\n\t}\n\n\tcheckQueueDir(t, q, []string{})\n}\n\nfunc TestQueueDSN(t *testing.T) {\n\tt.Parallel()\n\n\tdsnTarget := unreliableTarget{\n\t\tcommitted: make(chan testutils.Msg, 10),\n\t\taborted:   make(chan testutils.Msg, 10),\n\t}\n\n\tdt := unreliableTarget{\n\t\trcptFailures: []map[string]error{\n\t\t\t{\n\t\t\t\t\"tester1@example.org\": exterrors.WithTemporary(errors.New(\"go away\"), false),\n\t\t\t\t\"tester2@example.org\": exterrors.WithTemporary(errors.New(\"go away\"), false),\n\t\t\t},\n\t\t},\n\t\tcommitted: make(chan testutils.Msg, 10),\n\t\taborted:   make(chan testutils.Msg, 10),\n\t}\n\tq := newTestQueue(t, &dt)\n\tq.hostname = \"mx.example.org\"\n\tq.autogenMsgDomain = \"example.org\"\n\tq.dsnPipeline = &dsnTarget\n\tdefer cleanQueue(t, q)\n\n\ttestutils.DoTestDelivery(t, q, \"tester@example.com\", []string{\"tester1@example.org\", \"tester2@example.org\"})\n\n\t// Wait for message delivery attempt to complete (aborted because all recipients fail).\n\treadMsgChanTimeout(t, dt.aborted, 5*time.Second)\n\n\t// Wait for DSN.\n\tmsg := readMsgChanTimeout(t, dsnTarget.committed, 5*time.Second)\n\n\tif msg.MailFrom != \"\" {\n\t\tt.Fatalf(\"wrong MAIL FROM address in DSN: %v\", msg.MailFrom)\n\t}\n\tif !reflect.DeepEqual(msg.RcptTo, []string{\"tester@example.com\"}) {\n\t\tt.Fatalf(\"wrong RCPT TO address in DSN: %v\", msg.RcptTo)\n\t}\n}\n\nfunc TestQueueDSN_FromEmptyAddr(t *testing.T) {\n\tt.Parallel()\n\n\tdsnTarget := unreliableTarget{\n\t\tcommitted: make(chan testutils.Msg, 10),\n\t\taborted:   make(chan testutils.Msg, 10),\n\t}\n\n\tdt := unreliableTarget{\n\t\trcptFailures: []map[string]error{\n\t\t\t{\n\t\t\t\t\"tester1@example.org\": exterrors.WithTemporary(errors.New(\"go away\"), false),\n\t\t\t\t\"tester2@example.org\": exterrors.WithTemporary(errors.New(\"go away\"), false),\n\t\t\t},\n\t\t},\n\t\tcommitted: make(chan testutils.Msg, 10),\n\t\taborted:   make(chan testutils.Msg, 10),\n\t}\n\tq := newTestQueue(t, &dt)\n\tq.hostname = \"mx.example.org\"\n\tq.autogenMsgDomain = \"example.org\"\n\tq.dsnPipeline = &dsnTarget\n\tdefer cleanQueue(t, q)\n\n\ttestutils.DoTestDelivery(t, q, \"\", []string{\"tester1@example.org\", \"tester2@example.org\"})\n\n\t// Wait for message delivery attempt to complete (aborted because all recipients fail).\n\treadMsgChanTimeout(t, dt.aborted, 5*time.Second)\n\n\ttime.Sleep(1 * time.Second)\n\n\t// There should be no DSN for it.\n\tif dsnTarget.passedMessages != 0 {\n\t\tt.Errorf(\"dsnTarget accepted %d messages\", dsnTarget.passedMessages)\n\t}\n\tcheckQueueDir(t, q, []string{})\n}\n\nfunc TestQueueDSN_NoDSNforDSN(t *testing.T) {\n\tt.Parallel()\n\n\tdsnTarget := unreliableTarget{\n\t\trcptFailures: []map[string]error{\n\t\t\t{\n\t\t\t\t\"tester@example.org\": exterrors.WithTemporary(errors.New(\"go away\"), false),\n\t\t\t},\n\t\t},\n\t\tcommitted: make(chan testutils.Msg, 10),\n\t\taborted:   make(chan testutils.Msg, 10),\n\t}\n\n\tdt := unreliableTarget{\n\t\trcptFailures: []map[string]error{\n\t\t\t{\n\t\t\t\t\"tester1@example.org\": exterrors.WithTemporary(errors.New(\"go away\"), false),\n\t\t\t\t\"tester2@example.org\": exterrors.WithTemporary(errors.New(\"go away\"), false),\n\t\t\t},\n\t\t},\n\t\tcommitted: make(chan testutils.Msg, 10),\n\t\taborted:   make(chan testutils.Msg, 10),\n\t}\n\tq := newTestQueue(t, &dt)\n\tq.hostname = \"mx.example.org\"\n\tq.autogenMsgDomain = \"example.org\"\n\tq.dsnPipeline = &dsnTarget\n\tdefer cleanQueue(t, q)\n\n\ttestutils.DoTestDelivery(t, q, \"tester@example.org\", []string{\"tester1@example.org\", \"tester2@example.org\"})\n\n\t// Wait for message delivery attempt to complete (aborted because all recipients fail).\n\treadMsgChanTimeout(t, dt.aborted, 5*time.Second)\n\n\t// DSN will be emitted but will fail, so 'aborted'\n\treadMsgChanTimeout(t, dsnTarget.aborted, 5*time.Second)\n\n\ttime.Sleep(1 * time.Second)\n\n\t// There should be no DSN for DSN (dsnTarget handled one message - the DSN itself).\n\tif dsnTarget.passedMessages != 1 {\n\t\tt.Errorf(\"dsnTarget accepted %d messages\", dsnTarget.passedMessages)\n\t}\n\tcheckQueueDir(t, q, []string{})\n}\n\nfunc TestQueueDSN_RcptRewrite(t *testing.T) {\n\tt.Parallel()\n\n\tdsnTarget := unreliableTarget{\n\t\tcommitted: make(chan testutils.Msg, 10),\n\t\taborted:   make(chan testutils.Msg, 10),\n\t}\n\n\tdt := unreliableTarget{\n\t\trcptFailures: []map[string]error{\n\t\t\t{\n\t\t\t\t\"test@example.org\":  exterrors.WithTemporary(errors.New(\"go away\"), false),\n\t\t\t\t\"test2@example.org\": exterrors.WithTemporary(errors.New(\"go away\"), false),\n\t\t\t},\n\t\t},\n\t\tcommitted: make(chan testutils.Msg, 10),\n\t\taborted:   make(chan testutils.Msg, 10),\n\t}\n\tq := newTestQueue(t, &dt)\n\tq.hostname = \"mx.example.org\"\n\tq.autogenMsgDomain = \"example.org\"\n\tq.dsnPipeline = &dsnTarget\n\tdefer cleanQueue(t, q)\n\n\tIDRaw := sha1.Sum([]byte(t.Name()))\n\tencodedID := hex.EncodeToString(IDRaw[:])\n\n\tbody := buffer.MemoryBuffer{Slice: []byte(\"foobar\\r\\n\")}\n\tctx := module.MsgMetadata{\n\t\tDontTraceSender: true,\n\t\tOriginalFrom:    \"test3@example.org\",\n\t\tOriginalRcpts: map[string]string{\n\t\t\t\"test@example.org\":  \"test+public@example.com\",\n\t\t\t\"test2@example.org\": \"test2+public@example.com\",\n\t\t},\n\t\tID: encodedID,\n\t}\n\tdelivery, err := q.StartDelivery(context.Background(), &ctx, \"test3@example.org\")\n\tif err != nil {\n\t\tt.Fatalf(\"unexpected StartDelivery err: %v\", err)\n\t}\n\tfor _, rcpt := range [...]string{\"test@example.org\", \"test2@example.org\"} {\n\t\tif err := delivery.AddRcpt(context.Background(), rcpt, smtp.RcptOptions{}); err != nil {\n\t\t\tt.Fatalf(\"unexpected AddRcpt err for %s: %v\", rcpt, err)\n\t\t}\n\t}\n\tif err := delivery.Body(context.Background(), textproto.Header{}, body); err != nil {\n\t\tt.Fatalf(\"unexpected Body err: %v\", err)\n\t}\n\tif err := delivery.Commit(context.Background()); err != nil {\n\t\tt.Fatalf(\"unexpected Commit err: %v\", err)\n\t}\n\n\t// Wait for message delivery attempt to complete (aborted because all recipients fail).\n\treadMsgChanTimeout(t, dt.aborted, 5*time.Second)\n\n\t// Wait for DSN.\n\tmsg := readMsgChanTimeout(t, dsnTarget.committed, 5*time.Second)\n\n\tif msg.MailFrom != \"\" {\n\t\tt.Fatalf(\"wrong MAIL FROM address in DSN: %v\", msg.MailFrom)\n\t}\n\tif !reflect.DeepEqual(msg.RcptTo, []string{\"test3@example.org\"}) {\n\t\tt.Fatalf(\"wrong RCPT TO address in DSN: %v\", msg.RcptTo)\n\t}\n\n\tif bytes.Contains(msg.Body, []byte(\"test@example.org\")) || bytes.Contains(msg.Body, []byte(\"test2@example.org\")) {\n\t\tt.Errorf(\"DSN contents mention real final addresses\")\n\t}\n\tif !bytes.Contains(msg.Body, []byte(\"test+public@example.com\")) || !bytes.Contains(msg.Body, []byte(\"test2+public@example.com\")) {\n\t\tt.Errorf(\"DSN contents do not mention original addresses\")\n\t}\n}\n\nfunc init() {\n\tdontRecover = true\n}\n"
  },
  {
    "path": "internal/target/queue/timewheel.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage queue\n\nimport (\n\t\"container/list\"\n\t\"context\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n)\n\ntype TimeSlot[Value any] struct {\n\tTime  time.Time\n\tValue Value\n}\n\ntype TimeWheel[Value any] struct {\n\tstopped uint32\n\n\tslots     *list.List\n\tslotsLock sync.Mutex\n\n\tupdateNotify chan time.Time\n\tstopNotify   chan struct{}\n\ttickerCtx    context.Context\n\ttickerCancel context.CancelFunc\n\n\tdispatch func(context.Context, TimeSlot[Value])\n}\n\nfunc NewTimeWheel[Value any](dispatch func(context.Context, TimeSlot[Value])) *TimeWheel[Value] {\n\tctx, cancel := context.WithCancel(context.Background())\n\n\ttw := &TimeWheel[Value]{\n\t\tslots:        list.New(),\n\t\tstopNotify:   make(chan struct{}),\n\t\ttickerCtx:    ctx,\n\t\ttickerCancel: cancel,\n\t\tupdateNotify: make(chan time.Time),\n\t\tdispatch:     dispatch,\n\t}\n\tgo tw.tick(context.Background())\n\treturn tw\n}\n\nfunc (tw *TimeWheel[Value]) Add(target time.Time, value Value) {\n\tif atomic.LoadUint32(&tw.stopped) == 1 {\n\t\t// Already stopped, ignore.\n\t\treturn\n\t}\n\n\ttw.slotsLock.Lock()\n\ttw.slots.PushBack(TimeSlot[Value]{Time: target, Value: value})\n\ttw.slotsLock.Unlock()\n\n\ttw.updateNotify <- target\n}\n\nfunc (tw *TimeWheel[Value]) Close() {\n\tatomic.StoreUint32(&tw.stopped, 1)\n\n\t// Idempotent Close is convenient sometimes.\n\tif tw.stopNotify == nil {\n\t\treturn\n\t}\n\n\ttw.tickerCancel()\n\n\ttw.stopNotify <- struct{}{}\n\t<-tw.stopNotify\n\n\ttw.stopNotify = nil\n\n\tclose(tw.updateNotify)\n}\n\nfunc (tw *TimeWheel[Value]) tick(ctx context.Context) {\n\tfor {\n\t\tnow := time.Now()\n\t\t// Look for list element closest to now.\n\t\ttw.slotsLock.Lock()\n\t\tvar closestSlot TimeSlot[Value]\n\t\tvar closestEl *list.Element\n\t\tfor e := tw.slots.Front(); e != nil; e = e.Next() {\n\t\t\tslot := e.Value.(TimeSlot[Value])\n\t\t\tif slot.Time.Sub(now) < closestSlot.Time.Sub(now) || closestEl == nil {\n\t\t\t\tclosestSlot = slot\n\t\t\t\tclosestEl = e\n\t\t\t}\n\t\t}\n\t\ttw.slotsLock.Unlock()\n\t\t// Only this goroutine removes elements from TimeWheel so we can be safe using closestSlot.\n\n\t\t// Queue is empty. Just wait until update.\n\t\tif closestEl == nil {\n\t\t\tselect {\n\t\t\tcase <-tw.updateNotify:\n\t\t\t\tcontinue\n\t\t\tcase <-tw.stopNotify:\n\t\t\t\ttw.stopNotify <- struct{}{}\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\t\ttimer := time.NewTimer(closestSlot.Time.Sub(now))\n\n\tselectloop:\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-timer.C:\n\t\t\t\ttw.slotsLock.Lock()\n\t\t\t\ttw.slots.Remove(closestEl)\n\t\t\t\ttw.slotsLock.Unlock()\n\n\t\t\t\ttw.dispatch(ctx, closestSlot)\n\n\t\t\t\tbreak selectloop\n\t\t\tcase newTarget := <-tw.updateNotify:\n\t\t\t\t// Avoid unnecessary restarts if new target is not going to affect our\n\t\t\t\t// current wait time.\n\t\t\t\tif closestSlot.Time.Sub(now) <= newTarget.Sub(now) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\ttimer.Stop()\n\t\t\t\t// Recalculate new slot time.\n\t\t\t\tbreak selectloop\n\t\t\tcase <-tw.stopNotify:\n\t\t\t\ttw.stopNotify <- struct{}{}\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/target/queue/timewheel_test.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage queue\n\nimport (\n\t\"context\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestTimeWheelAdd(t *testing.T) {\n\tt.Parallel()\n\n\tcalled := make(chan TimeSlot[int])\n\n\tw := NewTimeWheel[int](func(ctx context.Context, slot TimeSlot[int]) {\n\t\tcalled <- slot\n\t})\n\tdefer w.Close()\n\n\tw.Add(time.Now().Add(1*time.Second), 1)\n\n\tslot := <-called\n\tif slot.Value != 1 {\n\t\tt.Errorf(\"Wrong slot value: %v\", slot.Value)\n\t}\n}\n\nfunc TestTimeWheelAdd_Ordering(t *testing.T) {\n\tt.Parallel()\n\n\tcalled := make(chan TimeSlot[int])\n\n\tw := NewTimeWheel[int](func(ctx context.Context, slot TimeSlot[int]) {\n\t\tcalled <- slot\n\t})\n\tdefer w.Close()\n\n\tw.Add(time.Now().Add(1*time.Second), 1)\n\tw.Add(time.Now().Add(1250*time.Millisecond), 2)\n\n\tslot := <-called\n\tif slot.Value != 1 {\n\t\tt.Errorf(\"Wrong first slot value: %v\", slot.Value)\n\t}\n\tslot = <-called\n\tif slot.Value != 2 {\n\t\tt.Errorf(\"Wrong second slot value: %v\", slot.Value)\n\t}\n}\n\nfunc TestTimeWheelAdd_Restart(t *testing.T) {\n\tt.Parallel()\n\n\tcalled := make(chan TimeSlot[int])\n\n\tw := NewTimeWheel[int](func(ctx context.Context, slot TimeSlot[int]) {\n\t\tcalled <- slot\n\t})\n\tdefer w.Close()\n\n\tw.Add(time.Now().Add(1*time.Second), 1)\n\tw.Add(time.Now().Add(500*time.Millisecond), 2)\n\n\tslot := <-called\n\tif slot.Value != 2 {\n\t\tt.Errorf(\"Wrong first slot value: %v\", slot.Value)\n\t}\n\tslot = <-called\n\tif slot.Value != 1 {\n\t\tt.Errorf(\"Wrong second slot value: %v\", slot.Value)\n\t}\n}\n\nfunc TestTimeWheelAdd_MissingGotoBug(t *testing.T) {\n\tt.Parallel()\n\n\tcalled := make(chan TimeSlot[int])\n\n\tw := NewTimeWheel[int](func(ctx context.Context, slot TimeSlot[int]) {\n\t\tcalled <- slot\n\t})\n\tdefer w.Close()\n\n\tw.Add(time.Now().Add(90000*time.Hour), 1)      // practically newer\n\tw.Add(time.Now().Add(500*time.Millisecond), 2) // should correctly restart\n\n\tslot := <-called\n\tif slot.Value != 2 {\n\t\tt.Errorf(\"Wrong first slot value: %v\", slot.Value)\n\t}\n}\n\nfunc TestTimeWheelAdd_EmptyUpdWait(t *testing.T) {\n\tt.Parallel()\n\n\tcalled := make(chan TimeSlot[int])\n\n\tw := NewTimeWheel[int](func(ctx context.Context, slot TimeSlot[int]) {\n\t\tcalled <- slot\n\t})\n\tdefer w.Close()\n\n\ttime.Sleep(500 * time.Millisecond)\n\n\tw.Add(time.Now().Add(1*time.Second), 1)\n\n\tslot := <-called\n\tif slot.Value != 1 {\n\t\tt.Errorf(\"Wrong slot value: %v\", slot.Value)\n\t}\n}\n"
  },
  {
    "path": "internal/target/received.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage target\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"net\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/foxcpp/maddy/framework/address\"\n\t\"github.com/foxcpp/maddy/framework/dns\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n)\n\nfunc SanitizeForHeader(raw string) string {\n\treturn strings.ReplaceAll(raw, \"\\n\", \"\")\n}\n\nfunc GenerateReceived(ctx context.Context, msgMeta *module.MsgMetadata, ourHostname, mailFrom string) (string, error) {\n\tif msgMeta.Conn == nil {\n\t\treturn \"\", errors.New(\"can't generate Received for a locally generated message\")\n\t}\n\n\tbuilder := strings.Builder{}\n\n\t// Empirically guessed value that should be enough to fit\n\t// the entire value in most cases.\n\tbuilder.Grow(256 + len(msgMeta.Conn.Hostname))\n\n\tif !msgMeta.DontTraceSender && (strings.Contains(msgMeta.Conn.Proto, \"SMTP\") ||\n\t\tstrings.Contains(msgMeta.Conn.Proto, \"LMTP\")) {\n\t\t// INTERNATIONALIZATION: See RFC 6531 Section 3.7.3.\n\t\thostname, err := dns.SelectIDNA(msgMeta.SMTPOpts.UTF8, msgMeta.Conn.Hostname)\n\t\tif err == nil {\n\t\t\tbuilder.WriteString(\"from \")\n\t\t\tbuilder.WriteString(hostname)\n\t\t}\n\n\t\tif tcpAddr, ok := msgMeta.Conn.RemoteAddr.(*net.TCPAddr); ok {\n\t\t\tbuilder.WriteString(\" (\")\n\t\t\tif msgMeta.Conn.RDNSName != nil {\n\t\t\t\trdnsName, err := msgMeta.Conn.RDNSName.GetContext(ctx)\n\t\t\t\tif err == nil && rdnsName != nil && rdnsName.(string) != \"\" {\n\t\t\t\t\t// INTERNATIONALIZATION: See RFC 6531 Section 3.7.3.\n\t\t\t\t\tencoded, err := dns.SelectIDNA(msgMeta.SMTPOpts.UTF8, rdnsName.(string))\n\t\t\t\t\tif err == nil {\n\t\t\t\t\t\tbuilder.WriteString(encoded)\n\t\t\t\t\t\tbuilder.WriteRune(' ')\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tbuilder.WriteRune('[')\n\t\t\tbuilder.WriteString(tcpAddr.IP.String())\n\t\t\tbuilder.WriteString(\"])\")\n\t\t}\n\t}\n\n\tif ourHostname != \"\" {\n\t\tourHostname, err := dns.SelectIDNA(msgMeta.SMTPOpts.UTF8, ourHostname)\n\t\tif err == nil {\n\t\t\tbuilder.WriteString(\" by \")\n\t\t\tbuilder.WriteString(SanitizeForHeader(ourHostname))\n\t\t}\n\t}\n\n\tif mailFrom != \"\" {\n\t\t// INTERNATIONALIZATION: See RFC 6531 Section 3.7.3.\n\t\tmailFrom, err := address.SelectIDNA(msgMeta.SMTPOpts.UTF8, mailFrom)\n\t\tif err == nil {\n\t\t\tbuilder.WriteString(\" (envelope-sender <\")\n\t\t\tbuilder.WriteString(SanitizeForHeader(mailFrom))\n\t\t\tbuilder.WriteString(\">)\")\n\t\t}\n\t}\n\n\tif msgMeta.Conn.Proto != \"\" {\n\t\tbuilder.WriteString(\" with \")\n\t\tif msgMeta.SMTPOpts.UTF8 {\n\t\t\tbuilder.WriteString(\"UTF8\")\n\t\t}\n\t\tbuilder.WriteString(msgMeta.Conn.Proto)\n\t}\n\tbuilder.WriteString(\" id \")\n\tbuilder.WriteString(msgMeta.ID)\n\tbuilder.WriteString(\"; \")\n\tbuilder.WriteString(time.Now().Format(time.RFC1123Z))\n\n\treturn strings.TrimSpace(builder.String()), nil\n}\n"
  },
  {
    "path": "internal/target/remote/connect.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage remote\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"net\"\n\t\"runtime/trace\"\n\t\"sort\"\n\t\"time\"\n\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/dns\"\n\t\"github.com/foxcpp/maddy/framework/exterrors\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/internal/smtpconn\"\n)\n\ntype mxConn struct {\n\t*smtpconn.C\n\n\t// Domain this MX belongs to.\n\tdomain   string\n\tdnssecOk bool\n\n\t// Errors occurred previously on this connection.\n\terrored bool\n\n\treuseLimit int\n\n\t// Amount of times connection was used for an SMTP transaction.\n\ttransactions int\n\tlastUseAt    time.Time\n\n\t// MX/TLS security level established for this connection.\n\tmxLevel  module.MXLevel\n\ttlsLevel module.TLSLevel\n}\n\nfunc (c *mxConn) Usable() bool {\n\tif c.C == nil || c.transactions > c.reuseLimit || c.Client() == nil || c.errored {\n\t\treturn false\n\t}\n\treturn c.C.Client().Reset() == nil\n}\n\nfunc (c *mxConn) LastUseAt() time.Time {\n\treturn c.lastUseAt\n}\n\nfunc (c *mxConn) Close() error {\n\treturn c.C.Close()\n}\n\nfunc isVerifyError(err error) bool {\n\tvar e *tls.CertificateVerificationError\n\treturn errors.As(err, &e)\n}\n\n// connect attempts to connect to the MX, first trying STARTTLS with X.509\n// verification but falling back to unauthenticated TLS or plaintext as\n// necessary.\n//\n// Return values:\n// - tlsLevel    TLS security level that was estabilished.\n// - tlsErr      Error that prevented TLS from working if tlsLevel != TLSAuthenticated\nfunc (rd *remoteDelivery) connect(ctx context.Context, conn mxConn, host string, tlsCfg *tls.Config) (tlsLevel module.TLSLevel, tlsErr, err error) {\n\ttlsLevel = module.TLSAuthenticated\n\tif rd.rt.tlsConfig != nil {\n\t\ttlsCfg = rd.rt.tlsConfig.Clone()\n\t\ttlsCfg.ServerName = host\n\t}\n\n\trd.log.DebugMsg(\"trying\", \"remote_server\", host, \"domain\", conn.domain)\n\nretry:\n\t// smtpconn.C default TLS behavior is not useful for us, we want to handle\n\t// TLS errors separately hence starttls=false.\n\t_, err = conn.Connect(ctx, config.Endpoint{\n\t\tHost: host,\n\t\tPort: smtpPort,\n\t}, false, nil)\n\tif err != nil {\n\t\treturn module.TLSNone, nil, err\n\t}\n\n\tstarttlsOk, _ := conn.Client().Extension(\"STARTTLS\")\n\tif starttlsOk && tlsCfg != nil {\n\t\tif err := conn.Client().StartTLS(tlsCfg); err != nil {\n\t\t\t// Here we just issue STARTTLS command. If it fails for some\n\t\t\t// reason - this is either a connection problem or server actively\n\t\t\t// rejecting STARTTLS (despite advertising STARTTLS).\n\t\t\t// We err on the caution side here and do not perform any fallbacks.\n\t\t\tif err := conn.DirectClose(); err != nil {\n\t\t\t\trd.log.Error(\"conn.DirectClose failed\", err)\n\t\t\t}\n\t\t\treturn module.TLSNone, nil, err\n\t\t}\n\n\t\t// TLS handshake is deferred to here, this is where we check errors and allow fallback.\n\t\tif err := conn.Client().Hello(rd.rt.hostname); err != nil {\n\t\t\ttlsErr = err\n\n\t\t\t// Attempt TLS without authentication. It is still better than\n\t\t\t// plaintext and we might be able to actually authenticate the\n\t\t\t// server using DANE-EE/DANE-TA later.\n\t\t\t//\n\t\t\t// Check tlsLevel is to avoid looping forever if the same verify\n\t\t\t// error happens with InsecureSkipVerify too (e.g. certificate is\n\t\t\t// *too* broken).\n\t\t\tif isVerifyError(err) && tlsLevel == module.TLSAuthenticated {\n\t\t\t\trd.log.Error(\"TLS verify error, trying without authentication\", err, \"remote_server\", host, \"domain\", conn.domain)\n\t\t\t\ttlsCfg.InsecureSkipVerify = true\n\t\t\t\ttlsLevel = module.TLSEncrypted\n\n\t\t\t\t// TODO: Check go-smtp code to make TLS verification errors\n\t\t\t\t// non-sticky so we can properly send QUIT in this case.\n\t\t\t\tif err := conn.DirectClose(); err != nil {\n\t\t\t\t\trd.log.Error(\"conn.DirectClose failed\", err)\n\t\t\t\t}\n\n\t\t\t\tgoto retry\n\t\t\t}\n\n\t\t\trd.log.Error(\"TLS error, trying plaintext\", err, \"remote_server\", host, \"domain\", conn.domain)\n\t\t\ttlsCfg = nil\n\t\t\ttlsLevel = module.TLSNone\n\t\t\tif err := conn.DirectClose(); err != nil {\n\t\t\t\trd.log.Error(\"conn.DirectClose failed\", err)\n\t\t\t}\n\n\t\t\tgoto retry\n\t\t}\n\t} else {\n\t\ttlsLevel = module.TLSNone\n\t}\n\n\treturn tlsLevel, tlsErr, nil\n}\n\nfunc (rd *remoteDelivery) attemptMX(ctx context.Context, conn *mxConn, record *net.MX) error {\n\tmxLevel := module.MXNone\n\n\tconnCtx, cancel := context.WithCancel(ctx)\n\t// Cancel async policy lookups if rd.connect fails.\n\tdefer cancel()\n\n\tfor _, p := range rd.policies {\n\t\tpolicyLevel, err := p.CheckMX(connCtx, mxLevel, conn.domain, record.Host, conn.dnssecOk)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif policyLevel > mxLevel {\n\t\t\tmxLevel = policyLevel\n\t\t}\n\n\t\tp.PrepareConn(ctx, record.Host)\n\t}\n\n\ttlsLevel, tlsErr, err := rd.connect(connCtx, *conn, record.Host, rd.rt.tlsConfig)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Make decision based on the policy and connection state.\n\t//\n\t// Note: All policy errors are marked as temporary to give the local admin\n\t// chance to troubleshoot them without losing messages.\n\n\ttlsState, _ := conn.Client().TLSConnectionState()\n\tfor _, p := range rd.policies {\n\t\tpolicyLevel, err := p.CheckConn(connCtx, mxLevel, tlsLevel, conn.domain, record.Host, tlsState)\n\t\tif err != nil {\n\t\t\trd.closeConn(conn)\n\t\t\treturn exterrors.WithFields(err, map[string]interface{}{\"tls_err\": tlsErr})\n\t\t}\n\t\tif policyLevel > tlsLevel {\n\t\t\ttlsLevel = policyLevel\n\t\t}\n\t}\n\n\tconn.mxLevel = mxLevel\n\tconn.tlsLevel = tlsLevel\n\n\tmxLevelCnt.WithLabelValues(rd.rt.Name(), mxLevel.String()).Inc()\n\ttlsLevelCnt.WithLabelValues(rd.rt.Name(), tlsLevel.String()).Inc()\n\n\treturn nil\n}\n\nfunc (rd *remoteDelivery) closeConn(c *mxConn) {\n\tif err := c.Close(); err != nil {\n\t\trd.log.Error(\"client connection close failed\", err)\n\t}\n}\n\nfunc (rd *remoteDelivery) connectionForDomain(ctx context.Context, domain string) (*mxConn, error) {\n\tif c, ok := rd.connections[domain]; ok {\n\t\treturn c, nil\n\t}\n\n\tpooledConn, err := rd.rt.pool.Get(ctx, domain)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar conn *mxConn\n\t// Ignore pool for connections with REQUIRETLS to avoid \"pool poisoning\"\n\t// where attacker can make messages indeliverable by forcing reuse of old\n\t// connection with weaker security.\n\tif pooledConn != nil && !rd.msgMeta.SMTPOpts.RequireTLS {\n\t\tconn = pooledConn.(*mxConn)\n\t\trd.log.Msg(\"reusing cached connection\", \"domain\", domain, \"transactions_counter\", conn.transactions,\n\t\t\t\"local_addr\", conn.LocalAddr(), \"remote_addr\", conn.RemoteAddr())\n\t} else {\n\t\trd.log.DebugMsg(\"opening new connection\", \"domain\", domain, \"cache_ignored\", pooledConn != nil)\n\t\tconn, err = rd.newConn(ctx, domain)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tif rd.msgMeta.SMTPOpts.RequireTLS {\n\t\tif conn.tlsLevel < module.TLSAuthenticated {\n\t\t\trd.closeConn(conn)\n\t\t\treturn nil, &exterrors.SMTPError{\n\t\t\t\tCode:         550,\n\t\t\t\tEnhancedCode: exterrors.EnhancedCode{5, 7, 30},\n\t\t\t\tMessage:      \"TLS it not available or unauthenticated but required (REQUIRETLS)\",\n\t\t\t\tMisc: map[string]interface{}{\n\t\t\t\t\t\"tls_level\": conn.tlsLevel,\n\t\t\t\t},\n\t\t\t}\n\t\t}\n\t\tif conn.mxLevel < module.MX_MTASTS {\n\t\t\trd.closeConn(conn)\n\t\t\treturn nil, &exterrors.SMTPError{\n\t\t\t\tCode:         550,\n\t\t\t\tEnhancedCode: exterrors.EnhancedCode{5, 7, 30},\n\t\t\t\tMessage:      \"Failed to establish the MX record authenticity (REQUIRETLS)\",\n\t\t\t\tMisc: map[string]interface{}{\n\t\t\t\t\t\"mx_level\": conn.mxLevel,\n\t\t\t\t},\n\t\t\t}\n\t\t}\n\t}\n\n\tregion := trace.StartRegion(ctx, \"remote/limits.TakeDest\")\n\tif err := rd.rt.limits.TakeDest(ctx, domain); err != nil {\n\t\tregion.End()\n\t\trd.closeConn(conn)\n\t\treturn nil, err\n\t}\n\tregion.End()\n\n\t// Relaxed REQUIRETLS mode is not conforming to the specification strictly\n\t// but allows to start deploying client support for REQUIRETLS without the\n\t// requirement for servers in the whole world to support it. The assumption\n\t// behind it is that MX for the recipient domain is the final destination\n\t// and all other forwarders behind it already have secure connection to\n\t// each other. Therefore it is enough to enforce strict security only on\n\t// the path to the MX even if it does not support the REQUIRETLS to propagate\n\t// this requirement further.\n\tif ok, _ := conn.Client().Extension(\"REQUIRETLS\"); rd.rt.relaxedREQUIRETLS && !ok {\n\t\trd.msgMeta.SMTPOpts.RequireTLS = false\n\t}\n\n\tif err := conn.Mail(ctx, rd.mailFrom, rd.msgMeta.SMTPOpts); err != nil {\n\t\trd.closeConn(conn)\n\t\treturn nil, err\n\t}\n\tconn.lastUseAt = time.Now()\n\n\trd.connections[domain] = conn\n\treturn conn, nil\n}\n\nfunc (rd *remoteDelivery) newConn(ctx context.Context, domain string) (*mxConn, error) {\n\tconn := mxConn{\n\t\treuseLimit: rd.rt.connReuseLimit,\n\t\tC:          smtpconn.New(),\n\t\tdomain:     domain,\n\t\tlastUseAt:  time.Now(),\n\t}\n\n\tconn.Dialer = rd.rt.dialer\n\tconn.Log = rd.log\n\tconn.Hostname = rd.rt.hostname\n\tconn.AddrInSMTPMsg = true\n\tif rd.rt.connectTimeout != 0 {\n\t\tconn.ConnectTimeout = rd.rt.connectTimeout\n\t}\n\tif rd.rt.commandTimeout != 0 {\n\t\tconn.CommandTimeout = rd.rt.commandTimeout\n\t}\n\tif rd.rt.submissionTimeout != 0 {\n\t\tconn.SubmissionTimeout = rd.rt.submissionTimeout\n\t}\n\n\tfor _, p := range rd.policies {\n\t\tp.PrepareDomain(ctx, domain)\n\t}\n\n\tregion := trace.StartRegion(ctx, \"remote/LookupMX\")\n\tdnssecOk, records, err := rd.lookupMX(ctx, domain)\n\tregion.End()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tconn.dnssecOk = dnssecOk\n\n\tvar lastErr error\n\tregion = trace.StartRegion(ctx, \"remote/Connect+TLS\")\n\tfor _, record := range records {\n\t\tif record.Host == \".\" {\n\t\t\treturn nil, &exterrors.SMTPError{\n\t\t\t\tCode:         556,\n\t\t\t\tEnhancedCode: exterrors.EnhancedCode{5, 1, 10},\n\t\t\t\tMessage:      \"Domain does not accept email (null MX)\",\n\t\t\t}\n\t\t}\n\n\t\tif err := rd.attemptMX(ctx, &conn, record); err != nil {\n\t\t\tif len(records) != 0 {\n\t\t\t\trd.log.Error(\"cannot use MX\", err, \"remote_server\", record.Host, \"domain\", domain)\n\t\t\t}\n\t\t\tlastErr = err\n\t\t\tcontinue\n\t\t}\n\t\tbreak\n\t}\n\tregion.End()\n\n\t// Still not connected? Bail out.\n\tif conn.Client() == nil {\n\t\treturn nil, &exterrors.SMTPError{\n\t\t\tCode:         exterrors.SMTPCode(lastErr, 451, 550),\n\t\t\tEnhancedCode: exterrors.SMTPEnchCode(lastErr, exterrors.EnhancedCode{0, 4, 0}),\n\t\t\tMessage:      \"No usable MXs, last err: \" + lastErr.Error(),\n\t\t\tTargetName:   \"remote\",\n\t\t\tErr:          lastErr,\n\t\t\tMisc: map[string]interface{}{\n\t\t\t\t\"domain\": domain,\n\t\t\t},\n\t\t}\n\t}\n\n\treturn &conn, nil\n}\n\nfunc (rd *remoteDelivery) lookupMX(ctx context.Context, domain string) (dnssecOk bool, records []*net.MX, err error) {\n\tif rd.rt.extResolver != nil {\n\t\tdnssecOk, records, err = rd.rt.extResolver.AuthLookupMX(context.Background(), domain)\n\t} else {\n\t\trecords, err = rd.rt.resolver.LookupMX(ctx, dns.FQDN(domain))\n\t}\n\tif err != nil {\n\t\treason, misc := exterrors.UnwrapDNSErr(err)\n\t\treturn false, nil, &exterrors.SMTPError{\n\t\t\tCode:         exterrors.SMTPCode(err, 451, 554),\n\t\t\tEnhancedCode: exterrors.SMTPEnchCode(err, exterrors.EnhancedCode{0, 4, 4}),\n\t\t\tMessage:      \"MX lookup error\",\n\t\t\tTargetName:   \"remote\",\n\t\t\tReason:       reason,\n\t\t\tErr:          err,\n\t\t\tMisc:         misc,\n\t\t}\n\t}\n\n\tsort.Slice(records, func(i, j int) bool {\n\t\treturn records[i].Pref < records[j].Pref\n\t})\n\n\t// Fallback to A/AAA RR when no MX records are present as\n\t// required by RFC 5321 Section 5.1.\n\tif len(records) == 0 {\n\t\trecords = append(records, &net.MX{\n\t\t\tHost: domain,\n\t\t\tPref: 0,\n\t\t})\n\t}\n\n\treturn dnssecOk, records, err\n}\n"
  },
  {
    "path": "internal/target/remote/dane.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage remote\n\nimport (\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"time\"\n\n\t\"github.com/foxcpp/maddy/framework/dns\"\n\t\"github.com/foxcpp/maddy/framework/exterrors\"\n)\n\n// Used to override verification time for DANE-TA tests.\nvar verifyDANETime time.Time\n\n// verifyDANE checks whether TLSA records require TLS use and match the\n// certificate and name used by the server.\n//\n// overridePKIX result indicates whether DANE should make server authentication\n// succeed even if PKIX/X.509 verification fails. That is, if InsecureSkipVerify\n// is used and verifyDANE returns overridePKIX=true, the server certificate\n// should trusted.\nfunc verifyDANE(recs []dns.TLSA, connState tls.ConnectionState) (overridePKIX bool, err error) {\n\ttlsErr := &exterrors.SMTPError{\n\t\tCode:         550,\n\t\tEnhancedCode: exterrors.EnhancedCode{5, 7, 1},\n\t\tMessage:      \"TLS is required but unsupported or failed (enforced by DANE)\",\n\t\tTargetName:   \"remote\",\n\t\tMisc: map[string]interface{}{\n\t\t\t\"remote_server\": connState.ServerName,\n\t\t},\n\t}\n\n\t// See https://tools.ietf.org/html/rfc7672#section-2.2 for requirements of\n\t// TLS discovery.\n\t// We assume upstream resolver will generate an error if the DNSSEC\n\t// signature is bogus so this case is \"DNSSEC-authenticated denial of existence\".\n\tif len(recs) == 0 {\n\t\treturn false, nil\n\t}\n\n\t// Require TLS even if all records are not usable, per Section 2.2 of RFC 7672.\n\tif !connState.HandshakeComplete {\n\t\treturn false, tlsErr\n\t}\n\n\t// Ignore invalid records.\n\tvar (\n\t\teeRecs []dns.TLSA\n\t\ttaRecs []dns.TLSA\n\t)\n\tfor _, rec := range recs {\n\t\tswitch rec.MatchingType {\n\t\tcase 0, 1, 2:\n\t\tdefault:\n\t\t\tcontinue\n\t\t}\n\t\tswitch rec.Selector {\n\t\tcase 0, 1:\n\t\tdefault:\n\t\t\tcontinue\n\t\t}\n\n\t\tswitch rec.Usage {\n\t\tcase 2:\n\t\t\ttaRecs = append(taRecs, rec)\n\t\tcase 3:\n\t\t\teeRecs = append(eeRecs, rec)\n\t\tdefault:\n\t\t\tcontinue\n\t\t}\n\t}\n\n\t// Authentication is not required if all records are unusable, see\n\t// RFC 7672 Section 2.1.1.\n\tif len(eeRecs) == 0 && len(taRecs) == 0 {\n\t\treturn false, nil\n\t}\n\n\tfor _, rec := range eeRecs {\n\t\tif rec.Verify(connState.PeerCertificates[0]) == nil {\n\t\t\t// https://tools.ietf.org/html/rfc7672#section-3.1.1\n\t\t\t// - SAN/CN are not considered.\n\t\t\t// - Expired certificates are fine too.\n\t\t\treturn true, nil\n\t\t}\n\t}\n\n\t// Don't bother building a temporary certificate pool if there are no\n\t// records to check.\n\tif len(taRecs) == 0 {\n\t\treturn true, &exterrors.SMTPError{\n\t\t\tCode:         550,\n\t\t\tEnhancedCode: exterrors.EnhancedCode{5, 7, 0},\n\t\t\tMessage:      \"No matching TLSA records\",\n\t\t\tTargetName:   \"remote\",\n\t\t\tMisc: map[string]interface{}{\n\t\t\t\t\"remote_server\": connState.ServerName,\n\t\t\t},\n\t\t}\n\t}\n\n\t// Collect certificates presented by the server as possible intermediates.\n\t// Add all certificates from the chain that match any record to the root\n\t// pool.\n\topts := x509.VerifyOptions{\n\t\tDNSName:       connState.ServerName,\n\t\tIntermediates: x509.NewCertPool(),\n\t\tRoots:         x509.NewCertPool(),\n\t\tCurrentTime:   verifyDANETime,\n\t}\n\tfor _, cert := range connState.PeerCertificates {\n\t\troot := false\n\t\tfor _, rec := range taRecs {\n\t\t\tif cert.IsCA && rec.Verify(cert) == nil {\n\t\t\t\topts.Roots.AddCert(cert)\n\t\t\t\troot = true\n\t\t\t}\n\t\t}\n\t\tif !root {\n\t\t\topts.Intermediates.AddCert(cert)\n\t\t}\n\t}\n\n\t// ... then run the standard X.509 verification. This will verify that the\n\t// server certificate chains to any of asserted TA certificates.\n\tif _, err := connState.PeerCertificates[0].Verify(opts); err == nil {\n\t\treturn true, nil\n\t}\n\n\t// There are valid records, but none matched.\n\treturn false, &exterrors.SMTPError{\n\t\tCode:         550,\n\t\tEnhancedCode: exterrors.EnhancedCode{5, 7, 0},\n\t\tMessage:      \"No matching TLSA records\",\n\t\tTargetName:   \"remote\",\n\t\tMisc: map[string]interface{}{\n\t\t\t\"remote_server\": connState.ServerName,\n\t\t},\n\t}\n}\n"
  },
  {
    "path": "internal/target/remote/dane_delivery_test.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage remote\n\nimport (\n\t\"crypto/tls\"\n\t\"net\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"github.com/foxcpp/go-mockdns\"\n\t\"github.com/foxcpp/maddy/framework/dns\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/internal/testutils\"\n\tmiekgdns \"github.com/miekg/dns\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc targetWithExtResolver(t *testing.T, zones map[string]mockdns.Zone) (*mockdns.Server, *Target) {\n\tl := testutils.Logger(t, \"mockdns\")\n\tdnsSrv, err := mockdns.NewServerWithLogger(zones, l, false)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tdialer := net.Dialer{}\n\tdialer.Resolver = &net.Resolver{}\n\tdnsSrv.PatchNet(dialer.Resolver)\n\taddr := dnsSrv.LocalAddr().(*net.UDPAddr)\n\n\textResolver, err := dns.NewExtResolver()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\textResolver.Cfg.Servers = []string{addr.IP.String()}\n\textResolver.Cfg.Port = strconv.Itoa(addr.Port)\n\n\ttgt := testTarget(t, zones, extResolver, []module.MXAuthPolicy{\n\t\ttestDANEPolicy(t, extResolver),\n\t})\n\treturn dnsSrv, tgt\n}\n\nfunc tlsaRecord(name string, usage, matchType, selector uint8, cert string) map[miekgdns.Type][]miekgdns.RR {\n\treturn map[miekgdns.Type][]miekgdns.RR{\n\t\tmiekgdns.Type(miekgdns.TypeTLSA): {\n\t\t\t&miekgdns.TLSA{\n\t\t\t\tHdr: miekgdns.RR_Header{\n\t\t\t\t\tName:   name,\n\t\t\t\t\tClass:  miekgdns.ClassINET,\n\t\t\t\t\tRrtype: miekgdns.TypeTLSA,\n\t\t\t\t\tTtl:    9999,\n\t\t\t\t},\n\t\t\t\tUsage:        usage,\n\t\t\t\tMatchingType: matchType,\n\t\t\t\tSelector:     selector,\n\t\t\t\tCertificate:  cert,\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc TestRemoteDelivery_DANE_Ok(t *testing.T) {\n\t_, be, srv := testutils.SMTPServerSTARTTLS(t, \"127.0.0.1:\"+smtpPort)\n\tdefer func() {\n\t\trequire.NoError(t, srv.Close())\n\t}()\n\tdefer testutils.CheckSMTPConnLeak(t, srv)\n\n\t// RFC 7672, Section 2.2.2. \"Non-CNAME\" case.\n\tzones := map[string]mockdns.Zone{\n\t\t\"example.invalid.\": {\n\t\t\tMX: []net.MX{{Host: \"mx.example.invalid.\", Pref: 10}},\n\t\t},\n\t\t\"mx.example.invalid.\": {\n\t\t\tAD: true,\n\t\t\tA:  []string{\"127.0.0.1\"},\n\t\t},\n\t\t\"_25._tcp.mx.example.invalid.\": {\n\t\t\tAD: true,\n\t\t\tMisc: tlsaRecord(\n\t\t\t\t\"_25._tcp.mx.example.invalid.\",\n\t\t\t\t3, 1, 1, \"a9b5cb4d02f996f6385debe9a8952f1af1f4aec7eae0f37c2cd6d0d8ee8391cf\"),\n\t\t},\n\t}\n\n\tdnsSrv, tgt := targetWithExtResolver(t, zones)\n\tdefer func() {\n\t\tassert.NoError(t, dnsSrv.Close())\n\t}()\n\ttgt.policies = append(tgt.policies,\n\t\t&localPolicy{\n\t\t\tminTLSLevel: module.TLSAuthenticated, // Established via DANE instead of PKIX.\n\t\t},\n\t)\n\n\ttestutils.DoTestDelivery(t, tgt, \"test@example.com\", []string{\"test@example.invalid\"})\n\tbe.CheckMsg(t, 0, \"test@example.com\", []string{\"test@example.invalid\"})\n}\n\nfunc TestRemoteDelivery_DANE_CNAMEd_1(t *testing.T) {\n\t_, be, srv := testutils.SMTPServerSTARTTLS(t, \"127.0.0.1:\"+smtpPort)\n\tdefer func() {\n\t\trequire.NoError(t, srv.Close())\n\t}()\n\tdefer testutils.CheckSMTPConnLeak(t, srv)\n\n\t// RFC 7672, Section 2.2.2. \"Secure CNAME\" case - TLSA at CNAME matches.\n\tzones := map[string]mockdns.Zone{\n\t\t\"example.invalid.\": {\n\t\t\tMX: []net.MX{{Host: \"mx.example.invalid.\", Pref: 10}},\n\t\t},\n\t\t\"mx.example.invalid.\": {\n\t\t\tAD:    true,\n\t\t\tCNAME: \"mx.cname.invalid.\",\n\t\t},\n\t\t\"mx.cname.invalid.\": {\n\t\t\tA: []string{\"127.0.0.1\"},\n\t\t},\n\t\t\"_25._tcp.mx.cname.invalid.\": {\n\t\t\tAD: true,\n\t\t\tMisc: tlsaRecord(\n\t\t\t\t\"_25._tcp.mx.cname.invalid.\",\n\t\t\t\t3, 1, 1, \"a9b5cb4d02f996f6385debe9a8952f1af1f4aec7eae0f37c2cd6d0d8ee8391cf\"),\n\t\t},\n\t}\n\n\tdnsSrv, tgt := targetWithExtResolver(t, zones)\n\tdefer func() {\n\t\tassert.NoError(t, dnsSrv.Close())\n\t}()\n\ttgt.policies = append(tgt.policies,\n\t\t&localPolicy{\n\t\t\tminTLSLevel: module.TLSAuthenticated, // Established via DANE instead of PKIX.\n\t\t},\n\t)\n\n\ttestutils.DoTestDelivery(t, tgt, \"test@example.com\", []string{\"test@example.invalid\"})\n\tbe.CheckMsg(t, 0, \"test@example.com\", []string{\"test@example.invalid\"})\n}\n\nfunc TestRemoteDelivery_DANE_CNAMEd_2(t *testing.T) {\n\t_, be, srv := testutils.SMTPServerSTARTTLS(t, \"127.0.0.1:\"+smtpPort)\n\tdefer func() {\n\t\trequire.NoError(t, srv.Close())\n\t}()\n\tdefer testutils.CheckSMTPConnLeak(t, srv)\n\n\t// RFC 7672, Section 2.2.2. \"Secure CNAME\" case - TLSA at initial name matches.\n\tzones := map[string]mockdns.Zone{\n\t\t\"example.invalid.\": {\n\t\t\tMX: []net.MX{{Host: \"mx.example.invalid.\", Pref: 10}},\n\t\t},\n\t\t\"mx.example.invalid.\": {\n\t\t\tAD:    true,\n\t\t\tCNAME: \"mx.cname.invalid.\",\n\t\t},\n\t\t\"_25._tcp.mx.example.invalid.\": {\n\t\t\tAD: true,\n\t\t\tMisc: tlsaRecord(\n\t\t\t\t\"_25._tcp.mx.cname.invalid.\",\n\t\t\t\t3, 1, 1, \"a9b5cb4d02f996f6385debe9a8952f1af1f4aec7eae0f37c2cd6d0d8ee8391cf\"),\n\t\t},\n\t\t\"mx.cname.invalid.\": {\n\t\t\tAD: true,\n\t\t\tA:  []string{\"127.0.0.1\"},\n\t\t},\n\t}\n\n\tdnsSrv, tgt := targetWithExtResolver(t, zones)\n\tdefer func() {\n\t\tassert.NoError(t, dnsSrv.Close())\n\t}()\n\ttgt.policies = append(tgt.policies,\n\t\t&localPolicy{\n\t\t\tminTLSLevel: module.TLSAuthenticated, // Established via DANE instead of PKIX.\n\t\t},\n\t)\n\n\ttestutils.DoTestDelivery(t, tgt, \"test@example.com\", []string{\"test@example.invalid\"})\n\tbe.CheckMsg(t, 0, \"test@example.com\", []string{\"test@example.invalid\"})\n}\n\nfunc TestRemoteDelivery_DANE_InsecureCNAMEDest(t *testing.T) {\n\tclientCfg, be, srv := testutils.SMTPServerSTARTTLS(t, \"127.0.0.1:\"+smtpPort)\n\tdefer func() {\n\t\trequire.NoError(t, srv.Close())\n\t}()\n\tdefer testutils.CheckSMTPConnLeak(t, srv)\n\n\t// RFC 7672, Section 2.2.2. \"Insecure CNAME\" case - initial name is secure.\n\tzones := map[string]mockdns.Zone{\n\t\t\"example.invalid.\": {\n\t\t\tMX: []net.MX{{Host: \"mx.example.invalid.\", Pref: 10}},\n\t\t},\n\t\t\"mx.example.invalid.\": {\n\t\t\tAD:    true,\n\t\t\tCNAME: \"mx.cname.invalid.\",\n\t\t},\n\t\t\"_25._tcp.mx.example.invalid.\": {\n\t\t\tAD: true,\n\t\t\t// This is the record that activates DANE but does not match the cert\n\t\t\t// => delivery is failed.\n\t\t\tMisc: tlsaRecord(\n\t\t\t\t\"_25._tcp.mx.example.invalid.\",\n\t\t\t\t3, 1, 1, \"a9b5cb4d02f996f6385debe9a8952f1af1f4aec7eae0f37c2cd6d0d8ee8391cb\"),\n\t\t},\n\t\t\"_25._tcp.mx.cname.invalid.\": {\n\t\t\tAD: false,\n\t\t\t// This is the record that matches the cert and would make delivery succeed\n\t\t\t// but it should not be considered since AD=false.\n\t\t\tMisc: tlsaRecord(\n\t\t\t\t\"_25._tcp.mx.cname.invalid.\",\n\t\t\t\t3, 1, 1, \"a9b5cb4d02f996f6385debe9a8952f1af1f4aec7eae0f37c2cd6d0d8ee8391cf\"),\n\t\t},\n\t}\n\n\tdnsSrv, tgt := targetWithExtResolver(t, zones)\n\tdefer func() {\n\t\trequire.NoError(t, dnsSrv.Close())\n\t}()\n\ttgt.tlsConfig = clientCfg\n\n\t_, err := testutils.DoTestDeliveryErr(t, tgt, \"test@example.com\", []string{\"test@example.invalid\"})\n\tif err == nil {\n\t\tt.Error(\"Expected an error, got none\")\n\t}\n\tif be.MailFromCounter != 0 {\n\t\tt.Fatal(\"MAIL FROM issued but should not\")\n\t}\n}\n\nfunc TestRemoteDelivery_DANE_NonAD_TLSA_Ignore(t *testing.T) {\n\tbe, srv := testutils.SMTPServer(t, \"127.0.0.1:\"+smtpPort)\n\tdefer func() {\n\t\trequire.NoError(t, srv.Close())\n\t}()\n\tdefer testutils.CheckSMTPConnLeak(t, srv)\n\n\t// RFC 7672, Section 2.2.2. \"Non-CNAME\" case - initial name is insecure.\n\tzones := map[string]mockdns.Zone{\n\t\t\"example.invalid.\": {\n\t\t\tMX: []net.MX{{Host: \"mx.example.invalid.\", Pref: 10}},\n\t\t},\n\t\t\"mx.example.invalid.\": {\n\t\t\tA: []string{\"127.0.0.1\"},\n\t\t},\n\t\t\"_25._tcp.mx.example.invalid.\": {\n\t\t\tMisc: tlsaRecord(\n\t\t\t\t\"_25._tcp.mx.example.invalid.\",\n\t\t\t\t3, 1, 1, \"a9b5cb4d02f996f6385debe9a8952f1af1f4aec7eae0f37c2cd6d0d8ee8391cb\"),\n\t\t},\n\t}\n\n\tdnsSrv, tgt := targetWithExtResolver(t, zones)\n\tdefer func() {\n\t\trequire.NoError(t, dnsSrv.Close())\n\t}()\n\n\ttestutils.DoTestDelivery(t, tgt, \"test@example.com\", []string{\"test@example.invalid\"})\n\tbe.CheckMsg(t, 0, \"test@example.com\", []string{\"test@example.invalid\"})\n}\n\nfunc TestRemoteDelivery_DANE_NonADIgnore_CNAME(t *testing.T) {\n\tbe, srv := testutils.SMTPServer(t, \"127.0.0.1:\"+smtpPort)\n\tdefer func() {\n\t\trequire.NoError(t, srv.Close())\n\t}()\n\tdefer testutils.CheckSMTPConnLeak(t, srv)\n\n\t// RFC 7672, Section 2.2.2. \"Insecure CNAME\" case - initial name is insecure.\n\tzones := map[string]mockdns.Zone{\n\t\t\"example.invalid.\": {\n\t\t\tMX: []net.MX{{Host: \"mx.example.invalid.\", Pref: 10}},\n\t\t},\n\t\t\"mx.example.invalid.\": {\n\t\t\tCNAME: \"mx.cname.invalid.\",\n\t\t},\n\t\t\"mx.cname.invalid.\": {\n\t\t\tA: []string{\"127.0.0.1\"},\n\t\t},\n\t\t\"_25._tcp.mx.cname.invalid.\": {\n\t\t\tAD: true,\n\t\t\tMisc: tlsaRecord(\n\t\t\t\t\"_25._tcp.mx.example.invalid.\",\n\t\t\t\t3, 1, 1, \"a9b5cb4d02f996f6385debe9a8952f1af1f4aec7eae0f37c2cd6d0d8ee8391cb\"),\n\t\t},\n\t}\n\n\tdnsSrv, tgt := targetWithExtResolver(t, zones)\n\tdefer func() {\n\t\trequire.NoError(t, dnsSrv.Close())\n\t}()\n\n\ttestutils.DoTestDelivery(t, tgt, \"test@example.com\", []string{\"test@example.invalid\"})\n\tbe.CheckMsg(t, 0, \"test@example.com\", []string{\"test@example.invalid\"})\n}\n\nfunc TestRemoteDelivery_DANE_SkipAUnauth(t *testing.T) {\n\tclientCfg, be, srv := testutils.SMTPServerSTARTTLS(t, \"127.0.0.1:\"+smtpPort)\n\tdefer func() {\n\t\trequire.NoError(t, srv.Close())\n\t}()\n\tdefer testutils.CheckSMTPConnLeak(t, srv)\n\n\tzones := map[string]mockdns.Zone{\n\t\t\"example.invalid.\": {\n\t\t\tMX: []net.MX{{Host: \"mx.example.invalid.\", Pref: 10}},\n\t\t},\n\t\t\"mx.example.invalid.\": {\n\t\t\tA: []string{\"127.0.0.1\"},\n\t\t},\n\t\t\"_25._tcp.mx.example.invalid.\": {\n\t\t\tAD: false,\n\t\t\tMisc: tlsaRecord(\n\t\t\t\t\"_25._tcp.mx.example.invalid.\",\n\t\t\t\t3, 1, 1, \"invalid hex will cause serialization error and no response will be sent\"),\n\t\t},\n\t}\n\n\tdnsSrv, tgt := targetWithExtResolver(t, zones)\n\tdefer func() {\n\t\trequire.NoError(t, dnsSrv.Close())\n\t}()\n\ttgt.tlsConfig = clientCfg\n\n\ttestutils.DoTestDelivery(t, tgt, \"test@example.com\", []string{\"test@example.invalid\"})\n\tbe.CheckMsg(t, 0, \"test@example.com\", []string{\"test@example.invalid\"})\n}\n\nfunc TestRemoteDelivery_DANE_Mismatch(t *testing.T) {\n\tclientCfg, be, srv := testutils.SMTPServerSTARTTLS(t, \"127.0.0.1:\"+smtpPort)\n\tdefer func() {\n\t\trequire.NoError(t, srv.Close())\n\t}()\n\tdefer testutils.CheckSMTPConnLeak(t, srv)\n\n\tzones := map[string]mockdns.Zone{\n\t\t\"example.invalid.\": {\n\t\t\tMX: []net.MX{{Host: \"mx.example.invalid.\", Pref: 10}},\n\t\t},\n\t\t\"mx.example.invalid.\": {\n\t\t\tAD: true,\n\t\t\tA:  []string{\"127.0.0.1\"},\n\t\t},\n\t\t\"_25._tcp.mx.example.invalid.\": {\n\t\t\tAD: true,\n\t\t\tMisc: tlsaRecord(\n\t\t\t\t\"_25._tcp.mx.example.invalid.\",\n\t\t\t\t3, 1, 1, \"ffb5cb4d02f996f6385debe9a8952f1af1f4aec7eae0f37c2cd6d0d8ee8391cf\"),\n\t\t},\n\t}\n\n\tdnsSrv, tgt := targetWithExtResolver(t, zones)\n\tdefer func() {\n\t\trequire.NoError(t, dnsSrv.Close())\n\t}()\n\ttgt.tlsConfig = clientCfg\n\n\t_, err := testutils.DoTestDeliveryErr(t, tgt, \"test@example.com\", []string{\"test@example.invalid\"})\n\tif err == nil {\n\t\tt.Error(\"Expected an error, got none\")\n\t}\n\tif be.MailFromCounter != 0 {\n\t\tt.Fatal(\"MAIL FROM issued but should not\")\n\t}\n}\n\nfunc TestRemoteDelivery_DANE_NoRecord(t *testing.T) {\n\tclientCfg, be, srv := testutils.SMTPServerSTARTTLS(t, \"127.0.0.1:\"+smtpPort)\n\tdefer func() {\n\t\trequire.NoError(t, srv.Close())\n\t}()\n\tdefer testutils.CheckSMTPConnLeak(t, srv)\n\n\tzones := map[string]mockdns.Zone{\n\t\t\"example.invalid.\": {\n\t\t\tMX: []net.MX{{Host: \"mx.example.invalid.\", Pref: 10}},\n\t\t},\n\t\t\"mx.example.invalid.\": {\n\t\t\tAD: true,\n\t\t\tA:  []string{\"127.0.0.1\"},\n\t\t},\n\t}\n\n\tdnsSrv, tgt := targetWithExtResolver(t, zones)\n\tdefer func() {\n\t\trequire.NoError(t, dnsSrv.Close())\n\t}()\n\ttgt.tlsConfig = clientCfg\n\n\ttestutils.DoTestDelivery(t, tgt, \"test@example.com\", []string{\"test@example.invalid\"})\n\tbe.CheckMsg(t, 0, \"test@example.com\", []string{\"test@example.invalid\"})\n}\n\nfunc TestRemoteDelivery_DANE_LookupErr(t *testing.T) {\n\tclientCfg, be, srv := testutils.SMTPServerSTARTTLS(t, \"127.0.0.1:\"+smtpPort)\n\tdefer func() {\n\t\trequire.NoError(t, srv.Close())\n\t}()\n\tdefer testutils.CheckSMTPConnLeak(t, srv)\n\n\tzones := map[string]mockdns.Zone{\n\t\t\"example.invalid.\": {\n\t\t\tMX: []net.MX{{Host: \"mx.example.invalid.\", Pref: 10}},\n\t\t},\n\t\t\"mx.example.invalid.\": {\n\t\t\tAD: true,\n\t\t\tA:  []string{\"127.0.0.1\"},\n\t\t},\n\t\t\"_25._tcp.mx.example.invalid.\": {\n\t\t\tErr: &net.DNSError{},\n\t\t},\n\t}\n\n\tdnsSrv, tgt := targetWithExtResolver(t, zones)\n\tdefer func() {\n\t\trequire.NoError(t, dnsSrv.Close())\n\t}()\n\ttgt.tlsConfig = clientCfg\n\n\t_, err := testutils.DoTestDeliveryErr(t, tgt, \"test@example.com\", []string{\"test@example.invalid\"})\n\tif err == nil {\n\t\tt.Error(\"Expected an error, got none\")\n\t}\n\tif be.MailFromCounter != 0 {\n\t\tt.Fatal(\"MAIL FROM issued but should not\")\n\t}\n}\n\nfunc TestRemoteDelivery_DANE_NoTLS(t *testing.T) {\n\tbe, srv := testutils.SMTPServer(t, \"127.0.0.1:\"+smtpPort)\n\tdefer func() {\n\t\trequire.NoError(t, srv.Close())\n\t}()\n\tdefer testutils.CheckSMTPConnLeak(t, srv)\n\n\tzones := map[string]mockdns.Zone{\n\t\t\"example.invalid.\": {\n\t\t\tMX: []net.MX{{Host: \"mx.example.invalid.\", Pref: 10}},\n\t\t},\n\t\t\"mx.example.invalid.\": {\n\t\t\tAD: true,\n\t\t\tA:  []string{\"127.0.0.1\"},\n\t\t},\n\t\t\"_25._tcp.mx.example.invalid.\": {\n\t\t\tAD: true,\n\t\t\tMisc: tlsaRecord(\n\t\t\t\t\"_25._tcp.mx.example.invalid.\",\n\t\t\t\t3, 1, 1, \"a9b5cb4d02f996f6385debe9a8952f1af1f4aec7eae0f37c2cd6d0d8ee8391cf\"),\n\t\t},\n\t}\n\tdnsSrv, tgt := targetWithExtResolver(t, zones)\n\tdefer func() {\n\t\trequire.NoError(t, dnsSrv.Close())\n\t}()\n\n\t_, err := testutils.DoTestDeliveryErr(t, tgt, \"test@example.com\", []string{\"test@example.invalid\"})\n\tif err == nil {\n\t\tt.Error(\"Expected an error, got none\")\n\t}\n\tif be.MailFromCounter != 0 {\n\t\tt.Fatal(\"MAIL FROM issued but should not\")\n\t}\n}\n\nfunc TestRemoteDelivery_DANE_TLSError(t *testing.T) {\n\t_, be, srv := testutils.SMTPServerSTARTTLS(t, \"127.0.0.1:\"+smtpPort)\n\tdefer func() {\n\t\trequire.NoError(t, srv.Close())\n\t}()\n\tdefer testutils.CheckSMTPConnLeak(t, srv)\n\n\tzones := map[string]mockdns.Zone{\n\t\t\"example.invalid.\": {\n\t\t\tAD: true,\n\t\t\tMX: []net.MX{{Host: \"mx.example.invalid.\", Pref: 10}},\n\t\t},\n\t\t\"mx.example.invalid.\": {\n\t\t\tAD: true,\n\t\t\tA:  []string{\"127.0.0.1\"},\n\t\t},\n\t\t\"_25._tcp.mx.example.invalid.\": {\n\t\t\tAD: true,\n\t\t\tMisc: tlsaRecord(\n\t\t\t\t\"_25._tcp.mx.example.invalid.\",\n\t\t\t\t3, 1, 1, \"a9b5cb4d02f996f6385debe9a8952f1af1f4aec7eae0f37c2cd6d0d8ee8391cf\"),\n\t\t},\n\t}\n\tdnsSrv, tgt := targetWithExtResolver(t, zones)\n\tdefer func() {\n\t\trequire.NoError(t, dnsSrv.Close())\n\t}()\n\n\t// Cause failure through version incompatibility.\n\ttgt.tlsConfig = &tls.Config{\n\t\tMaxVersion: tls.VersionTLS12,\n\t\tMinVersion: tls.VersionTLS12,\n\t}\n\tsrv.TLSConfig.MinVersion = tls.VersionTLS11\n\tsrv.TLSConfig.MaxVersion = tls.VersionTLS11\n\n\t// DANE should prevent the fallback to plaintext.\n\t_, err := testutils.DoTestDeliveryErr(t, tgt, \"test@example.com\", []string{\"test@example.invalid\"})\n\tif err == nil {\n\t\tt.Error(\"Expected an error, got none\")\n\t}\n\tif be.MailFromCounter != 0 {\n\t\tt.Fatal(\"MAIL FROM issued but should not\")\n\t}\n}\n"
  },
  {
    "path": "internal/target/remote/dane_test.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage remote\n\nimport (\n\t\"crypto/sha256\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"encoding/hex\"\n\t\"encoding/pem\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/miekg/dns\"\n)\n\n// These certificates are related like this:\n//\n//\tRoot A -> Intermediate A -> Leaf A\n//\tRoot B -> LeafB\nvar (\n\trootA = `-----BEGIN CERTIFICATE-----\nMIIBMDCB46ADAgECAhRDwag3n5CG90BEO87zEMAPejn6YTAFBgMrZXAwFjEUMBIG\nA1UEAxMLVGVzdCBSb290IEEwHhcNMjAxMTI4MjExODA4WhcNMzAxMTI2MjExODA4\nWjAWMRQwEgYDVQQDEwtUZXN0IFJvb3QgQTAqMAUGAytlcAMhADXMzcRec5ocluNR\nExnnNT7I5fmcpjf2P4ik5k0DJNbco0MwQTAPBgNVHRMBAf8EBTADAQH/MA8GA1Ud\nDwEB/wQFAwMHBAAwHQYDVR0OBBYEFM5b/b1di1vA+YpMZcsF4K7N1LbaMAUGAytl\ncANBAAZ0XTxBDjN9VGPqWjXrYqGPUqbjm4JD3PeHUB4YGH+MNTgeVIlU8qCLIXtM\n9kmAkCk7+j5G8p0gMjJMNygeuwE=\n-----END CERTIFICATE-----`\n\tintermediateA = `-----BEGIN CERTIFICATE-----\nMIIBWjCCAQygAwIBAgIUEOd619/8HC1pWXxaEpQ1vUZOe7wwBQYDK2VwMBYxFDAS\nBgNVBAMTC1Rlc3QgUm9vdCBBMB4XDTIwMTEyODIxMTk0M1oXDTMwMTEyNjIxMTk0\nM1owHjEcMBoGA1UEAxMTVGVzdCBJbnRlcm1lZGlhdGUgQTAqMAUGAytlcAMhAFgW\naZz5316olEIHn1Q4RTPd2u/EjN2bo+Cn3EmSlFxto2QwYjAPBgNVHRMBAf8EBTAD\nAQH/MA8GA1UdDwEB/wQFAwMHBAAwHQYDVR0OBBYEFB0P00Qphygy+KgkI9tjihFD\nELxhMB8GA1UdIwQYMBaAFM5b/b1di1vA+YpMZcsF4K7N1LbaMAUGAytlcANBAJJH\nzsS8ahEjdyRCNUlsPalZiKW8N3G0LnwdVKFhVfcCT+RTRcrMP7vjuWsbJyD5e7hu\nz2eCI68xreLQlNySdQ0=\n-----END CERTIFICATE-----`\n\tleafA = `-----BEGIN CERTIFICATE-----\nMIIBjzCCAUGgAwIBAgIUONvbCs6r9zKFM3IAPRMdrNiJpNgwBQYDK2VwMB4xHDAa\nBgNVBAMTE1Rlc3QgSW50ZXJtZWRpYXRlIEEwHhcNMjAxMTI4MjEyMTIyWhcNMzAx\nMTI2MjEyMTIyWjAWMRQwEgYDVQQDEwtUZXN0IExlYWYgQTAqMAUGAytlcAMhABIj\nW7gwY78RCWHs9eSIdy4x4MXjzdhZwgNSNHHCp5pAo4GYMIGVMAwGA1UdEwEB/wQC\nMAAwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMBUGA1UdEQQOMAyCCm1h\nZGR5LnRlc3QwDwYDVR0PAQH/BAUDAweAADAdBgNVHQ4EFgQU9PFQCnG5fNpNPXUT\n8rCuylS6tVwwHwYDVR0jBBgwFoAUHQ/TRCmHKDL4qCQj22OKEUMQvGEwBQYDK2Vw\nA0EAGdvHA4VLxpUeUu1Vjom2YX3MukPJG0a3/dB3HiAWWpxMgWfU+Ftie7noaNcI\noUW+M8my46dqN6oXSHU47/QjDg==\n-----END CERTIFICATE-----`\n\trootB = `-----BEGIN CERTIFICATE-----\nMIIBMDCB46ADAgECAhRXD7xuPkipDyxyCtm8pZaxhuulaDAFBgMrZXAwFjEUMBIG\nA1UEAxMLVGVzdCBSb290IEIwHhcNMjAxMTI4MjExODMwWhcNMzAxMTI2MjExODMw\nWjAWMRQwEgYDVQQDEwtUZXN0IFJvb3QgQjAqMAUGAytlcAMhAPOIGJJh5jK8N/Vc\nlLrFpysV+SiZjT1Cmt7hoFtMrlbTo0MwQTAPBgNVHRMBAf8EBTADAQH/MA8GA1Ud\nDwEB/wQFAwMHBAAwHQYDVR0OBBYEFOLGYf4mkhKbZPwZKCv952tfz/KDMAUGAytl\ncANBAOX2gb6ud8CAvOsCgw6uaRm0+jMDVZfkAkNuCIO6cJ/WYfdvuXYXu3e88SuI\ngri++h118PomIzJ5PHAaCYsFPgQ=\n-----END CERTIFICATE-----`\n\tleafB = `-----BEGIN CERTIFICATE-----\nMIIBhzCCATmgAwIBAgIUR2bVQ/Cu4j7Td5TdbWd6Q0LEpOgwBQYDK2VwMBYxFDAS\nBgNVBAMTC1Rlc3QgUm9vdCBCMB4XDTIwMTEyODIxMjE0M1oXDTMwMTEyNjIxMjE0\nM1owFjEUMBIGA1UEAxMLVGVzdCBMZWFmIEIwKjAFBgMrZXADIQBiHCTUxF3UxPIV\nM/o5OkTtmUrI7AInOvMa0dchU4iJXqOBmDCBlTAMBgNVHRMBAf8EAjAAMB0GA1Ud\nJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATAVBgNVHREEDjAMggptYWRkeS50ZXN0\nMA8GA1UdDwEB/wQFAwMHgAAwHQYDVR0OBBYEFPYZPubaAXyr6kXs3khqpMNfdHKK\nMB8GA1UdIwQYMBaAFOLGYf4mkhKbZPwZKCv952tfz/KDMAUGAytlcANBABlOwVxE\nh7vYmaMYoyOSF1GQiB0ZLsGUjrTNHDnv0+Xp8xG5Td5mGnBi/4Ehq39PdLrj2T7j\n3Xy0aiqdDomvwQY=\n-----END CERTIFICATE-----`\n)\n\nfunc parsePEMCert(blob string) *x509.Certificate {\n\tblock, _ := pem.Decode([]byte(blob))\n\tcert, err := x509.ParseCertificate(block.Bytes)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn cert\n}\n\nfunc singleTlsaRecord(usage, matchType, selector uint8, cert string) dns.TLSA {\n\treturn dns.TLSA{\n\t\tHdr: dns.RR_Header{\n\t\t\tName:   \"maddy.test.\",\n\t\t\tClass:  dns.ClassINET,\n\t\t\tRrtype: dns.TypeTLSA,\n\t\t\tTtl:    9999,\n\t\t},\n\t\tUsage:        usage,\n\t\tMatchingType: matchType,\n\t\tSelector:     selector,\n\t\tCertificate:  cert,\n\t}\n}\n\nfunc keySHA256(blob string) string {\n\tcert := parsePEMCert(blob)\n\thash := sha256.Sum256(cert.RawSubjectPublicKeyInfo)\n\treturn hex.EncodeToString(hash[:])\n}\n\nfunc TestVerifyDANE(t *testing.T) {\n\tverifyDANETime = time.Unix(1606600100, 0)\n\ttest := func(name string, recs []dns.TLSA, connState tls.ConnectionState, expectErr bool) {\n\t\tt.Helper()\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tt.Helper()\n\t\t\t_, err := verifyDANE(recs, connState)\n\t\t\tif (err != nil) != expectErr {\n\t\t\t\tt.Error(\"err:\", err, \"expectErr:\", expectErr)\n\t\t\t}\n\t\t})\n\t}\n\n\t// RFC 7672, Section 2.2:\n\t// An \"insecure\" TLSA RRset or DNSSEC-authenticated denial of existence\n\t// of the TLSA records:\n\t//    A connection to the MTA SHOULD be made using (pre-DANE)\n\t// opportunistic TLS;\n\t//\n\t// \"Insecure\" TLSA RRset results in verifyDANE not being called at all,\n\t// but for the latter (authenticated denial of existence) it is still\n\t// called and should be tested for.\n\t//\n\t// More specific tests for TLSA RRset discovery (including CNAME\n\t// shenanigans) are in dane_delivery_test.go.\n\ttest(\"no TLSA, TLS\", []dns.TLSA{}, tls.ConnectionState{\n\t\tHandshakeComplete: true,\n\t}, false)\n\ttest(\"no TLSA, no TLS\", []dns.TLSA{}, tls.ConnectionState{\n\t\tHandshakeComplete: false,\n\t}, false)\n\n\t// RFC 7272, Section 2.2:\n\t// A \"secure\" non-empty TLSA RRset where all the records are unusable:\n\t//  Any connection to the MTA MUST be made via TLS, but authentication\n\t//  is not required.\n\ttest(\"unusable TLSA, TLS\", []dns.TLSA{\n\t\tsingleTlsaRecord(4, 1, 2, \"whatever\"),\n\t\tsingleTlsaRecord(4, 5, 2, \"whatever\"),\n\t\tsingleTlsaRecord(4, 1, 1, \"whatever\"),\n\t}, tls.ConnectionState{\n\t\tHandshakeComplete: true,\n\t\tPeerCertificates:  []*x509.Certificate{parsePEMCert(leafA)},\n\t}, false)\n\ttest(\"unusable TLSA, no TLS\", []dns.TLSA{\n\t\tsingleTlsaRecord(4, 1, 2, \"whatever\"),\n\t}, tls.ConnectionState{\n\t\tHandshakeComplete: false,\n\t}, true)\n\n\t// RFC 7672, Section 2.2:\n\t// A \"secure\" TLSA RRset with at least one usable record:  Any\n\t//  connection to the MTA MUST employ TLS encryption and MUST\n\t//  authenticate the SMTP server using the techniques discussed in the\n\t//  rest of this document.\n\ttest(\"DANE-EE, non-self-signed\", []dns.TLSA{\n\t\tsingleTlsaRecord(3, 1, 1, keySHA256(leafA)),\n\t}, tls.ConnectionState{\n\t\tHandshakeComplete: true,\n\t\tPeerCertificates:  []*x509.Certificate{parsePEMCert(leafA)},\n\t}, false)\n\ttest(\"DANE-EE, multiple records\", []dns.TLSA{\n\t\tsingleTlsaRecord(3, 1, 1, keySHA256(leafB)),\n\t\tsingleTlsaRecord(3, 1, 1, keySHA256(leafA)),\n\t}, tls.ConnectionState{\n\t\tHandshakeComplete: true,\n\t\tPeerCertificates:  []*x509.Certificate{parsePEMCert(leafA)},\n\t}, false)\n\ttest(\"DANE-EE, self-signed\", []dns.TLSA{\n\t\tsingleTlsaRecord(3, 1, 1, keySHA256(rootA)),\n\t}, tls.ConnectionState{\n\t\tHandshakeComplete: true,\n\t\tPeerCertificates:  []*x509.Certificate{parsePEMCert(rootA)},\n\t}, false)\n\ttest(\"DANE-TA, intermediate TA\", []dns.TLSA{\n\t\tsingleTlsaRecord(2, 1, 1, keySHA256(intermediateA)),\n\t}, tls.ConnectionState{\n\t\tHandshakeComplete: true,\n\t\tPeerCertificates: []*x509.Certificate{\n\t\t\tparsePEMCert(leafA),\n\t\t\tparsePEMCert(intermediateA),\n\t\t\tparsePEMCert(rootA),\n\t\t},\n\t}, false)\n\ttest(\"DANE-TA, intermediate TA, mismatch\", []dns.TLSA{\n\t\tsingleTlsaRecord(2, 1, 1, keySHA256(intermediateA)),\n\t}, tls.ConnectionState{\n\t\tHandshakeComplete: true,\n\t\tPeerCertificates: []*x509.Certificate{\n\t\t\tparsePEMCert(leafB),\n\t\t\tparsePEMCert(rootB),\n\t\t},\n\t}, true)\n\ttest(\"DANE-TA, intermediate TA, multiple records\", []dns.TLSA{\n\t\tsingleTlsaRecord(2, 1, 1, keySHA256(rootB)),\n\t\tsingleTlsaRecord(2, 1, 1, keySHA256(intermediateA)),\n\t\t// Add multiple times to make sure that multiple records matching the\n\t\t// same cert do not break anything.\n\t\tsingleTlsaRecord(2, 1, 1, keySHA256(intermediateA)),\n\t}, tls.ConnectionState{\n\t\tHandshakeComplete: true,\n\t\tPeerCertificates: []*x509.Certificate{\n\t\t\tparsePEMCert(leafA),\n\t\t\tparsePEMCert(intermediateA),\n\t\t\tparsePEMCert(rootA),\n\t\t},\n\t}, false)\n}\n"
  },
  {
    "path": "internal/target/remote/debugflags.go",
    "content": "//go:build debugflags\n// +build debugflags\n\n/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage remote\n\nimport (\n\tmaddycli \"github.com/foxcpp/maddy/internal/cli\"\n\t\"github.com/urfave/cli/v2\"\n)\n\nfunc init() {\n\tmaddycli.AddGlobalFlag(&cli.StringFlag{\n\t\tName:        \"debug.smtpport\",\n\t\tUsage:       \"SMTP port to use for connections in tests\",\n\t\tDestination: &smtpPort,\n\t})\n}\n"
  },
  {
    "path": "internal/target/remote/metrics.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage remote\n\nimport \"github.com/prometheus/client_golang/prometheus\"\n\nvar mxLevelCnt = prometheus.NewCounterVec(\n\tprometheus.CounterOpts{\n\t\tNamespace: \"maddy\",\n\t\tSubsystem: \"remote\",\n\t\tName:      \"conns_mx_level\",\n\t\tHelp:      \"Outbound connections established with specific MX security level\",\n\t},\n\t[]string{\"module\", \"level\"},\n)\n\nvar tlsLevelCnt = prometheus.NewCounterVec(\n\tprometheus.CounterOpts{\n\t\tNamespace: \"maddy\",\n\t\tSubsystem: \"remote\",\n\t\tName:      \"conns_tls_level\",\n\t\tHelp:      \"Outbound connections established with specific TLS security level\",\n\t},\n\t[]string{\"module\", \"level\"},\n)\n\nfunc init() {\n\tprometheus.MustRegister(mxLevelCnt)\n\tprometheus.MustRegister(tlsLevelCnt)\n}\n"
  },
  {
    "path": "internal/target/remote/mxauth_test.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage remote\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"net\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"github.com/emersion/go-smtp\"\n\t\"github.com/foxcpp/go-mockdns\"\n\t\"github.com/foxcpp/go-mtasts\"\n\t\"github.com/foxcpp/maddy/framework/dns\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/internal/testutils\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestRemoteDelivery_AuthMX_MTASTS(t *testing.T) {\n\tclientCfg, be, srv := testutils.SMTPServerSTARTTLS(t, \"127.0.0.1:\"+smtpPort)\n\tdefer func() {\n\t\trequire.NoError(t, srv.Close())\n\t}()\n\tdefer testutils.CheckSMTPConnLeak(t, srv)\n\tzones := map[string]mockdns.Zone{\n\t\t\"example.invalid.\": {\n\t\t\tMX: []net.MX{{Host: \"mx.example.invalid.\", Pref: 10}},\n\t\t},\n\t\t\"mx.example.invalid.\": {\n\t\t\tA: []string{\"127.0.0.1\"},\n\t\t},\n\t}\n\n\tmtastsGet := func(_ context.Context, domain string) (*mtasts.Policy, error) {\n\t\tif domain != \"example.invalid\" {\n\t\t\treturn nil, errors.New(\"Wrong domain in lookup\")\n\t\t}\n\n\t\treturn &mtasts.Policy{\n\t\t\t// Testing policy is enough.\n\t\t\tMode: mtasts.ModeTesting,\n\t\t\tMX:   []string{\"mx.example.invalid\"},\n\t\t}, nil\n\t}\n\n\ttgt := testTarget(t, zones, nil, []module.MXAuthPolicy{\n\t\ttestSTSPolicy(t, zones, mtastsGet),\n\t})\n\ttgt.tlsConfig = clientCfg\n\tdefer func() {\n\t\tassert.NoError(t, tgt.Stop())\n\t}()\n\n\ttestutils.DoTestDelivery(t, tgt, \"test@example.com\", []string{\"test@example.invalid\"})\n\tbe.CheckMsg(t, 0, \"test@example.com\", []string{\"test@example.invalid\"})\n}\n\nfunc TestRemoteDelivery_MTASTS_SkipNonMatching(t *testing.T) {\n\t_, be1, srv1 := testutils.SMTPServerSTARTTLS(t, \"127.0.0.1:\"+smtpPort)\n\tdefer func() {\n\t\trequire.NoError(t, srv1.Close())\n\t}()\n\tdefer testutils.CheckSMTPConnLeak(t, srv1)\n\n\tclientCfg, be, srv := testutils.SMTPServerSTARTTLS(t, \"127.0.0.2:\"+smtpPort)\n\tdefer func() {\n\t\trequire.NoError(t, srv.Close())\n\t}()\n\tdefer testutils.CheckSMTPConnLeak(t, srv)\n\tzones := map[string]mockdns.Zone{\n\t\t\"example.invalid.\": {\n\t\t\tMX: []net.MX{\n\t\t\t\t{Host: \"mx2.example.invalid.\", Pref: 5},\n\t\t\t\t{Host: \"mx1.example.invalid.\", Pref: 10},\n\t\t\t},\n\t\t},\n\t\t\"mx1.example.invalid.\": {\n\t\t\tA: []string{\"127.0.0.1\"},\n\t\t},\n\t\t\"mx2.example.invalid.\": {\n\t\t\tA: []string{\"127.0.0.2\"},\n\t\t},\n\t}\n\n\tmtastsGet := func(_ context.Context, domain string) (*mtasts.Policy, error) {\n\t\tif domain != \"example.invalid\" {\n\t\t\treturn nil, errors.New(\"Wrong domain in lookup\")\n\t\t}\n\n\t\treturn &mtasts.Policy{\n\t\t\tMode: mtasts.ModeEnforce,\n\t\t\tMX:   []string{\"mx2.example.invalid\"},\n\t\t}, nil\n\t}\n\n\ttgt := testTarget(t, zones, nil, []module.MXAuthPolicy{\n\t\ttestSTSPolicy(t, zones, mtastsGet),\n\t\t&localPolicy{minMXLevel: module.MX_MTASTS},\n\t})\n\ttgt.tlsConfig = clientCfg\n\tdefer func() {\n\t\tassert.NoError(t, tgt.Stop())\n\t}()\n\n\ttestutils.DoTestDelivery(t, tgt, \"test@example.com\", []string{\"test@example.invalid\"})\n\tbe.CheckMsg(t, 0, \"test@example.com\", []string{\"test@example.invalid\"})\n\n\tif be1.MailFromCounter != 0 {\n\t\tt.Fatal(\"MAIL FROM issued for server failing authentication\")\n\t}\n}\n\nfunc TestRemoteDelivery_AuthMX_MTASTS_Fail(t *testing.T) {\n\tclientCfg, be1, srv1 := testutils.SMTPServerSTARTTLS(t, \"127.0.0.1:\"+smtpPort)\n\tdefer func() {\n\t\tassert.NoError(t, srv1.Close())\n\t}()\n\tdefer testutils.CheckSMTPConnLeak(t, srv1)\n\n\tzones := map[string]mockdns.Zone{\n\t\t\"example.invalid.\": {\n\t\t\tMX: []net.MX{{Host: \"mx.example.invalid.\", Pref: 10}},\n\t\t},\n\t\t\"mx.example.invalid.\": {\n\t\t\tA: []string{\"127.0.0.1\"},\n\t\t},\n\t}\n\n\tmtastsGet := func(_ context.Context, domain string) (*mtasts.Policy, error) {\n\t\tif domain != \"example.invalid\" {\n\t\t\treturn nil, errors.New(\"Wrong domain in lookup\")\n\t\t}\n\n\t\treturn &mtasts.Policy{\n\t\t\tMode: mtasts.ModeTesting,\n\t\t\tMX:   []string{\"mx4.example.invalid\"}, // not mx.example.invalid!\n\t\t}, nil\n\t}\n\n\ttgt := testTarget(t, zones, nil, []module.MXAuthPolicy{\n\t\ttestSTSPolicy(t, zones, mtastsGet),\n\t\t&localPolicy{minMXLevel: module.MX_MTASTS},\n\t})\n\ttgt.tlsConfig = clientCfg\n\tdefer func() {\n\t\tassert.NoError(t, tgt.Stop())\n\t}()\n\n\t_, err := testutils.DoTestDeliveryErr(t, tgt, \"test@example.com\", []string{\"test@example.invalid\"})\n\tif err == nil {\n\t\tt.Fatal(\"Expected an error, got none\")\n\t}\n\n\tif be1.MailFromCounter != 0 {\n\t\tt.Fatal(\"MAIL FROM issued for server failing authentication\")\n\t}\n}\n\nfunc TestRemoteDelivery_AuthMX_MTASTS_NoTLS(t *testing.T) {\n\tbe1, srv1 := testutils.SMTPServer(t, \"127.0.0.1:\"+smtpPort)\n\tdefer func() {\n\t\tassert.NoError(t, srv1.Close())\n\t}()\n\tdefer testutils.CheckSMTPConnLeak(t, srv1)\n\n\tzones := map[string]mockdns.Zone{\n\t\t\"example.invalid.\": {\n\t\t\tMX: []net.MX{{Host: \"mx.example.invalid.\", Pref: 10}},\n\t\t},\n\t\t\"mx.example.invalid.\": {\n\t\t\tA: []string{\"127.0.0.1\"},\n\t\t},\n\t}\n\n\tmtastsGet := func(_ context.Context, domain string) (*mtasts.Policy, error) {\n\t\tif domain != \"example.invalid\" {\n\t\t\treturn nil, errors.New(\"Wrong domain in lookup\")\n\t\t}\n\n\t\treturn &mtasts.Policy{\n\t\t\tMode: mtasts.ModeEnforce,\n\t\t\tMX:   []string{\"mx.example.invalid\"},\n\t\t}, nil\n\t}\n\n\ttgt := testTarget(t, zones, nil, []module.MXAuthPolicy{\n\t\ttestSTSPolicy(t, zones, mtastsGet),\n\t\t&localPolicy{minMXLevel: module.MX_MTASTS},\n\t})\n\tdefer func() {\n\t\tassert.NoError(t, tgt.Stop())\n\t}()\n\n\t_, err := testutils.DoTestDeliveryErr(t, tgt, \"test@example.com\", []string{\"test@example.invalid\"})\n\tif err == nil {\n\t\tt.Fatal(\"Expected an error, got none\")\n\t}\n\n\tif be1.MailFromCounter != 0 {\n\t\tt.Fatal(\"MAIL FROM issued for server failing authentication\")\n\t}\n}\n\nfunc TestRemoteDelivery_AuthMX_MTASTS_RequirePKIX(t *testing.T) {\n\t_, be1, srv1 := testutils.SMTPServerSTARTTLS(t, \"127.0.0.1:\"+smtpPort)\n\tdefer func() {\n\t\trequire.NoError(t, srv1.Close())\n\t}()\n\tdefer testutils.CheckSMTPConnLeak(t, srv1)\n\n\tzones := map[string]mockdns.Zone{\n\t\t\"example.invalid.\": {\n\t\t\tMX: []net.MX{{Host: \"mx.example.invalid.\", Pref: 10}},\n\t\t},\n\t\t\"mx.example.invalid.\": {\n\t\t\tA: []string{\"127.0.0.1\"},\n\t\t},\n\t}\n\n\tmtastsGet := func(_ context.Context, domain string) (*mtasts.Policy, error) {\n\t\tif domain != \"example.invalid\" {\n\t\t\treturn nil, errors.New(\"Wrong domain in lookup\")\n\t\t}\n\n\t\treturn &mtasts.Policy{\n\t\t\tMode: mtasts.ModeEnforce,\n\t\t\tMX:   []string{\"mx.example.invalid\"},\n\t\t}, nil\n\t}\n\n\ttgt := testTarget(t, zones, nil, []module.MXAuthPolicy{\n\t\ttestSTSPolicy(t, zones, mtastsGet),\n\t\t&localPolicy{minMXLevel: module.MX_MTASTS},\n\t})\n\tdefer func(tgt *Target) {\n\t\terr := tgt.Stop()\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t}(tgt)\n\n\t_, err := testutils.DoTestDeliveryErr(t, tgt, \"test@example.com\", []string{\"test@example.invalid\"})\n\tif err == nil {\n\t\tt.Fatal(\"Expected an error, got none\")\n\t}\n\n\tif be1.MailFromCounter != 0 {\n\t\tt.Fatal(\"MAIL FROM issued for server failing authentication\")\n\t}\n}\n\nfunc TestRemoteDelivery_AuthMX_MTASTS_NoPolicy(t *testing.T) {\n\t// At the moment, implementation ensures all MX policy checks are completed\n\t// before attempting to connect.\n\t// However, we cannot run complete go-smtp server to check whether it is\n\t// violated and the connection is actually estabilished since this causes\n\t// weird race conditions when test completes before go-smtp has the\n\t// chance to fully initialize itself (Serve is still at the conn.listeners\n\t// assignment when Close is called).\n\t//\n\t// The issue was resolved upstream by introducing locking around internal\n\t// listeners slice use. Uses of FailOnConn remain since they pretty much do\n\t// not hurt.\n\t//\n\t// https://builds.sr.ht/~emersion/job/147975\n\ttarpit := testutils.FailOnConn(t, \"127.0.0.1:\"+smtpPort)\n\tdefer func() {\n\t\trequire.NoError(t, tarpit.Close())\n\t}()\n\n\tzones := map[string]mockdns.Zone{\n\t\t\"example.invalid.\": {\n\t\t\tMX: []net.MX{{Host: \"mx.example.invalid.\", Pref: 10}},\n\t\t},\n\t\t\"mx.example.invalid.\": {\n\t\t\tA: []string{\"127.0.0.1\"},\n\t\t},\n\t}\n\n\tmtastsGet := func(_ context.Context, domain string) (*mtasts.Policy, error) {\n\t\tif domain != \"example.invalid\" {\n\t\t\treturn nil, errors.New(\"Wrong domain in lookup\")\n\t\t}\n\n\t\treturn nil, mtasts.ErrNoPolicy\n\t}\n\n\ttgt := testTarget(t, zones, nil, []module.MXAuthPolicy{\n\t\ttestSTSPolicy(t, zones, mtastsGet),\n\t\t&localPolicy{minMXLevel: module.MX_MTASTS},\n\t})\n\tdefer func() {\n\t\tassert.NoError(t, tgt.Stop())\n\t}()\n\n\t_, err := testutils.DoTestDeliveryErr(t, tgt, \"test@example.com\", []string{\"test@example.invalid\"})\n\tif err == nil {\n\t\tt.Fatal(\"Expected an error, got none\")\n\t}\n}\n\nfunc TestRemoteDelivery_AuthMX_DNSSEC(t *testing.T) {\n\tbe, srv := testutils.SMTPServer(t, \"127.0.0.1:\"+smtpPort)\n\tdefer func() {\n\t\trequire.NoError(t, srv.Close())\n\t}()\n\tdefer testutils.CheckSMTPConnLeak(t, srv)\n\n\tzones := map[string]mockdns.Zone{\n\t\t\"example.invalid.\": {\n\t\t\tAD: true,\n\t\t\tMX: []net.MX{{Host: \"mx.example.invalid.\", Pref: 10}},\n\t\t},\n\t\t\"mx.example.invalid.\": {\n\t\t\tA: []string{\"127.0.0.1\"},\n\t\t},\n\t}\n\n\tl := testutils.Logger(t, \"mockdns\")\n\tdnsSrv, err := mockdns.NewServerWithLogger(zones, l, false)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer func() {\n\t\trequire.NoError(t, dnsSrv.Close())\n\t}()\n\n\tdialer := net.Dialer{}\n\tdialer.Resolver = &net.Resolver{}\n\tdnsSrv.PatchNet(dialer.Resolver)\n\taddr := dnsSrv.LocalAddr().(*net.UDPAddr)\n\n\textResolver, err := dns.NewExtResolver()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\textResolver.Cfg.Servers = []string{addr.IP.String()}\n\textResolver.Cfg.Port = strconv.Itoa(addr.Port)\n\n\ttgt := testTarget(t, zones, extResolver, nil)\n\tdefer func() {\n\t\tassert.NoError(t, tgt.Stop())\n\t}()\n\n\ttestutils.DoTestDelivery(t, tgt, \"test@example.com\", []string{\"test@example.invalid\"})\n\tbe.CheckMsg(t, 0, \"test@example.com\", []string{\"test@example.invalid\"})\n}\n\nfunc TestRemoteDelivery_AuthMX_DNSSEC_Fail(t *testing.T) {\n\tbe, srv := testutils.SMTPServer(t, \"127.0.0.1:\"+smtpPort)\n\tdefer func() {\n\t\trequire.NoError(t, srv.Close())\n\t}()\n\tdefer testutils.CheckSMTPConnLeak(t, srv)\n\n\tzones := map[string]mockdns.Zone{\n\t\t\"example.invalid.\": {\n\t\t\tMX: []net.MX{{Host: \"mx.example.invalid.\", Pref: 10}},\n\t\t},\n\t\t\"mx.example.invalid.\": {\n\t\t\tA: []string{\"127.0.0.1\"},\n\t\t},\n\t}\n\n\tl := testutils.Logger(t, \"mockdns\")\n\tdnsSrv, err := mockdns.NewServerWithLogger(zones, l, false)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer func() {\n\t\trequire.NoError(t, dnsSrv.Close())\n\t}()\n\n\tdialer := net.Dialer{}\n\tdialer.Resolver = &net.Resolver{}\n\tdnsSrv.PatchNet(dialer.Resolver)\n\taddr := dnsSrv.LocalAddr().(*net.UDPAddr)\n\n\textResolver, err := dns.NewExtResolver()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\textResolver.Cfg.Servers = []string{addr.IP.String()}\n\textResolver.Cfg.Port = strconv.Itoa(addr.Port)\n\n\ttgt := testTarget(t, zones, extResolver, []module.MXAuthPolicy{\n\t\t&localPolicy{minMXLevel: module.MX_DNSSEC},\n\t})\n\tdefer func() {\n\t\tassert.NoError(t, tgt.Stop())\n\t}()\n\n\t_, err = testutils.DoTestDeliveryErr(t, tgt, \"test@example.com\", []string{\"test@example.invalid\"})\n\tif err == nil {\n\t\tt.Fatal(\"Expected an error, got none\")\n\t}\n\n\tif be.MailFromCounter != 0 {\n\t\tt.Fatal(\"MAIL FROM issued for server failing authentication\")\n\t}\n}\n\nfunc TestRemoteDelivery_REQUIRETLS(t *testing.T) {\n\tclientCfg, be, srv := testutils.SMTPServerSTARTTLS(t, \"127.0.0.1:\"+smtpPort)\n\tsrv.EnableREQUIRETLS = true\n\tdefer func() {\n\t\trequire.NoError(t, srv.Close())\n\t}()\n\tdefer testutils.CheckSMTPConnLeak(t, srv)\n\tzones := map[string]mockdns.Zone{\n\t\t\"example.invalid.\": {\n\t\t\tMX: []net.MX{{Host: \"mx.example.invalid.\", Pref: 10}},\n\t\t},\n\t\t\"mx.example.invalid.\": {\n\t\t\tA: []string{\"127.0.0.1\"},\n\t\t},\n\t}\n\n\tmtastsGet := func(_ context.Context, domain string) (*mtasts.Policy, error) {\n\t\tif domain != \"example.invalid\" {\n\t\t\treturn nil, errors.New(\"Wrong domain in lookup\")\n\t\t}\n\n\t\treturn &mtasts.Policy{\n\t\t\t// Testing policy is enough.\n\t\t\tMode: mtasts.ModeTesting,\n\t\t\tMX:   []string{\"mx.example.invalid\"},\n\t\t}, nil\n\t}\n\n\ttgt := testTarget(t, zones, nil, []module.MXAuthPolicy{\n\t\ttestSTSPolicy(t, zones, mtastsGet),\n\t})\n\ttgt.tlsConfig = clientCfg\n\tdefer func() {\n\t\tassert.NoError(t, tgt.Stop())\n\t}()\n\n\ttestutils.DoTestDeliveryMeta(t, tgt, \"test@example.com\", []string{\"test@example.invalid\"}, &module.MsgMetadata{\n\t\tOriginalFrom: \"test@example.com\",\n\t\tSMTPOpts: smtp.MailOptions{\n\t\t\tRequireTLS: true,\n\t\t},\n\t})\n\tbe.CheckMsg(t, 0, \"test@example.com\", []string{\"test@example.invalid\"})\n}\n\nfunc TestRemoteDelivery_REQUIRETLS_Fail(t *testing.T) {\n\tclientCfg, be, srv := testutils.SMTPServerSTARTTLS(t, \"127.0.0.1:\"+smtpPort)\n\tsrv.EnableREQUIRETLS = false /* no REQUIRETLS */\n\tdefer func() {\n\t\trequire.NoError(t, srv.Close())\n\t}()\n\tdefer testutils.CheckSMTPConnLeak(t, srv)\n\tzones := map[string]mockdns.Zone{\n\t\t\"example.invalid.\": {\n\t\t\tMX: []net.MX{{Host: \"mx.example.invalid.\", Pref: 10}},\n\t\t},\n\t\t\"mx.example.invalid.\": {\n\t\t\tA: []string{\"127.0.0.1\"},\n\t\t},\n\t}\n\n\tmtastsGet := func(_ context.Context, domain string) (*mtasts.Policy, error) {\n\t\tif domain != \"example.invalid\" {\n\t\t\treturn nil, errors.New(\"Wrong domain in lookup\")\n\t\t}\n\n\t\treturn &mtasts.Policy{\n\t\t\t// Testing policy is enough.\n\t\t\tMode: mtasts.ModeTesting,\n\t\t\tMX:   []string{\"mx.example.invalid\"},\n\t\t}, nil\n\t}\n\n\ttgt := testTarget(t, zones, nil, []module.MXAuthPolicy{\n\t\ttestSTSPolicy(t, zones, mtastsGet),\n\t})\n\ttgt.tlsConfig = clientCfg\n\tdefer func() {\n\t\tassert.NoError(t, tgt.Stop())\n\t}()\n\n\tif _, err := testutils.DoTestDeliveryErrMeta(t, tgt, \"test@example.com\", []string{\"test@example.invalid\"}, &module.MsgMetadata{\n\t\tOriginalFrom: \"test@example.com\",\n\t\tSMTPOpts: smtp.MailOptions{\n\t\t\tRequireTLS: true,\n\t\t},\n\t}); err == nil {\n\t\tt.Error(\"Expected an error, got none\")\n\t}\n\tif be.MailFromCounter != 0 {\n\t\tt.Fatal(\"MAIL FROM issued for server failing authentication\")\n\t}\n}\n\nfunc TestRemoteDelivery_REQUIRETLS_Relaxed(t *testing.T) {\n\tclientCfg, be, srv := testutils.SMTPServerSTARTTLS(t, \"127.0.0.1:\"+smtpPort)\n\tsrv.EnableREQUIRETLS = false /* no REQUIRETLS */\n\tdefer func() {\n\t\trequire.NoError(t, srv.Close())\n\t}()\n\tdefer testutils.CheckSMTPConnLeak(t, srv)\n\tzones := map[string]mockdns.Zone{\n\t\t\"example.invalid.\": {\n\t\t\tMX: []net.MX{{Host: \"mx.example.invalid.\", Pref: 10}},\n\t\t},\n\t\t\"mx.example.invalid.\": {\n\t\t\tA: []string{\"127.0.0.1\"},\n\t\t},\n\t}\n\n\tmtastsGet := func(_ context.Context, domain string) (*mtasts.Policy, error) {\n\t\tif domain != \"example.invalid\" {\n\t\t\treturn nil, errors.New(\"Wrong domain in lookup\")\n\t\t}\n\n\t\treturn &mtasts.Policy{\n\t\t\t// Testing policy is enough.\n\t\t\tMode: mtasts.ModeTesting,\n\t\t\tMX:   []string{\"mx.example.invalid\"},\n\t\t}, nil\n\t}\n\n\ttgt := testTarget(t, zones, nil, []module.MXAuthPolicy{\n\t\ttestSTSPolicy(t, zones, mtastsGet),\n\t})\n\ttgt.relaxedREQUIRETLS = true\n\ttgt.tlsConfig = clientCfg\n\tdefer func() {\n\t\tassert.NoError(t, tgt.Stop())\n\t}()\n\n\ttestutils.DoTestDeliveryMeta(t, tgt, \"test@example.com\", []string{\"test@example.invalid\"}, &module.MsgMetadata{\n\t\tOriginalFrom: \"test@example.com\",\n\t\tSMTPOpts: smtp.MailOptions{\n\t\t\tRequireTLS: true,\n\t\t},\n\t})\n\tbe.CheckMsg(t, 0, \"test@example.com\", []string{\"test@example.invalid\"})\n}\n\nfunc TestRemoteDelivery_REQUIRETLS_Relaxed_NoMXAuth(t *testing.T) {\n\tclientCfg, be, srv := testutils.SMTPServerSTARTTLS(t, \"127.0.0.1:\"+smtpPort)\n\tsrv.EnableREQUIRETLS = false /* no REQUIRETLS */\n\tdefer func() {\n\t\trequire.NoError(t, srv.Close())\n\t}()\n\tdefer testutils.CheckSMTPConnLeak(t, srv)\n\tzones := map[string]mockdns.Zone{\n\t\t\"example.invalid.\": {\n\t\t\tMX: []net.MX{{Host: \"mx.example.invalid.\", Pref: 10}},\n\t\t},\n\t\t\"mx.example.invalid.\": {\n\t\t\tA: []string{\"127.0.0.1\"},\n\t\t},\n\t}\n\n\tmtastsGet := func(_ context.Context, domain string) (*mtasts.Policy, error) {\n\t\tif domain != \"example.invalid\" {\n\t\t\treturn nil, errors.New(\"Wrong domain in lookup\")\n\t\t}\n\t\treturn nil, mtasts.ErrNoPolicy\n\t}\n\n\ttgt := testTarget(t, zones, nil, []module.MXAuthPolicy{\n\t\ttestSTSPolicy(t, zones, mtastsGet),\n\t})\n\ttgt.relaxedREQUIRETLS = true\n\ttgt.tlsConfig = clientCfg\n\tdefer func() {\n\t\tassert.NoError(t, tgt.Stop())\n\t}()\n\n\tif _, err := testutils.DoTestDeliveryErrMeta(t, tgt, \"test@example.com\", []string{\"test@example.invalid\"}, &module.MsgMetadata{\n\t\tOriginalFrom: \"test@example.com\",\n\t\tSMTPOpts: smtp.MailOptions{\n\t\t\tRequireTLS: true,\n\t\t},\n\t}); err == nil {\n\t\tt.Error(\"Expected an error, got none\")\n\t}\n\tif be.MailFromCounter != 0 {\n\t\tt.Fatal(\"MAIL FROM issued for server failing authentication\")\n\t}\n}\n\nfunc TestRemoteDelivery_REQUIRETLS_Relaxed_NoTLS(t *testing.T) {\n\tbe, srv := testutils.SMTPServer(t, \"127.0.0.1:\"+smtpPort)\n\tsrv.EnableREQUIRETLS = false /* no REQUIRETLS */\n\tdefer func() {\n\t\trequire.NoError(t, srv.Close())\n\t}()\n\tdefer testutils.CheckSMTPConnLeak(t, srv)\n\tzones := map[string]mockdns.Zone{\n\t\t\"example.invalid.\": {\n\t\t\tMX: []net.MX{{Host: \"mx.example.invalid.\", Pref: 10}},\n\t\t},\n\t\t\"mx.example.invalid.\": {\n\t\t\tA: []string{\"127.0.0.1\"},\n\t\t},\n\t}\n\n\tmtastsGet := func(_ context.Context, domain string) (*mtasts.Policy, error) {\n\t\tif domain != \"example.invalid\" {\n\t\t\treturn nil, errors.New(\"Wrong domain in lookup\")\n\t\t}\n\n\t\treturn &mtasts.Policy{\n\t\t\t// Testing policy is enough.\n\t\t\tMode: mtasts.ModeTesting,\n\t\t\tMX:   []string{\"mx.example.invalid\"},\n\t\t}, nil\n\t}\n\n\ttgt := testTarget(t, zones, nil, []module.MXAuthPolicy{\n\t\ttestSTSPolicy(t, zones, mtastsGet),\n\t})\n\ttgt.relaxedREQUIRETLS = true\n\ttgt.tlsConfig = nil\n\tdefer func() {\n\t\tassert.NoError(t, tgt.Stop())\n\t}()\n\n\tif _, err := testutils.DoTestDeliveryErrMeta(t, tgt, \"test@example.com\", []string{\"test@example.invalid\"}, &module.MsgMetadata{\n\t\tOriginalFrom: \"test@example.com\",\n\t\tSMTPOpts: smtp.MailOptions{\n\t\t\tRequireTLS: true,\n\t\t},\n\t}); err == nil {\n\t\tt.Error(\"Expected an error, got none\")\n\t}\n\tif be.MailFromCounter != 0 {\n\t\tt.Fatal(\"MAIL FROM issued for server failing authentication\")\n\t}\n}\n\nfunc TestRemoteDelivery_REQUIRETLS_Relaxed_TLSFail(t *testing.T) {\n\tclientCfg, be, srv := testutils.SMTPServerSTARTTLS(t, \"127.0.0.1:\"+smtpPort)\n\tsrv.EnableREQUIRETLS = false /* no REQUIRETLS */\n\tdefer func() {\n\t\trequire.NoError(t, srv.Close())\n\t}()\n\tdefer testutils.CheckSMTPConnLeak(t, srv)\n\tzones := map[string]mockdns.Zone{\n\t\t\"example.invalid.\": {\n\t\t\tMX: []net.MX{{Host: \"mx.example.invalid.\", Pref: 10}},\n\t\t},\n\t\t\"mx.example.invalid.\": {\n\t\t\tA: []string{\"127.0.0.1\"},\n\t\t},\n\t}\n\n\tmtastsGet := func(_ context.Context, domain string) (*mtasts.Policy, error) {\n\t\tif domain != \"example.invalid\" {\n\t\t\treturn nil, errors.New(\"Wrong domain in lookup\")\n\t\t}\n\n\t\treturn &mtasts.Policy{\n\t\t\t// Testing policy is enough.\n\t\t\tMode: mtasts.ModeTesting,\n\t\t\tMX:   []string{\"mx.example.invalid\"},\n\t\t}, nil\n\t}\n\n\ttgt := testTarget(t, zones, nil, []module.MXAuthPolicy{\n\t\ttestSTSPolicy(t, zones, mtastsGet),\n\t})\n\ttgt.relaxedREQUIRETLS = true\n\t// Cause failure through version incompatibility.\n\tclientCfg.MaxVersion = tls.VersionTLS12\n\tclientCfg.MinVersion = tls.VersionTLS12\n\tsrv.TLSConfig.MinVersion = tls.VersionTLS11\n\tsrv.TLSConfig.MaxVersion = tls.VersionTLS11\n\ttgt.tlsConfig = clientCfg\n\tdefer func() {\n\t\tassert.NoError(t, tgt.Stop())\n\t}()\n\n\tif _, err := testutils.DoTestDeliveryErrMeta(t, tgt, \"test@example.com\", []string{\"test@example.invalid\"}, &module.MsgMetadata{\n\t\tOriginalFrom: \"test@example.com\",\n\t\tSMTPOpts: smtp.MailOptions{\n\t\t\tRequireTLS: true,\n\t\t},\n\t}); err == nil {\n\t\tt.Error(\"Expected an error, got none\")\n\t}\n\tif be.MailFromCounter != 0 {\n\t\tt.Fatal(\"MAIL FROM issued for server failing authentication\")\n\t}\n}\n"
  },
  {
    "path": "internal/target/remote/policy_group.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage remote\n\nimport (\n\t\"github.com/foxcpp/maddy/framework/config\"\n\tmodconfig \"github.com/foxcpp/maddy/framework/config/module\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n)\n\n// PolicyGroup is a module container for a group of Policy implementations.\n//\n// It allows to share a set of policy configurations between remote target\n// instances using named configuration blocks (module instances) system.\n//\n// It is registered globally under the name 'mx_auth'. This is also the name of\n// corresponding remote target configuration directive. The object does not\n// implement any standard module interfaces besides module.Module and is\n// specific to the remote target.\ntype PolicyGroup struct {\n\tL        []module.MXAuthPolicy\n\tinstName string\n\tpols     map[string]module.MXAuthPolicy\n}\n\nfunc (pg *PolicyGroup) Configure(inlineArgs []string, cfg *config.Map) error {\n\tvar debugLog bool\n\tcfg.Bool(\"debug\", true, false, &debugLog)\n\tcfg.AllowUnknown()\n\tother, err := cfg.Process()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Policies have defined application order since some of them depend on\n\t// results of other policies. We first initialize them in the order they\n\t// are defined in and then reorder depending on the needed order.\n\n\tfor _, block := range other {\n\t\tif _, ok := pg.pols[block.Name]; ok {\n\t\t\treturn config.NodeErr(block, \"duplicate policy block: %v\", block.Name)\n\t\t}\n\n\t\tvar policy module.MXAuthPolicy\n\t\terr := modconfig.ModuleFromNode(\"mx_auth\", append([]string{block.Name}, block.Args...), block, cfg.Globals, &policy)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tpg.pols[block.Name] = policy\n\t}\n\n\tfor _, name := range [...]string{\n\t\t\"mtasts\",\n\t\t// sts_preload should go after mtasts so it will take not effect if\n\t\t// MXLevel is already MX_MTASTS.\n\t\t\"sts_preload\",\n\t\t\"dane\",\n\t\t\"dnssec\",\n\t\t// localPolicy should be the last one, since it considers levels defined by\n\t\t// other policies.\n\t\t\"local_policy\",\n\t} {\n\t\tpolicy, ok := pg.pols[name]\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\t\tpg.L = append(pg.L, policy)\n\t}\n\n\treturn nil\n}\n\nfunc (*PolicyGroup) Name() string {\n\treturn \"mx_auth\"\n}\n\nfunc (pg *PolicyGroup) InstanceName() string {\n\treturn pg.instName\n}\n\nfunc init() {\n\tmodules.Register(\"mx_auth\", func(_ *container.C, _, instName string) (module.Module, error) {\n\t\treturn &PolicyGroup{\n\t\t\tinstName: instName,\n\t\t\tpols:     map[string]module.MXAuthPolicy{},\n\t\t}, nil\n\t})\n}\n"
  },
  {
    "path": "internal/target/remote/remote.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\n// Package remote implements module which does outgoing\n// message delivery using servers discovered using DNS MX records.\n//\n// Implemented interfaces:\n// - module.DeliveryTarget\npackage remote\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"runtime/trace\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/emersion/go-message/textproto\"\n\t\"github.com/emersion/go-smtp\"\n\t\"github.com/foxcpp/maddy/framework/address\"\n\t\"github.com/foxcpp/maddy/framework/buffer\"\n\t\"github.com/foxcpp/maddy/framework/config\"\n\tmodconfig \"github.com/foxcpp/maddy/framework/config/module\"\n\ttls2 \"github.com/foxcpp/maddy/framework/config/tls\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/dns\"\n\t\"github.com/foxcpp/maddy/framework/exterrors\"\n\t\"github.com/foxcpp/maddy/framework/log\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n\t\"github.com/foxcpp/maddy/internal/limits\"\n\t\"github.com/foxcpp/maddy/internal/smtpconn/pool\"\n\t\"github.com/foxcpp/maddy/internal/target\"\n\t\"golang.org/x/net/idna\"\n)\n\nvar smtpPort = \"25\"\n\nfunc moduleError(err error) error {\n\treturn exterrors.WithFields(err, map[string]interface{}{\n\t\t\"target\": \"remote\",\n\t})\n}\n\ntype Target struct {\n\tname      string\n\thostname  string\n\tlocalIP   string\n\tipv4      bool\n\ttlsConfig *tls.Config\n\n\tresolver    dns.Resolver\n\tdialer      func(ctx context.Context, network, addr string) (net.Conn, error)\n\textResolver *dns.ExtResolver\n\n\tpolicies          []module.MXAuthPolicy\n\tlimits            *limits.Group\n\tallowSecOverride  bool\n\trelaxedREQUIRETLS bool\n\n\tpool           *pool.P\n\tconnReuseLimit int\n\n\tlog *log.Logger\n\n\tconnectTimeout    time.Duration\n\tcommandTimeout    time.Duration\n\tsubmissionTimeout time.Duration\n}\n\nvar _ module.DeliveryTarget = &Target{}\n\nfunc New(c *container.C, modName, instName string) (module.Module, error) {\n\t// Keep this synchronized with testTarget.\n\treturn &Target{\n\t\tname:     instName,\n\t\tresolver: dns.DefaultResolver(),\n\t\tdialer:   (&net.Dialer{}).DialContext,\n\t\tlog:      c.DefaultLogger.Sublogger(modName),\n\t}, nil\n}\n\nfunc (rt *Target) Configure(inlineArgs []string, cfg *config.Map) error {\n\tif len(inlineArgs) != 0 {\n\t\treturn errors.New(\"remote: inline arguments are not used\")\n\t}\n\n\tvar err error\n\trt.extResolver, err = dns.NewExtResolver()\n\tif err != nil {\n\t\trt.log.Error(\"cannot initialize DNSSEC-aware resolver, DNSSEC and DANE are not available\", err)\n\t}\n\n\tcfg.String(\"hostname\", true, true, \"\", &rt.hostname)\n\tcfg.String(\"local_ip\", false, false, \"\", &rt.localIP)\n\tcfg.Bool(\"force_ipv4\", false, false, &rt.ipv4)\n\tcfg.Bool(\"debug\", true, false, &rt.log.Debug)\n\tcfg.Custom(\"tls_client\", true, false, func() (interface{}, error) {\n\t\treturn &tls.Config{}, nil\n\t}, tls2.TLSClientBlock, &rt.tlsConfig)\n\tcfg.Custom(\"mx_auth\", false, false, func() (interface{}, error) {\n\t\t// Default is \"no policies\" to follow the principles of explicit\n\t\t// configuration (if it is not requested - it is not done).\n\t\treturn nil, nil\n\t}, func(cfg *config.Map, n config.Node) (interface{}, error) {\n\t\t// Module instance is &PolicyGroup.\n\t\tvar p *PolicyGroup\n\t\tif err := modconfig.GroupFromNode(\"mx_auth\", n.Args, n, cfg.Globals, &p); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn p.L, nil\n\t}, &rt.policies)\n\tcfg.Custom(\"limits\", false, false, func() (interface{}, error) {\n\t\treturn &limits.Group{}, nil\n\t}, func(cfg *config.Map, n config.Node) (interface{}, error) {\n\t\tvar g *limits.Group\n\t\tif err := modconfig.GroupFromNode(\"limits\", n.Args, n, cfg.Globals, &g); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn g, nil\n\t}, &rt.limits)\n\tcfg.Bool(\"requiretls_override\", false, true, &rt.allowSecOverride)\n\tcfg.Bool(\"relaxed_requiretls\", false, true, &rt.relaxedREQUIRETLS)\n\tcfg.Int(\"conn_reuse_limit\", false, false, 10, &rt.connReuseLimit)\n\tcfg.Duration(\"connect_timeout\", false, false, 5*time.Minute, &rt.connectTimeout)\n\tcfg.Duration(\"command_timeout\", false, false, 5*time.Minute, &rt.commandTimeout)\n\tcfg.Duration(\"submission_timeout\", false, false, 5*time.Minute, &rt.submissionTimeout)\n\n\tpoolCfg := pool.Config{\n\t\tMaxKeys:             5000,\n\t\tMaxConnsPerKey:      5,      // basically, max. amount of idle connections in cache\n\t\tMaxConnLifetimeSec:  150,    // 2.5 mins, half of recommended idle time from RFC 5321\n\t\tStaleKeyLifetimeSec: 60 * 4, // make sure that cleanup runs before recommended idle time from RFC 5321\n\t}\n\tcfg.Int(\"conn_max_idle_count\", false, false, 5, &poolCfg.MaxConnsPerKey)\n\tcfg.Int64(\"conn_max_idle_time\", false, false, 150, &poolCfg.MaxConnLifetimeSec)\n\n\tif _, err := cfg.Process(); err != nil {\n\t\treturn err\n\t}\n\trt.pool = pool.New(poolCfg)\n\n\t// INTERNATIONALIZATION: See RFC 6531 Section 3.7.1.\n\trt.hostname, err = idna.ToASCII(rt.hostname)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"remote: cannot represent the hostname as an A-label name: %w\", err)\n\t}\n\n\tif rt.localIP != \"\" {\n\t\taddr, err := net.ResolveTCPAddr(\"tcp\", rt.localIP+\":0\")\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"remote: failed to parse local IP: %w\", err)\n\t\t}\n\t\trt.dialer = (&net.Dialer{\n\t\t\tLocalAddr: addr,\n\t\t}).DialContext\n\t}\n\tif rt.ipv4 {\n\t\tdial := rt.dialer\n\t\trt.dialer = func(ctx context.Context, network, addr string) (net.Conn, error) {\n\t\t\tif network == \"tcp\" {\n\t\t\t\tnetwork = \"tcp4\"\n\t\t\t}\n\t\t\treturn dial(ctx, network, addr)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (rt *Target) Start() error {\n\treturn nil\n}\n\nfunc (rt *Target) Stop() error {\n\trt.pool.Close()\n\n\treturn nil\n}\n\nfunc (rt *Target) Name() string {\n\treturn \"remote\"\n}\n\nfunc (rt *Target) InstanceName() string {\n\treturn rt.name\n}\n\ntype remoteDelivery struct {\n\trt       *Target\n\tmailFrom string\n\tmsgMeta  *module.MsgMetadata\n\tlog      *log.Logger\n\n\trecipients  []string\n\tconnections map[string]*mxConn\n\n\tpolicies []module.DeliveryMXAuthPolicy\n}\n\nfunc (rt *Target) StartDelivery(ctx context.Context, msgMeta *module.MsgMetadata, mailFrom string) (module.Delivery, error) {\n\tpolicies := make([]module.DeliveryMXAuthPolicy, 0, len(rt.policies))\n\tif !msgMeta.TLSRequireOverride || !rt.allowSecOverride {\n\t\tfor _, p := range rt.policies {\n\t\t\tpolicies = append(policies, p.StartDelivery(msgMeta))\n\t\t}\n\t}\n\n\tvar (\n\t\tratelimitDomain string\n\t\terr             error\n\t)\n\t// This will leave ratelimitDomain = \"\" for null return path which is fine\n\t// for purposes of ratelimiting.\n\tif mailFrom != \"\" {\n\t\t_, ratelimitDomain, err = address.Split(mailFrom)\n\t\tif err != nil {\n\t\t\treturn nil, &exterrors.SMTPError{\n\t\t\t\tCode:         501,\n\t\t\t\tEnhancedCode: exterrors.EnhancedCode{5, 1, 8},\n\t\t\t\tMessage:      \"Malformed sender address\",\n\t\t\t\tTargetName:   \"remote\",\n\t\t\t\tErr:          err,\n\t\t\t}\n\t\t}\n\t}\n\n\t// Domain is already should be normalized by the message source (e.g.\n\t// endpoint/smtp).\n\tregion := trace.StartRegion(ctx, \"remote/limits.Take\")\n\taddr := net.IPv4(127, 0, 0, 1)\n\tif msgMeta.Conn != nil && msgMeta.Conn.RemoteAddr != nil {\n\t\ttcpAddr, ok := msgMeta.Conn.RemoteAddr.(*net.TCPAddr)\n\t\tif ok {\n\t\t\taddr = tcpAddr.IP\n\t\t}\n\t}\n\tif err := rt.limits.TakeMsg(ctx, addr, ratelimitDomain); err != nil {\n\t\tregion.End()\n\t\treturn nil, &exterrors.SMTPError{\n\t\t\tCode:         451,\n\t\t\tEnhancedCode: exterrors.EnhancedCode{4, 4, 5},\n\t\t\tMessage:      \"High load, try again later\",\n\t\t\tReason:       \"Global limit timeout\",\n\t\t\tTargetName:   \"remote\",\n\t\t\tErr:          err,\n\t\t}\n\t}\n\tregion.End()\n\n\treturn &remoteDelivery{\n\t\trt:          rt,\n\t\tmailFrom:    mailFrom,\n\t\tmsgMeta:     msgMeta,\n\t\tlog:         target.DeliveryLogger(rt.log, msgMeta),\n\t\tconnections: map[string]*mxConn{},\n\t\tpolicies:    policies,\n\t}, nil\n}\n\nfunc (rd *remoteDelivery) AddRcpt(ctx context.Context, to string, opts smtp.RcptOptions) error {\n\tdefer trace.StartRegion(ctx, \"remote/AddRcpt\").End()\n\n\tif rd.msgMeta.Quarantine {\n\t\treturn &exterrors.SMTPError{\n\t\t\tCode:         550,\n\t\t\tEnhancedCode: exterrors.EnhancedCode{5, 7, 0},\n\t\t\tMessage:      \"Refusing to deliver a quarantined message\",\n\t\t\tTargetName:   \"remote\",\n\t\t}\n\t}\n\n\t_, domain, err := address.Split(to)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Special-case for <postmaster> address. If it is not handled by a rewrite rule before\n\t// - we should not attempt to do anything with it and reject it as invalid.\n\tif domain == \"\" {\n\t\treturn &exterrors.SMTPError{\n\t\t\tCode:         550,\n\t\t\tEnhancedCode: exterrors.EnhancedCode{5, 1, 1},\n\t\t\tMessage:      \"<postmaster> address it no supported\",\n\t\t\tTargetName:   \"remote\",\n\t\t}\n\t}\n\n\tif strings.HasPrefix(domain, \"[\") {\n\t\treturn &exterrors.SMTPError{\n\t\t\tCode:         550,\n\t\t\tEnhancedCode: exterrors.EnhancedCode{5, 1, 1},\n\t\t\tMessage:      \"IP address literals are not supported\",\n\t\t\tTargetName:   \"remote\",\n\t\t}\n\t}\n\n\tconn, err := rd.connectionForDomain(ctx, domain)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif err := conn.Rcpt(ctx, to, opts); err != nil {\n\t\treturn moduleError(err)\n\t}\n\tconn.lastUseAt = time.Now()\n\n\trd.recipients = append(rd.recipients, to)\n\treturn nil\n}\n\ntype multipleErrs struct {\n\terrs      map[string]error\n\tstatusLck sync.Mutex\n}\n\nfunc (m *multipleErrs) Error() string {\n\tm.statusLck.Lock()\n\tdefer m.statusLck.Unlock()\n\treturn fmt.Sprintf(\"Partial delivery failure, per-rcpt info: %+v\", m.errs)\n}\n\nfunc (m *multipleErrs) Fields() map[string]interface{} {\n\tm.statusLck.Lock()\n\tdefer m.statusLck.Unlock()\n\n\t// If there are any temporary errors - the sender should retry to make sure\n\t// all recipients will get the message. However, since we can't tell it\n\t// which recipients got the message, this will generate duplicates for\n\t// them.\n\t//\n\t// We favor delivery with duplicates over incomplete delivery here.\n\n\tvar (\n\t\tcode     = 550\n\t\tenchCode = exterrors.EnhancedCode{5, 0, 0}\n\t)\n\tfor _, err := range m.errs {\n\t\tif exterrors.IsTemporary(err) {\n\t\t\tcode = 451\n\t\t\tenchCode = exterrors.EnhancedCode{4, 0, 0}\n\t\t}\n\t}\n\n\treturn map[string]interface{}{\n\t\t\"smtp_code\":     code,\n\t\t\"smtp_enchcode\": enchCode,\n\t\t\"smtp_msg\":      \"Partial delivery failure, additional attempts may result in duplicates\",\n\t\t\"target\":        \"remote\",\n\t\t\"errs\":          m.errs,\n\t}\n}\n\nfunc (m *multipleErrs) SetStatus(rcptTo string, err error) {\n\tm.statusLck.Lock()\n\tdefer m.statusLck.Unlock()\n\tm.errs[rcptTo] = err\n}\n\nfunc (rd *remoteDelivery) Body(ctx context.Context, header textproto.Header, buffer buffer.Buffer) error {\n\tdefer trace.StartRegion(ctx, \"remote/Body\").End()\n\n\tmerr := multipleErrs{\n\t\terrs: make(map[string]error),\n\t}\n\trd.BodyNonAtomic(ctx, &merr, header, buffer)\n\n\tfor _, v := range merr.errs {\n\t\tif v != nil {\n\t\t\tif len(merr.errs) == 1 {\n\t\t\t\treturn v\n\t\t\t}\n\t\t\treturn &merr\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (rd *remoteDelivery) BodyNonAtomic(ctx context.Context, c module.StatusCollector, header textproto.Header, b buffer.Buffer) {\n\tdefer trace.StartRegion(ctx, \"remote/BodyNonAtomic\").End()\n\n\tif rd.msgMeta.Quarantine {\n\t\tfor _, rcpt := range rd.recipients {\n\t\t\tc.SetStatus(rcpt, &exterrors.SMTPError{\n\t\t\t\tCode:         550,\n\t\t\t\tEnhancedCode: exterrors.EnhancedCode{5, 7, 0},\n\t\t\t\tMessage:      \"Refusing to deliver quarantined message\",\n\t\t\t\tTargetName:   \"remote\",\n\t\t\t})\n\t\t}\n\t\treturn\n\t}\n\n\tvar wg sync.WaitGroup\n\n\tfor i, conn := range rd.connections {\n\t\twg.Add(1)\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\n\t\t\tbodyR, err := b.Open()\n\t\t\tif err != nil {\n\t\t\t\tfor _, rcpt := range conn.Rcpts() {\n\t\t\t\t\tc.SetStatus(rcpt, err)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tdefer func() {\n\t\t\t\tif err := bodyR.Close(); err != nil {\n\t\t\t\t\trd.log.Error(\"failed to close message buffer\", err)\n\t\t\t\t}\n\t\t\t}()\n\n\t\t\terr = conn.Data(ctx, header, bodyR)\n\t\t\tfor _, rcpt := range conn.Rcpts() {\n\t\t\t\tc.SetStatus(rcpt, err)\n\t\t\t}\n\t\t\trd.connections[i].errored = err != nil\n\t\t\tconn.lastUseAt = time.Now()\n\t\t}()\n\t}\n\n\twg.Wait()\n}\n\nfunc (rd *remoteDelivery) Abort(ctx context.Context) error {\n\treturn rd.Close()\n}\n\nfunc (rd *remoteDelivery) Commit(ctx context.Context) error {\n\t// It is not possible to implement it atomically, so users of remoteDelivery have to\n\t// take care of partial failures.\n\treturn rd.Close()\n}\n\nfunc (rd *remoteDelivery) Close() error {\n\tfor _, conn := range rd.connections {\n\t\trd.rt.limits.ReleaseDest(conn.domain)\n\t\tconn.transactions++\n\n\t\tif !conn.Usable() {\n\t\t\trd.log.Debugf(\"disconnected %v from %s (errored=%v,transactions=%v,disconnected before=%v)\",\n\t\t\t\tconn.LocalAddr(), conn.ServerName(), conn.errored, conn.transactions, conn.Client() == nil)\n\t\t\trd.closeConn(conn)\n\t\t} else {\n\t\t\trd.log.Debugf(\"returning connection %v for %s to pool\", conn.LocalAddr(), conn.ServerName())\n\t\t\trd.rt.pool.Return(conn.domain, conn)\n\t\t}\n\t}\n\n\tvar (\n\t\tratelimitDomain string\n\t\terr             error\n\t)\n\tif rd.mailFrom != \"\" {\n\t\t_, ratelimitDomain, err = address.Split(rd.mailFrom)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\taddr := net.IPv4(127, 0, 0, 1)\n\tif rd.msgMeta.Conn != nil && rd.msgMeta.Conn.RemoteAddr != nil {\n\t\ttcpAddr, ok := rd.msgMeta.Conn.RemoteAddr.(*net.TCPAddr)\n\t\tif ok {\n\t\t\taddr = tcpAddr.IP\n\t\t}\n\t}\n\trd.rt.limits.ReleaseMsg(addr, ratelimitDomain)\n\n\treturn nil\n}\n\nfunc init() {\n\tmodules.Register(\"target.remote\", New)\n}\n"
  },
  {
    "path": "internal/target/remote/remote_test.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage remote\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"flag\"\n\t\"math/rand\"\n\t\"net\"\n\t\"os\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"github.com/emersion/go-message/textproto\"\n\t\"github.com/emersion/go-smtp\"\n\t\"github.com/foxcpp/go-mockdns\"\n\t\"github.com/foxcpp/go-mtasts\"\n\t\"github.com/foxcpp/maddy/framework/buffer\"\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/dns\"\n\t\"github.com/foxcpp/maddy/framework/exterrors\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/internal/limits\"\n\t\"github.com/foxcpp/maddy/internal/smtpconn/pool\"\n\t\"github.com/foxcpp/maddy/internal/testutils\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// .invalid TLD is used here to make sure if there is something wrong about\n// DNS hooks and lookups go to the real Internet, they will not result in\n// any useful data that can lead to outgoing connections being made.\n\nfunc testTarget(t *testing.T, zones map[string]mockdns.Zone, extResolver *dns.ExtResolver,\n\textraPolicies []module.MXAuthPolicy) *Target {\n\tresolver := &mockdns.Resolver{Zones: zones}\n\n\ttgt := Target{\n\t\tname:        \"remote\",\n\t\thostname:    \"mx.example.com\",\n\t\tresolver:    resolver,\n\t\tdialer:      resolver.DialContext,\n\t\textResolver: extResolver,\n\t\ttlsConfig:   &tls.Config{},\n\t\tlog:         testutils.Logger(t, \"remote\"),\n\t\tpolicies:    extraPolicies,\n\t\tlimits:      &limits.Group{},\n\t\tpool: pool.New(pool.Config{\n\t\t\tMaxKeys:             5000,\n\t\t\tMaxConnsPerKey:      5,      // basically, max. amount of idle connections in cache\n\t\t\tMaxConnLifetimeSec:  150,    // 2.5 mins, half of recommended idle time from RFC 5321\n\t\t\tStaleKeyLifetimeSec: 60 * 4, // make sure that cleanup runs before recommended idle time from RFC 5321\n\t\t}),\n\t}\n\n\treturn &tgt\n}\n\nfunc testSTSPolicy(t *testing.T, zones map[string]mockdns.Zone, mtastsGet func(context.Context, string) (*mtasts.Policy, error)) *mtastsPolicy {\n\tm, err := NewMTASTSPolicy(container.New(), \"mx_auth.mtasts\", \"test\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tp := m.(*mtastsPolicy)\n\terr = p.Configure(nil, config.NewMap(nil, config.Node{\n\t\tChildren: []config.Node{\n\t\t\t{\n\t\t\t\tName: \"cache\",\n\t\t\t\tArgs: []string{\"ram\"},\n\t\t\t},\n\t\t},\n\t}))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tp.mtastsGet = mtastsGet\n\tp.log = testutils.Logger(t, \"remote/mtasts\")\n\tp.cache.Resolver = &mockdns.Resolver{Zones: zones}\n\tp.StartUpdater()\n\n\treturn p\n}\n\nfunc testDANEPolicy(t *testing.T, extR *dns.ExtResolver) *danePolicy {\n\tm, err := NewDANEPolicy(container.New(), \"mx_auth.dane\", \"test\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tp := m.(*danePolicy)\n\terr = p.Configure(nil, config.NewMap(nil, config.Node{\n\t\tChildren: nil,\n\t}))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tp.extResolver = extR\n\tp.log = testutils.Logger(t, \"remote/dane\")\n\treturn p\n}\n\nfunc TestRemoteDelivery(t *testing.T) {\n\tbe, srv := testutils.SMTPServer(t, \"127.0.0.1:\"+smtpPort)\n\tdefer func() {\n\t\trequire.NoError(t, srv.Close())\n\t}()\n\tdefer testutils.CheckSMTPConnLeak(t, srv)\n\tzones := map[string]mockdns.Zone{\n\t\t\"example.invalid.\": {\n\t\t\tMX: []net.MX{{Host: \"mx.example.invalid.\", Pref: 10}},\n\t\t},\n\t\t\"mx.example.invalid.\": {\n\t\t\tA: []string{\"127.0.0.1\"},\n\t\t},\n\t}\n\n\ttgt := testTarget(t, zones, nil, nil)\n\tdefer func() {\n\t\tassert.NoError(t, tgt.Stop())\n\t}()\n\ttestutils.DoTestDelivery(t, tgt, \"test@example.com\", []string{\"test@example.invalid\"})\n\n\tbe.CheckMsg(t, 0, \"test@example.com\", []string{\"test@example.invalid\"})\n}\n\nfunc TestRemoteDelivery_NoMXFallback(t *testing.T) {\n\ttarpit := testutils.FailOnConn(t, \"127.0.0.1:\"+smtpPort)\n\tdefer func() {\n\t\tassert.NoError(t, tarpit.Close())\n\t}()\n\n\tzones := map[string]mockdns.Zone{\n\t\t\"example.invalid.\": {\n\t\t\tMX: []net.MX{},\n\t\t},\n\t}\n\n\ttgt := testTarget(t, zones, nil, nil)\n\tdefer func() {\n\t\tassert.NoError(t, tgt.Stop())\n\t}()\n\n\tdelivery, err := tgt.StartDelivery(context.Background(), &module.MsgMetadata{ID: \"test...\"}, \"test@example.com\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tif err := delivery.AddRcpt(context.Background(), \"test@example.invalid\", smtp.RcptOptions{}); err == nil {\n\t\tt.Fatal(\"Expected an error, got none\")\n\t}\n\n\tif err := delivery.Abort(context.Background()); err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\nfunc TestRemoteDelivery_EmptySender(t *testing.T) {\n\tbe, srv := testutils.SMTPServer(t, \"127.0.0.1:\"+smtpPort)\n\tdefer func() {\n\t\trequire.NoError(t, srv.Close())\n\t}()\n\tdefer testutils.CheckSMTPConnLeak(t, srv)\n\tzones := map[string]mockdns.Zone{\n\t\t\"example.invalid.\": {\n\t\t\tMX: []net.MX{{Host: \"mx.example.invalid.\", Pref: 10}},\n\t\t},\n\t\t\"mx.example.invalid.\": {\n\t\t\tA: []string{\"127.0.0.1\"},\n\t\t},\n\t}\n\n\ttgt := testTarget(t, zones, nil, nil)\n\tdefer func() {\n\t\tassert.NoError(t, tgt.Stop())\n\t}()\n\ttestutils.DoTestDelivery(t, tgt, \"\", []string{\"test@example.invalid\"})\n\n\tbe.CheckMsg(t, 0, \"\", []string{\"test@example.invalid\"})\n}\n\nfunc TestRemoteDelivery_IPLiteral(t *testing.T) {\n\tt.Skip(\"Support disabled\")\n\n\tbe, srv := testutils.SMTPServer(t, \"127.0.0.1:\"+smtpPort)\n\tdefer func() {\n\t\trequire.NoError(t, srv.Close())\n\t}()\n\tdefer testutils.CheckSMTPConnLeak(t, srv)\n\n\tzones := map[string]mockdns.Zone{\n\t\t\"example.invalid.\": {\n\t\t\tMX: []net.MX{{Host: \"mx.example.invalid.\", Pref: 10}},\n\t\t},\n\t\t\"mx.example.invalid.\": {\n\t\t\tA: []string{\"127.0.0.1\"},\n\t\t},\n\t\t\"1.0.0.127.in-addr.arpa.\": {\n\t\t\tPTR: []string{\"mx.example.invalid.\"},\n\t\t},\n\t}\n\n\ttgt := testTarget(t, zones, nil, nil)\n\tdefer func() {\n\t\tassert.NoError(t, tgt.Stop())\n\t}()\n\ttestutils.DoTestDelivery(t, tgt, \"test@example.com\", []string{\"test@[127.0.0.1]\"})\n\n\tbe.CheckMsg(t, 0, \"test@example.com\", []string{\"test@[127.0.0.1]\"})\n}\n\nfunc TestRemoteDelivery_FallbackMX(t *testing.T) {\n\tbe, srv := testutils.SMTPServer(t, \"127.0.0.1:\"+smtpPort)\n\tdefer func() {\n\t\trequire.NoError(t, srv.Close())\n\t}()\n\tdefer testutils.CheckSMTPConnLeak(t, srv)\n\tzones := map[string]mockdns.Zone{\n\t\t\"example.invalid.\": {\n\t\t\tA: []string{\"127.0.0.1\"},\n\t\t},\n\t}\n\n\ttgt := testTarget(t, zones, nil, nil)\n\tdefer func() {\n\t\tassert.NoError(t, tgt.Stop())\n\t}()\n\n\ttestutils.DoTestDelivery(t, tgt, \"test@example.com\", []string{\"test@example.invalid\"})\n\tbe.CheckMsg(t, 0, \"test@example.com\", []string{\"test@example.invalid\"})\n}\n\nfunc TestRemoteDelivery_BodyNonAtomic(t *testing.T) {\n\tbe, srv := testutils.SMTPServer(t, \"127.0.0.1:\"+smtpPort)\n\tdefer func() {\n\t\trequire.NoError(t, srv.Close())\n\t}()\n\tdefer testutils.CheckSMTPConnLeak(t, srv)\n\tzones := map[string]mockdns.Zone{\n\t\t\"example.invalid.\": {\n\t\t\tMX: []net.MX{{Host: \"mx.example.invalid.\", Pref: 10}},\n\t\t},\n\t\t\"mx.example.invalid.\": {\n\t\t\tA: []string{\"127.0.0.1\"},\n\t\t},\n\t}\n\n\ttgt := testTarget(t, zones, nil, nil)\n\tdefer func() {\n\t\tassert.NoError(t, tgt.Stop())\n\t}()\n\n\tc := multipleErrs{\n\t\terrs: map[string]error{},\n\t}\n\ttestutils.DoTestDeliveryNonAtomic(t, &c, tgt, \"test@example.com\", []string{\"test@example.invalid\"})\n\n\tif err := c.errs[\"test@example.invalid\"]; err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tbe.CheckMsg(t, 0, \"test@example.com\", []string{\"test@example.invalid\"})\n}\n\nfunc TestRemoteDelivery_Abort(t *testing.T) {\n\t_, srv := testutils.SMTPServer(t, \"127.0.0.1:\"+smtpPort)\n\tdefer func() {\n\t\trequire.NoError(t, srv.Close())\n\t}()\n\tdefer testutils.CheckSMTPConnLeak(t, srv)\n\tzones := map[string]mockdns.Zone{\n\t\t\"example.invalid.\": {\n\t\t\tMX: []net.MX{{Host: \"mx.example.invalid\", Pref: 10}},\n\t\t},\n\t\t\"mx.example.invalid.\": {\n\t\t\tA: []string{\"127.0.0.1\"},\n\t\t},\n\t}\n\n\ttgt := testTarget(t, zones, nil, nil)\n\tdefer func() {\n\t\tassert.NoError(t, tgt.Stop())\n\t}()\n\n\tdelivery, err := tgt.StartDelivery(context.Background(), &module.MsgMetadata{ID: \"test...\"}, \"test@example.com\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tif err := delivery.AddRcpt(context.Background(), \"test@example.invalid\", smtp.RcptOptions{}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tif err := delivery.Abort(context.Background()); err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\nfunc TestRemoteDelivery_CommitWithoutBody(t *testing.T) {\n\t_, srv := testutils.SMTPServer(t, \"127.0.0.1:\"+smtpPort)\n\tdefer func() {\n\t\trequire.NoError(t, srv.Close())\n\t}()\n\tdefer testutils.CheckSMTPConnLeak(t, srv)\n\tzones := map[string]mockdns.Zone{\n\t\t\"example.invalid.\": {\n\t\t\tMX: []net.MX{{Host: \"mx.example.invalid.\", Pref: 10}},\n\t\t},\n\t\t\"mx.example.invalid.\": {\n\t\t\tA: []string{\"127.0.0.1\"},\n\t\t},\n\t}\n\n\ttgt := testTarget(t, zones, nil, nil)\n\tdefer func() {\n\t\tassert.NoError(t, tgt.Stop())\n\t}()\n\n\tdelivery, err := tgt.StartDelivery(context.Background(), &module.MsgMetadata{ID: \"test...\"}, \"test@example.com\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tif err := delivery.AddRcpt(context.Background(), \"test@example.invalid\", smtp.RcptOptions{}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// Currently it does nothing, probably it should fail.\n\tif err := delivery.Commit(context.Background()); err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\nfunc TestRemoteDelivery_MAILFROMErr(t *testing.T) {\n\tbe, srv := testutils.SMTPServer(t, \"127.0.0.1:\"+smtpPort)\n\tdefer func() {\n\t\trequire.NoError(t, srv.Close())\n\t}()\n\tdefer testutils.CheckSMTPConnLeak(t, srv)\n\tzones := map[string]mockdns.Zone{\n\t\t\"example.invalid.\": {\n\t\t\tMX: []net.MX{{Host: \"mx.example.invalid.\", Pref: 10}},\n\t\t},\n\t\t\"mx.example.invalid.\": {\n\t\t\tA: []string{\"127.0.0.1\"},\n\t\t},\n\t}\n\n\tbe.MailErr = &smtp.SMTPError{\n\t\tCode:         550,\n\t\tEnhancedCode: smtp.EnhancedCode{5, 1, 2},\n\t\tMessage:      \"Hey\",\n\t}\n\n\ttgt := testTarget(t, zones, nil, nil)\n\tdefer func() {\n\t\tassert.NoError(t, tgt.Stop())\n\t}()\n\n\tdelivery, err := tgt.StartDelivery(context.Background(), &module.MsgMetadata{ID: \"test...\"}, \"test@example.com\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\terr = delivery.AddRcpt(context.Background(), \"test@example.invalid\", smtp.RcptOptions{})\n\ttestutils.CheckSMTPErr(t, err, 550, exterrors.EnhancedCode{5, 1, 2}, \"mx.example.invalid. said: Hey\")\n\n\tif err := delivery.Abort(context.Background()); err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\nfunc TestRemoteDelivery_NoMX(t *testing.T) {\n\ttarpit := testutils.FailOnConn(t, \"127.0.0.1:\"+smtpPort)\n\tdefer func() {\n\t\trequire.NoError(t, tarpit.Close())\n\t}()\n\n\tzones := map[string]mockdns.Zone{\n\t\t\"example.invalid.\": {\n\t\t\tMX: []net.MX{},\n\t\t},\n\t}\n\n\ttgt := testTarget(t, zones, nil, nil)\n\tdefer func() {\n\t\tassert.NoError(t, tgt.Stop())\n\t}()\n\n\tdelivery, err := tgt.StartDelivery(context.Background(), &module.MsgMetadata{ID: \"test...\"}, \"test@example.com\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tif err := delivery.AddRcpt(context.Background(), \"test@example.invalid\", smtp.RcptOptions{}); err == nil {\n\t\tt.Fatal(\"Expected an error, got none\")\n\t}\n\n\tif err := delivery.Abort(context.Background()); err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\nfunc TestRemoteDelivery_NullMX(t *testing.T) {\n\t// Hang the test if it actually connects to the server to\n\t// deliver the message. Use of testutils.SMTPServer here\n\t// causes weird race conditions.\n\ttarpit := testutils.FailOnConn(t, \"127.0.0.1:\"+smtpPort)\n\tdefer func() {\n\t\trequire.NoError(t, tarpit.Close())\n\t}()\n\n\tzones := map[string]mockdns.Zone{\n\t\t\"example.invalid.\": {\n\t\t\tMX: []net.MX{{Host: \".\", Pref: 10}},\n\t\t},\n\t}\n\n\ttgt := testTarget(t, zones, nil, nil)\n\tdefer func() {\n\t\tassert.NoError(t, tgt.Stop())\n\t}()\n\n\tdelivery, err := tgt.StartDelivery(context.Background(), &module.MsgMetadata{ID: \"test...\"}, \"test@example.com\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\terr = delivery.AddRcpt(context.Background(), \"test@example.invalid\", smtp.RcptOptions{})\n\ttestutils.CheckSMTPErr(t, err, 556, exterrors.EnhancedCode{5, 1, 10}, \"Domain does not accept email (null MX)\")\n\n\tif err := delivery.Abort(context.Background()); err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\nfunc TestRemoteDelivery_Quarantined(t *testing.T) {\n\t_, srv := testutils.SMTPServer(t, \"127.0.0.1:\"+smtpPort)\n\tdefer func() {\n\t\trequire.NoError(t, srv.Close())\n\t}()\n\tdefer testutils.CheckSMTPConnLeak(t, srv)\n\tzones := map[string]mockdns.Zone{\n\t\t\"example.invalid.\": {\n\t\t\tMX: []net.MX{{Host: \"mx.example.invalid.\", Pref: 10}},\n\t\t},\n\t\t\"mx.example.invalid.\": {\n\t\t\tA: []string{\"127.0.0.1\"},\n\t\t},\n\t}\n\n\ttgt := testTarget(t, zones, nil, nil)\n\tdefer func() {\n\t\tassert.NoError(t, tgt.Stop())\n\t}()\n\n\tmeta := module.MsgMetadata{ID: \"test...\"}\n\n\tdelivery, err := tgt.StartDelivery(context.Background(), &meta, \"test@example.com\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tif err := delivery.AddRcpt(context.Background(), \"test@example.invalid\", smtp.RcptOptions{}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tmeta.Quarantine = true\n\n\thdr := textproto.Header{}\n\thdr.Add(\"B\", \"2\")\n\thdr.Add(\"A\", \"1\")\n\tbody := buffer.MemoryBuffer{Slice: []byte(\"foobar\\n\")}\n\tif err := delivery.Body(context.Background(), textproto.Header{}, body); err == nil {\n\t\tt.Fatal(\"Expected an error, got none\")\n\t}\n\n\tif err := delivery.Abort(context.Background()); err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\nfunc TestRemoteDelivery_MAILFROMErr_Repeated(t *testing.T) {\n\tbe, srv := testutils.SMTPServer(t, \"127.0.0.1:\"+smtpPort)\n\tdefer func() {\n\t\trequire.NoError(t, srv.Close())\n\t}()\n\tdefer testutils.CheckSMTPConnLeak(t, srv)\n\tzones := map[string]mockdns.Zone{\n\t\t\"example.invalid.\": {\n\t\t\tMX: []net.MX{{Host: \"mx.example.invalid.\", Pref: 10}},\n\t\t},\n\t\t\"mx.example.invalid.\": {\n\t\t\tA: []string{\"127.0.0.1\"},\n\t\t},\n\t}\n\n\tbe.MailErr = &smtp.SMTPError{\n\t\tCode:         550,\n\t\tEnhancedCode: smtp.EnhancedCode{5, 1, 2},\n\t\tMessage:      \"Hey\",\n\t}\n\n\ttgt := testTarget(t, zones, nil, nil)\n\tdefer func() {\n\t\tassert.NoError(t, tgt.Stop())\n\t}()\n\n\tdelivery, err := tgt.StartDelivery(context.Background(), &module.MsgMetadata{ID: \"test...\"}, \"test@example.com\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\terr = delivery.AddRcpt(context.Background(), \"test@example.invalid\", smtp.RcptOptions{})\n\ttestutils.CheckSMTPErr(t, err, 550, exterrors.EnhancedCode{5, 1, 2}, \"mx.example.invalid. said: Hey\")\n\n\terr = delivery.AddRcpt(context.Background(), \"test2@example.invalid\", smtp.RcptOptions{})\n\ttestutils.CheckSMTPErr(t, err, 550, exterrors.EnhancedCode{5, 1, 2}, \"mx.example.invalid. said: Hey\")\n\n\tif err := delivery.Abort(context.Background()); err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\nfunc TestRemoteDelivery_RcptErr(t *testing.T) {\n\tbe, srv := testutils.SMTPServer(t, \"127.0.0.1:\"+smtpPort)\n\tdefer func() {\n\t\trequire.NoError(t, srv.Close())\n\t}()\n\tdefer testutils.CheckSMTPConnLeak(t, srv)\n\tzones := map[string]mockdns.Zone{\n\t\t\"example.invalid.\": {\n\t\t\tMX: []net.MX{{Host: \"mx.example.invalid.\", Pref: 10}},\n\t\t},\n\t\t\"mx.example.invalid.\": {\n\t\t\tA: []string{\"127.0.0.1\"},\n\t\t},\n\t}\n\n\tbe.RcptErr = map[string]error{\n\t\t\"test@example.invalid\": &smtp.SMTPError{\n\t\t\tCode:         550,\n\t\t\tEnhancedCode: smtp.EnhancedCode{5, 1, 2},\n\t\t\tMessage:      \"Hey\",\n\t\t},\n\t}\n\n\ttgt := testTarget(t, zones, nil, nil)\n\tdefer func() {\n\t\tassert.NoError(t, tgt.Stop())\n\t}()\n\n\tdelivery, err := tgt.StartDelivery(context.Background(), &module.MsgMetadata{ID: \"test...\"}, \"test@example.com\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\terr = delivery.AddRcpt(context.Background(), \"test@example.invalid\", smtp.RcptOptions{})\n\ttestutils.CheckSMTPErr(t, err, 550, exterrors.EnhancedCode{5, 1, 2}, \"mx.example.invalid. said: Hey\")\n\n\t// It should be possible to, however, add another recipient and continue\n\t// delivery as if nothing happened.\n\tif err := delivery.AddRcpt(context.Background(), \"test2@example.invalid\", smtp.RcptOptions{}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\thdr := textproto.Header{}\n\thdr.Add(\"B\", \"2\")\n\thdr.Add(\"A\", \"1\")\n\tbody := buffer.MemoryBuffer{Slice: []byte(\"foobar\\n\")}\n\tif err := delivery.Body(context.Background(), hdr, body); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tif err := delivery.Commit(context.Background()); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tbe.CheckMsg(t, 0, \"test@example.com\", []string{\"test2@example.invalid\"})\n}\n\nfunc TestRemoteDelivery_DownMX(t *testing.T) {\n\tbe, srv := testutils.SMTPServer(t, \"127.0.0.1:\"+smtpPort)\n\tdefer func() {\n\t\trequire.NoError(t, srv.Close())\n\t}()\n\tdefer testutils.CheckSMTPConnLeak(t, srv)\n\tzones := map[string]mockdns.Zone{\n\t\t\"example.invalid.\": {\n\t\t\tMX: []net.MX{\n\t\t\t\t{Host: \"mx1.example.invalid.\", Pref: 20},\n\t\t\t\t{Host: \"mx2.example.invalid.\", Pref: 10},\n\t\t\t},\n\t\t},\n\t\t\"mx1.example.invalid.\": {\n\t\t\tA: []string{\"127.0.0.1\"},\n\t\t},\n\t\t\"mx2.example.invalid.\": {\n\t\t\tA: []string{\"127.0.0.2\"},\n\t\t},\n\t}\n\n\ttgt := testTarget(t, zones, nil, nil)\n\tdefer func() {\n\t\tassert.NoError(t, tgt.Stop())\n\t}()\n\n\ttestutils.DoTestDelivery(t, tgt, \"test@example.com\", []string{\"test@example.invalid\"})\n\tbe.CheckMsg(t, 0, \"test@example.com\", []string{\"test@example.invalid\"})\n}\n\nfunc TestRemoteDelivery_AllMXDown(t *testing.T) {\n\tzones := map[string]mockdns.Zone{\n\t\t\"example.invalid.\": {\n\t\t\tMX: []net.MX{\n\t\t\t\t{Host: \"mx1.example.invalid.\", Pref: 20},\n\t\t\t\t{Host: \"mx2.example.invalid.\", Pref: 10},\n\t\t\t},\n\t\t},\n\t\t\"mx1.example.invalid.\": {\n\t\t\tA: []string{\"127.0.0.1\"},\n\t\t},\n\t\t\"mx2.example.invalid.\": {\n\t\t\tA: []string{\"127.0.0.2\"},\n\t\t},\n\t}\n\n\ttgt := testTarget(t, zones, nil, nil)\n\tdefer func() {\n\t\tassert.NoError(t, tgt.Stop())\n\t}()\n\n\t_, err := testutils.DoTestDeliveryErr(t, tgt, \"test@example.com\", []string{\"test@example.invalid\"})\n\tif err == nil {\n\t\tt.Fatal(\"Expected an error, got none\")\n\t}\n}\n\nfunc TestRemoteDelivery_Split(t *testing.T) {\n\tbe1, srv1 := testutils.SMTPServer(t, \"127.0.0.1:\"+smtpPort)\n\tdefer func() {\n\t\tassert.NoError(t, srv1.Close())\n\t}()\n\tdefer testutils.CheckSMTPConnLeak(t, srv1)\n\tbe2, srv2 := testutils.SMTPServer(t, \"127.0.0.2:\"+smtpPort)\n\tdefer func() {\n\t\tassert.NoError(t, srv2.Close())\n\t}()\n\tdefer testutils.CheckSMTPConnLeak(t, srv2)\n\tzones := map[string]mockdns.Zone{\n\t\t\"example.invalid.\": {\n\t\t\tMX: []net.MX{{Host: \"mx.example.invalid.\", Pref: 10}},\n\t\t},\n\t\t\"example2.invalid.\": {\n\t\t\tMX: []net.MX{{Host: \"mx.example2.invalid.\", Pref: 10}},\n\t\t},\n\t\t\"mx.example.invalid.\": {\n\t\t\tA: []string{\"127.0.0.1\"},\n\t\t},\n\t\t\"mx.example2.invalid.\": {\n\t\t\tA: []string{\"127.0.0.2\"},\n\t\t},\n\t}\n\n\ttgt := testTarget(t, zones, nil, nil)\n\tdefer func() {\n\t\tassert.NoError(t, tgt.Stop())\n\t}()\n\n\ttestutils.DoTestDelivery(t, tgt, \"test@example.com\", []string{\"test@example.invalid\", \"test@example2.invalid\"})\n\n\tbe1.CheckMsg(t, 0, \"test@example.com\", []string{\"test@example.invalid\"})\n\tbe2.CheckMsg(t, 0, \"test@example.com\", []string{\"test@example2.invalid\"})\n}\n\nfunc TestRemoteDelivery_Split_Fail(t *testing.T) {\n\tbe1, srv1 := testutils.SMTPServer(t, \"127.0.0.1:\"+smtpPort)\n\tdefer func() {\n\t\trequire.NoError(t, srv1.Close())\n\t}()\n\tdefer testutils.CheckSMTPConnLeak(t, srv1)\n\tbe2, srv2 := testutils.SMTPServer(t, \"127.0.0.2:\"+smtpPort)\n\tdefer func() {\n\t\trequire.NoError(t, srv2.Close())\n\t}()\n\tdefer testutils.CheckSMTPConnLeak(t, srv2)\n\tzones := map[string]mockdns.Zone{\n\t\t\"example.invalid.\": {\n\t\t\tMX: []net.MX{{Host: \"mx.example.invalid.\", Pref: 10}},\n\t\t},\n\t\t\"example2.invalid.\": {\n\t\t\tMX: []net.MX{{Host: \"mx.example2.invalid.\", Pref: 10}},\n\t\t},\n\t\t\"mx.example.invalid.\": {\n\t\t\tA: []string{\"127.0.0.1\"},\n\t\t},\n\t\t\"mx.example2.invalid.\": {\n\t\t\tA: []string{\"127.0.0.2\"},\n\t\t},\n\t}\n\n\tbe1.RcptErr = map[string]error{\n\t\t\"test@example.invalid\": &smtp.SMTPError{\n\t\t\tCode:         550,\n\t\t\tEnhancedCode: smtp.EnhancedCode{5, 1, 2},\n\t\t\tMessage:      \"Hey\",\n\t\t},\n\t}\n\n\ttgt := testTarget(t, zones, nil, nil)\n\tdefer func() {\n\t\tassert.NoError(t, tgt.Stop())\n\t}()\n\n\tdelivery, err := tgt.StartDelivery(context.Background(), &module.MsgMetadata{ID: \"test...\"}, \"test@example.com\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\terr = delivery.AddRcpt(context.Background(), \"test@example.invalid\", smtp.RcptOptions{})\n\tif err == nil {\n\t\tt.Fatal(\"Expected an error, got none\")\n\t}\n\n\t// It should be possible to, however, add another recipient and continue\n\t// delivery as if nothing happened.\n\tif err := delivery.AddRcpt(context.Background(), \"test@example2.invalid\", smtp.RcptOptions{}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\thdr := textproto.Header{}\n\thdr.Add(\"B\", \"2\")\n\thdr.Add(\"A\", \"1\")\n\tbody := buffer.MemoryBuffer{Slice: []byte(\"foobar\\n\")}\n\tif err := delivery.Body(context.Background(), hdr, body); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tif err := delivery.Commit(context.Background()); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tbe2.CheckMsg(t, 0, \"test@example.com\", []string{\"test@example2.invalid\"})\n}\n\nfunc TestRemoteDelivery_BodyErr(t *testing.T) {\n\tbe, srv := testutils.SMTPServer(t, \"127.0.0.1:\"+smtpPort)\n\tdefer func() {\n\t\trequire.NoError(t, srv.Close())\n\t}()\n\tdefer testutils.CheckSMTPConnLeak(t, srv)\n\tzones := map[string]mockdns.Zone{\n\t\t\"example.invalid.\": {\n\t\t\tMX: []net.MX{{Host: \"mx.example.invalid.\", Pref: 10}},\n\t\t},\n\t\t\"mx.example.invalid.\": {\n\t\t\tA: []string{\"127.0.0.1\"},\n\t\t},\n\t}\n\n\tbe.DataErr = &smtp.SMTPError{\n\t\tCode:         550,\n\t\tEnhancedCode: smtp.EnhancedCode{5, 1, 2},\n\t\tMessage:      \"Hey\",\n\t}\n\n\ttgt := testTarget(t, zones, nil, nil)\n\tdefer func() {\n\t\tassert.NoError(t, tgt.Stop())\n\t}()\n\n\tdelivery, err := tgt.StartDelivery(context.Background(), &module.MsgMetadata{ID: \"test...\"}, \"test@example.com\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\terr = delivery.AddRcpt(context.Background(), \"test@example.invalid\", smtp.RcptOptions{})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\thdr := textproto.Header{}\n\thdr.Add(\"B\", \"2\")\n\thdr.Add(\"A\", \"1\")\n\tbody := buffer.MemoryBuffer{Slice: []byte(\"foobar\\n\")}\n\tif err := delivery.Body(context.Background(), hdr, body); err == nil {\n\t\tt.Fatal(\"expected an error, got none\")\n\t}\n\n\tif err := delivery.Abort(context.Background()); err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\nfunc TestRemoteDelivery_Split_BodyErr(t *testing.T) {\n\tbe1, srv1 := testutils.SMTPServer(t, \"127.0.0.1:\"+smtpPort)\n\tdefer func() {\n\t\trequire.NoError(t, srv1.Close())\n\t}()\n\tdefer testutils.CheckSMTPConnLeak(t, srv1)\n\t_, srv2 := testutils.SMTPServer(t, \"127.0.0.2:\"+smtpPort)\n\tdefer func() {\n\t\trequire.NoError(t, srv2.Close())\n\t}()\n\tdefer testutils.CheckSMTPConnLeak(t, srv2)\n\tzones := map[string]mockdns.Zone{\n\t\t\"example.invalid.\": {\n\t\t\tMX: []net.MX{{Host: \"mx.example.invalid.\", Pref: 10}},\n\t\t},\n\t\t\"example2.invalid.\": {\n\t\t\tMX: []net.MX{{Host: \"mx.example2.invalid.\", Pref: 10}},\n\t\t},\n\t\t\"mx.example.invalid.\": {\n\t\t\tA: []string{\"127.0.0.1\"},\n\t\t},\n\t\t\"mx.example2.invalid.\": {\n\t\t\tA: []string{\"127.0.0.2\"},\n\t\t},\n\t}\n\n\tbe1.DataErr = &smtp.SMTPError{\n\t\tCode:         421,\n\t\tEnhancedCode: smtp.EnhancedCode{4, 1, 2},\n\t\tMessage:      \"Hey\",\n\t}\n\n\ttgt := testTarget(t, zones, nil, nil)\n\tdefer func() {\n\t\tassert.NoError(t, tgt.Stop())\n\t}()\n\n\tdelivery, err := tgt.StartDelivery(context.Background(), &module.MsgMetadata{ID: \"test...\"}, \"test@example.com\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tif err := delivery.AddRcpt(context.Background(), \"test@example.invalid\", smtp.RcptOptions{}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := delivery.AddRcpt(context.Background(), \"test@example2.invalid\", smtp.RcptOptions{}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\thdr := textproto.Header{}\n\thdr.Add(\"B\", \"2\")\n\thdr.Add(\"A\", \"1\")\n\tbody := buffer.MemoryBuffer{Slice: []byte(\"foobar\\n\")}\n\terr = delivery.Body(context.Background(), hdr, body)\n\ttestutils.CheckSMTPErr(t, err, 451, exterrors.EnhancedCode{4, 0, 0},\n\t\t\"Partial delivery failure, additional attempts may result in duplicates\")\n\n\tif err := delivery.Abort(context.Background()); err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\nfunc TestRemoteDelivery_Split_BodyErr_NonAtomic(t *testing.T) {\n\tbe1, srv1 := testutils.SMTPServer(t, \"127.0.0.1:\"+smtpPort)\n\tdefer func() {\n\t\trequire.NoError(t, srv1.Close())\n\t}()\n\tdefer testutils.CheckSMTPConnLeak(t, srv1)\n\t_, srv2 := testutils.SMTPServer(t, \"127.0.0.2:\"+smtpPort)\n\tdefer func() {\n\t\trequire.NoError(t, srv2.Close())\n\t}()\n\tdefer testutils.CheckSMTPConnLeak(t, srv2)\n\tzones := map[string]mockdns.Zone{\n\t\t\"example.invalid.\": {\n\t\t\tMX: []net.MX{{Host: \"mx.example.invalid.\", Pref: 10}},\n\t\t},\n\t\t\"example2.invalid.\": {\n\t\t\tMX: []net.MX{{Host: \"mx.example2.invalid.\", Pref: 10}},\n\t\t},\n\t\t\"mx.example.invalid.\": {\n\t\t\tA: []string{\"127.0.0.1\"},\n\t\t},\n\t\t\"mx.example2.invalid.\": {\n\t\t\tA: []string{\"127.0.0.2\"},\n\t\t},\n\t}\n\n\tbe1.DataErr = &smtp.SMTPError{\n\t\tCode:         550,\n\t\tEnhancedCode: smtp.EnhancedCode{5, 1, 2},\n\t\tMessage:      \"Hey\",\n\t}\n\n\ttgt := testTarget(t, zones, nil, nil)\n\tdefer func() {\n\t\tassert.NoError(t, tgt.Stop())\n\t}()\n\n\tdelivery, err := tgt.StartDelivery(context.Background(), &module.MsgMetadata{ID: \"test...\"}, \"test@example.com\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tif err := delivery.AddRcpt(context.Background(), \"test@example.invalid\", smtp.RcptOptions{}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := delivery.AddRcpt(context.Background(), \"test2@example.invalid\", smtp.RcptOptions{}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := delivery.AddRcpt(context.Background(), \"test@example2.invalid\", smtp.RcptOptions{}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\thdr := textproto.Header{}\n\thdr.Add(\"B\", \"2\")\n\thdr.Add(\"A\", \"1\")\n\tbody := buffer.MemoryBuffer{Slice: []byte(\"foobar\\n\")}\n\tc := multipleErrs{\n\t\terrs: map[string]error{},\n\t}\n\tdelivery.(module.PartialDelivery).BodyNonAtomic(context.Background(), &c, hdr, body)\n\n\ttestutils.CheckSMTPErr(t, c.errs[\"test@example.invalid\"],\n\t\t550, exterrors.EnhancedCode{5, 1, 2}, \"mx.example.invalid. said: Hey\")\n\ttestutils.CheckSMTPErr(t, c.errs[\"test2@example.invalid\"],\n\t\t550, exterrors.EnhancedCode{5, 1, 2}, \"mx.example.invalid. said: Hey\")\n\tif err := c.errs[\"test@example2.invalid\"]; err != nil {\n\t\tt.Errorf(\"Unexpected error for non-failing connection: %v\", err)\n\t}\n\n\tif err := delivery.Abort(context.Background()); err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\nfunc TestRemoteDelivery_TLSErrFallback(t *testing.T) {\n\tclientCfg, be, srv := testutils.SMTPServerSTARTTLS(t, \"127.0.0.1:\"+smtpPort)\n\tdefer func() {\n\t\trequire.NoError(t, srv.Close())\n\t}()\n\tdefer testutils.CheckSMTPConnLeak(t, srv)\n\tzones := map[string]mockdns.Zone{\n\t\t\"example.invalid.\": {\n\t\t\tMX: []net.MX{{Host: \"mx.example.invalid.\", Pref: 10}},\n\t\t},\n\t\t\"mx.example.invalid.\": {\n\t\t\tA: []string{\"127.0.0.1\"},\n\t\t},\n\t}\n\n\t// Cause failure through version incompatibility.\n\tclientCfg.MaxVersion = tls.VersionTLS12\n\tclientCfg.MinVersion = tls.VersionTLS12\n\tsrv.TLSConfig.MinVersion = tls.VersionTLS11\n\tsrv.TLSConfig.MaxVersion = tls.VersionTLS11\n\n\ttgt := testTarget(t, zones, nil, nil)\n\ttgt.tlsConfig = clientCfg\n\tdefer func() {\n\t\tassert.NoError(t, tgt.Stop())\n\t}()\n\n\ttestutils.DoTestDelivery(t, tgt, \"test@example.com\", []string{\"test@example.invalid\"})\n\tbe.CheckMsg(t, 0, \"test@example.com\", []string{\"test@example.invalid\"})\n}\n\nfunc TestRemoteDelivery_RequireTLS_Missing(t *testing.T) {\n\t_, srv := testutils.SMTPServer(t, \"127.0.0.1:\"+smtpPort)\n\tdefer func() {\n\t\trequire.NoError(t, srv.Close())\n\t}()\n\tdefer testutils.CheckSMTPConnLeak(t, srv)\n\tzones := map[string]mockdns.Zone{\n\t\t\"example.invalid.\": {\n\t\t\tMX: []net.MX{{Host: \"mx.example.invalid.\", Pref: 10}},\n\t\t},\n\t\t\"mx.example.invalid.\": {\n\t\t\tA: []string{\"127.0.0.1\"},\n\t\t},\n\t}\n\n\ttgt := testTarget(t, zones, nil, []module.MXAuthPolicy{\n\t\t&localPolicy{minTLSLevel: module.TLSEncrypted},\n\t})\n\tdefer func() {\n\t\tassert.NoError(t, tgt.Stop())\n\t}()\n\n\t_, err := testutils.DoTestDeliveryErr(t, tgt, \"test@example.com\", []string{\"test@example.invalid\"})\n\tif err == nil {\n\t\tt.Errorf(\"expected an error, got none\")\n\t}\n}\n\nfunc TestRemoteDelivery_RequireTLS_Present(t *testing.T) {\n\tclientCfg, be, srv := testutils.SMTPServerSTARTTLS(t, \"127.0.0.1:\"+smtpPort)\n\tdefer func() {\n\t\trequire.NoError(t, srv.Close())\n\t}()\n\tdefer testutils.CheckSMTPConnLeak(t, srv)\n\tzones := map[string]mockdns.Zone{\n\t\t\"example.invalid.\": {\n\t\t\tMX: []net.MX{{Host: \"mx.example.invalid.\", Pref: 10}},\n\t\t},\n\t\t\"mx.example.invalid.\": {\n\t\t\tA: []string{\"127.0.0.1\"},\n\t\t},\n\t}\n\n\ttgt := testTarget(t, zones, nil, []module.MXAuthPolicy{\n\t\t&localPolicy{minTLSLevel: module.TLSEncrypted},\n\t})\n\ttgt.tlsConfig = clientCfg\n\tdefer func() {\n\t\tassert.NoError(t, tgt.Stop())\n\t}()\n\n\ttestutils.DoTestDelivery(t, tgt, \"test@example.com\", []string{\"test@example.invalid\"})\n\tbe.CheckMsg(t, 0, \"test@example.com\", []string{\"test@example.invalid\"})\n}\n\nfunc TestRemoteDelivery_RequireTLS_NoErrFallback(t *testing.T) {\n\tclientCfg, _, srv := testutils.SMTPServerSTARTTLS(t, \"127.0.0.1:\"+smtpPort)\n\tdefer func() {\n\t\trequire.NoError(t, srv.Close())\n\t}()\n\tdefer testutils.CheckSMTPConnLeak(t, srv)\n\tzones := map[string]mockdns.Zone{\n\t\t\"example.invalid.\": {\n\t\t\tMX: []net.MX{{Host: \"mx.example.invalid.\", Pref: 10}},\n\t\t},\n\t\t\"mx.example.invalid.\": {\n\t\t\tA: []string{\"127.0.0.1\"},\n\t\t},\n\t}\n\n\t// Cause failure through version incompatibility.\n\tclientCfg.MaxVersion = tls.VersionTLS12\n\tclientCfg.MinVersion = tls.VersionTLS12\n\tsrv.TLSConfig.MinVersion = tls.VersionTLS11\n\tsrv.TLSConfig.MaxVersion = tls.VersionTLS11\n\n\ttgt := testTarget(t, zones, nil, []module.MXAuthPolicy{\n\t\t&localPolicy{minTLSLevel: module.TLSEncrypted},\n\t})\n\ttgt.tlsConfig = clientCfg\n\tdefer func() {\n\t\tassert.NoError(t, tgt.Stop())\n\t}()\n\n\t_, err := testutils.DoTestDeliveryErr(t, tgt, \"test@example.com\", []string{\"test@example.invalid\"})\n\tif err == nil {\n\t\tt.Fatal(\"Expected an error, got none\")\n\t}\n}\n\nfunc TestRemoteDelivery_TLS_FallbackNoVerify(t *testing.T) {\n\t_, be, srv := testutils.SMTPServerSTARTTLS(t, \"127.0.0.1:\"+smtpPort)\n\tdefer func() {\n\t\trequire.NoError(t, srv.Close())\n\t}()\n\tdefer testutils.CheckSMTPConnLeak(t, srv)\n\tzones := map[string]mockdns.Zone{\n\t\t\"example.invalid.\": {\n\t\t\tMX: []net.MX{{Host: \"mx.example.invalid.\", Pref: 10}},\n\t\t},\n\t\t\"mx.example.invalid.\": {\n\t\t\tA: []string{\"127.0.0.1\"},\n\t\t},\n\t}\n\n\t// tlsConfig is not configured to trust server cert.\n\ttgt := testTarget(t, zones, nil, []module.MXAuthPolicy{\n\t\t&localPolicy{minTLSLevel: module.TLSEncrypted},\n\t})\n\tdefer func() {\n\t\tassert.NoError(t, tgt.Stop())\n\t}()\n\n\ttestutils.DoTestDelivery(t, tgt, \"test@example.com\", []string{\"test@example.invalid\"})\n\tbe.CheckMsg(t, 0, \"test@example.com\", []string{\"test@example.invalid\"})\n\n\t// But it should still be delivered over TLS.\n\ttlsState, ok := be.Messages[0].Conn.TLSConnectionState()\n\tif !ok || !tlsState.HandshakeComplete {\n\t\tt.Fatal(\"Message was not delivered over TLS\")\n\t}\n}\n\nfunc TestRemoteDelivery_TLS_FallbackPlaintext(t *testing.T) {\n\tclientCfg, be, srv := testutils.SMTPServerSTARTTLS(t, \"127.0.0.1:\"+smtpPort)\n\tdefer func() {\n\t\trequire.NoError(t, srv.Close())\n\t}()\n\tdefer testutils.CheckSMTPConnLeak(t, srv)\n\tzones := map[string]mockdns.Zone{\n\t\t\"example.invalid.\": {\n\t\t\tMX: []net.MX{{Host: \"mx.example.invalid.\", Pref: 10}},\n\t\t},\n\t\t\"mx.example.invalid.\": {\n\t\t\tA: []string{\"127.0.0.1\"},\n\t\t},\n\t}\n\n\t// Cause failure through version incompatibility.\n\tclientCfg.MaxVersion = tls.VersionTLS12\n\tclientCfg.MinVersion = tls.VersionTLS12\n\tsrv.TLSConfig.MinVersion = tls.VersionTLS11\n\tsrv.TLSConfig.MaxVersion = tls.VersionTLS11\n\n\ttgt := testTarget(t, zones, nil, nil)\n\ttgt.tlsConfig = clientCfg\n\tdefer func() {\n\t\tassert.NoError(t, tgt.Stop())\n\t}()\n\n\ttestutils.DoTestDelivery(t, tgt, \"test@example.com\", []string{\"test@example.invalid\"})\n\tbe.CheckMsg(t, 0, \"test@example.com\", []string{\"test@example.invalid\"})\n}\n\nfunc TestMain(m *testing.M) {\n\tremoteSmtpPort := flag.String(\"test.smtpport\", \"random\", \"(maddy) SMTP port to use for connections in tests\")\n\tflag.Parse()\n\n\tif *remoteSmtpPort == \"random\" {\n\t\t*remoteSmtpPort = strconv.Itoa(rand.Intn(65536-10000) + 10000)\n\t}\n\n\tsmtpPort = *remoteSmtpPort\n\tos.Exit(m.Run())\n}\n\nfunc TestRemoteDelivery_ConnReuse(t *testing.T) {\n\tbe, srv := testutils.SMTPServer(t, \"127.0.0.1:\"+smtpPort)\n\tdefer func() {\n\t\trequire.NoError(t, srv.Close())\n\t}()\n\tdefer testutils.CheckSMTPConnLeak(t, srv)\n\tzones := map[string]mockdns.Zone{\n\t\t\"example.invalid.\": {\n\t\t\tMX: []net.MX{{Host: \"mx.example.invalid.\", Pref: 10}},\n\t\t},\n\t\t\"mx.example.invalid.\": {\n\t\t\tA: []string{\"127.0.0.1\"},\n\t\t},\n\t}\n\n\ttgt := testTarget(t, zones, nil, nil)\n\ttgt.connReuseLimit = 5\n\tdefer func() {\n\t\tassert.NoError(t, tgt.Stop())\n\t}()\n\ttestutils.DoTestDelivery(t, tgt, \"test@example.com\", []string{\"test@example.invalid\"})\n\ttestutils.DoTestDelivery(t, tgt, \"test@example.com\", []string{\"test@example.invalid\"})\n\n\tbe.CheckMsg(t, 0, \"test@example.com\", []string{\"test@example.invalid\"})\n\tbe.CheckMsg(t, 1, \"test@example.com\", []string{\"test@example.invalid\"})\n\n\tif len(be.SourceEndpoints) != 1 {\n\t\tt.Fatal(\"Only one session should be used, found\", be.SourceEndpoints)\n\t}\n}\n"
  },
  {
    "path": "internal/target/remote/security.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage remote\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"os\"\n\t\"runtime/debug\"\n\t\"time\"\n\n\t\"github.com/foxcpp/go-mtasts\"\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/dns\"\n\t\"github.com/foxcpp/maddy/framework/exterrors\"\n\t\"github.com/foxcpp/maddy/framework/future\"\n\t\"github.com/foxcpp/maddy/framework/log\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n\t\"github.com/foxcpp/maddy/internal/target\"\n)\n\ntype (\n\tmtastsPolicy struct {\n\t\tcache       *mtasts.Cache\n\t\tmtastsGet   func(context.Context, string) (*mtasts.Policy, error)\n\t\tupdaterStop chan struct{}\n\t\tlog         *log.Logger\n\t\tinstName    string\n\t}\n\tmtastsDelivery struct {\n\t\tc         *mtastsPolicy\n\t\tdomain    string\n\t\tpolicyFut *future.Future\n\t\tlog       *log.Logger\n\t}\n)\n\nfunc NewMTASTSPolicy(c *container.C, modName, instName string) (module.Module, error) {\n\treturn &mtastsPolicy{\n\t\tinstName: instName,\n\t\tlog:      c.DefaultLogger.Sublogger(modName),\n\t}, nil\n}\n\nfunc (c *mtastsPolicy) Name() string {\n\treturn c.log.Name\n}\n\nfunc (c *mtastsPolicy) InstanceName() string {\n\treturn c.instName\n}\n\nfunc (c *mtastsPolicy) Weight() int {\n\treturn 10\n}\n\nfunc (c *mtastsPolicy) Configure(inlineArgs []string, cfg *config.Map) error {\n\tvar (\n\t\tstoreType string\n\t\tstoreDir  string\n\t)\n\tcfg.Enum(\"cache\", false, false, []string{\"ram\", \"fs\"}, \"fs\", &storeType)\n\tcfg.String(\"fs_dir\", false, false, \"mtasts_cache\", &storeDir)\n\tif _, err := cfg.Process(); err != nil {\n\t\treturn err\n\t}\n\n\tswitch storeType {\n\tcase \"fs\":\n\t\tif err := os.MkdirAll(storeDir, os.ModePerm); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tc.cache = mtasts.NewFSCache(storeDir)\n\tcase \"ram\":\n\t\tc.cache = mtasts.NewRAMCache()\n\tdefault:\n\t\tpanic(\"mtasts policy init: unknown cache type\")\n\t}\n\tc.cache.Resolver = dns.DefaultResolver()\n\tc.mtastsGet = c.cache.Get\n\n\treturn nil\n}\n\nfunc (c *mtastsPolicy) Start() error {\n\tc.StartUpdater()\n\treturn nil\n}\n\n// StartUpdater starts a goroutine to update MTA-STS cache periodically until\n// Close is called.\n//\n// It can be called only once per mtastsPolicy instance.\nfunc (c *mtastsPolicy) StartUpdater() {\n\tc.updaterStop = make(chan struct{})\n\tgo c.updater()\n}\n\nfunc (c *mtastsPolicy) Stop() error {\n\tif c.updaterStop != nil {\n\t\tc.updaterStop <- struct{}{}\n\t\t<-c.updaterStop\n\t\tc.updaterStop = nil\n\t}\n\treturn nil\n}\n\nfunc (c *mtastsPolicy) updater() {\n\tdefer func() {\n\t\tif err := recover(); err != nil {\n\t\t\tstack := debug.Stack()\n\t\t\tlog.Printf(\"panic during MTA-STS update: %v\\n%s\", err, stack)\n\t\t\tlog.Printf(\"MTA-STS cache refresh disabled due to critical error\")\n\t\t\tc.updaterStop = nil\n\t\t}\n\t}()\n\n\t// Always update cache on start-up since we may have been down for some\n\t// time.\n\tc.log.Debugln(\"updating MTA-STS cache...\")\n\tif err := c.cache.Refresh(); err != nil {\n\t\tc.log.Error(\"MTA-STS cache update error\", err)\n\t}\n\tc.log.Debugln(\"updating MTA-STS cache... done!\")\n\n\tt := time.NewTicker(12 * time.Hour)\n\tfor {\n\t\tselect {\n\t\tcase <-t.C:\n\t\t\tc.log.Debugln(\"updating MTA-STS cache...\")\n\t\t\tif err := c.cache.Refresh(); err != nil {\n\t\t\t\tc.log.Error(\"MTA-STS cache opdate error\", err)\n\t\t\t}\n\t\t\tc.log.Debugln(\"updating MTA-STS cache... done!\")\n\t\tcase <-c.updaterStop:\n\t\t\tc.updaterStop <- struct{}{}\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc (c *mtastsPolicy) StartDelivery(msgMeta *module.MsgMetadata) module.DeliveryMXAuthPolicy {\n\treturn &mtastsDelivery{\n\t\tc:   c,\n\t\tlog: target.DeliveryLogger(c.log, msgMeta),\n\t}\n}\n\nfunc (c *mtastsDelivery) PrepareDomain(ctx context.Context, domain string) {\n\tc.policyFut = future.New()\n\tgo func() {\n\t\tc.policyFut.Set(c.c.mtastsGet(ctx, domain))\n\t}()\n}\n\nfunc (c *mtastsDelivery) PrepareConn(ctx context.Context, mx string) {}\n\nfunc (c *mtastsDelivery) CheckMX(ctx context.Context, mxLevel module.MXLevel, domain, mx string, dnssec bool) (module.MXLevel, error) {\n\tpolicyI, err := c.policyFut.GetContext(ctx)\n\tif err != nil {\n\t\tc.log.DebugMsg(\"MTA-STS error\", \"err\", err)\n\t\treturn module.MXNone, nil\n\t}\n\tpolicy := policyI.(*mtasts.Policy)\n\n\tif !policy.Match(mx) {\n\t\tif policy.Mode == mtasts.ModeEnforce {\n\t\t\treturn module.MXNone, &exterrors.SMTPError{\n\t\t\t\tCode:         550,\n\t\t\t\tEnhancedCode: exterrors.EnhancedCode{5, 7, 0},\n\t\t\t\tMessage:      \"Failed to establish the MX record authenticity (MTA-STS)\",\n\t\t\t}\n\t\t}\n\t\tc.log.Msg(\"MX does not match published non-enforced MTA-STS policy\", \"mx\", mx, \"domain\", c.domain)\n\t\treturn module.MXNone, nil\n\t}\n\treturn module.MX_MTASTS, nil\n}\n\nfunc (c *mtastsDelivery) CheckConn(ctx context.Context, mxLevel module.MXLevel, tlsLevel module.TLSLevel, domain, mx string, tlsState tls.ConnectionState) (module.TLSLevel, error) {\n\tpolicyI, err := c.policyFut.GetContext(ctx)\n\tif err != nil {\n\t\tc.c.log.DebugMsg(\"MTA-STS error\", \"err\", err)\n\t\treturn module.TLSNone, nil\n\t}\n\tpolicy := policyI.(*mtasts.Policy)\n\n\tif policy.Mode != mtasts.ModeEnforce {\n\t\treturn module.TLSNone, nil\n\t}\n\n\tif !tlsState.HandshakeComplete {\n\t\treturn module.TLSNone, &exterrors.SMTPError{\n\t\t\tCode:         451,\n\t\t\tEnhancedCode: exterrors.EnhancedCode{4, 7, 1},\n\t\t\tMessage:      \"TLS is required but unavailable or failed (MTA-STS)\",\n\t\t}\n\t}\n\n\tif tlsState.VerifiedChains == nil {\n\t\treturn module.TLSNone, &exterrors.SMTPError{\n\t\t\tCode:         451,\n\t\t\tEnhancedCode: exterrors.EnhancedCode{4, 7, 1},\n\t\t\tMessage: \"Recipient server TLS certificate is not trusted but \" +\n\t\t\t\t\"authentication is required by MTA-STS\",\n\t\t\tMisc: map[string]interface{}{\n\t\t\t\t\"tls_level\": tlsLevel,\n\t\t\t},\n\t\t}\n\t}\n\n\treturn module.TLSNone, nil\n}\n\nfunc (c *mtastsDelivery) Reset(msgMeta *module.MsgMetadata) {\n\tc.policyFut = nil\n\tif msgMeta != nil {\n\t\tc.log = target.DeliveryLogger(c.c.log, msgMeta)\n\t}\n}\n\n// Stub that will be removed in 0.5.\ntype stsPreloadPolicy struct {\n\tlog      *log.Logger\n\tinstName string\n}\n\nfunc NewSTSPreload(c *container.C, modName, instName string) (module.Module, error) {\n\treturn &stsPreloadPolicy{\n\t\tinstName: instName,\n\t\tlog:      c.DefaultLogger.Sublogger(modName),\n\t}, nil\n}\n\nfunc (c *stsPreloadPolicy) Name() string {\n\treturn c.log.Name\n}\n\nfunc (c *stsPreloadPolicy) InstanceName() string {\n\treturn c.instName\n}\n\nfunc (c *stsPreloadPolicy) Weight() int {\n\treturn 30 // after MTA-STS\n}\n\nfunc (c *stsPreloadPolicy) Configure(inlineArgs []string, cfg *config.Map) error {\n\tc.log.Println(\"sts_preload module is deprecated and is no-op as the list is expired and unmaintained\")\n\n\tvar (\n\t\tsourcePath     string\n\t\tenforceTesting bool\n\t)\n\tcfg.String(\"source\", false, false, \"eff\", &sourcePath)\n\tcfg.Bool(\"enforce_testing\", false, true, &enforceTesting)\n\tif _, err := cfg.Process(); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\ntype preloadDelivery struct {\n\t*stsPreloadPolicy\n}\n\nfunc (p *stsPreloadPolicy) StartDelivery(*module.MsgMetadata) module.DeliveryMXAuthPolicy {\n\treturn &preloadDelivery{stsPreloadPolicy: p}\n}\n\nfunc (p *preloadDelivery) Reset(*module.MsgMetadata)                        {}\nfunc (p *preloadDelivery) PrepareDomain(ctx context.Context, domain string) {}\nfunc (p *preloadDelivery) PrepareConn(ctx context.Context, mx string)       {}\nfunc (p *preloadDelivery) CheckMX(ctx context.Context, mxLevel module.MXLevel, domain, mx string, dnssec bool) (module.MXLevel, error) {\n\treturn mxLevel, nil\n}\n\nfunc (p *preloadDelivery) CheckConn(ctx context.Context, mxLevel module.MXLevel, tlsLevel module.TLSLevel, domain, mx string, tlsState tls.ConnectionState) (module.TLSLevel, error) {\n\treturn tlsLevel, nil\n}\n\ntype dnssecPolicy struct {\n\tinstName string\n}\n\nfunc NewDNSSECPolicy(_ *container.C, _, instName string) (module.Module, error) {\n\treturn &dnssecPolicy{\n\t\tinstName: instName,\n\t}, nil\n}\n\nfunc (dnssecPolicy) Name() string {\n\treturn \"mx_auth.dnssec\"\n}\n\nfunc (c dnssecPolicy) InstanceName() string {\n\treturn c.instName\n}\n\nfunc (dnssecPolicy) Weight() int {\n\treturn 1\n}\n\nfunc (dnssecPolicy) Configure(inlineArgs []string, cfg *config.Map) error {\n\t_, err := cfg.Process() // will fail if there is any directive\n\treturn err\n}\n\nfunc (dnssecPolicy) StartDelivery(*module.MsgMetadata) module.DeliveryMXAuthPolicy {\n\treturn dnssecPolicy{}\n}\n\nfunc (dnssecPolicy) Reset(*module.MsgMetadata)                        {}\nfunc (dnssecPolicy) PrepareDomain(ctx context.Context, domain string) {}\nfunc (dnssecPolicy) PrepareConn(ctx context.Context, mx string)       {}\n\nfunc (dnssecPolicy) CheckMX(ctx context.Context, mxLevel module.MXLevel, domain, mx string, dnssec bool) (module.MXLevel, error) {\n\tif dnssec {\n\t\treturn module.MX_DNSSEC, nil\n\t}\n\treturn module.MXNone, nil\n}\n\nfunc (dnssecPolicy) CheckConn(ctx context.Context, mxLevel module.MXLevel, tlsLevel module.TLSLevel, domain, mx string, tlsState tls.ConnectionState) (module.TLSLevel, error) {\n\treturn module.TLSNone, nil\n}\n\ntype (\n\tdanePolicy struct {\n\t\textResolver *dns.ExtResolver\n\t\tlog         *log.Logger\n\t\tinstName    string\n\t}\n\tdaneDelivery struct {\n\t\tc       *danePolicy\n\t\ttlsaFut *future.Future\n\t}\n)\n\nfunc NewDANEPolicy(c *container.C, modName, instName string) (module.Module, error) {\n\treturn &danePolicy{\n\t\tinstName: instName,\n\t\tlog:      c.DefaultLogger.Sublogger(modName),\n\t}, nil\n}\n\nfunc (c *danePolicy) Name() string {\n\treturn \"mx_auth.dane\"\n}\n\nfunc (c *danePolicy) InstanceName() string {\n\treturn c.instName\n}\n\nfunc (c *danePolicy) Weight() int {\n\treturn 10\n}\n\nfunc (c *danePolicy) Configure(inlineArgs []string, cfg *config.Map) error {\n\tvar err error\n\tc.extResolver, err = dns.NewExtResolver()\n\tif err != nil {\n\t\tc.log.Error(\"DANE support is no-op: unable to init EDNS resolver\", err)\n\t}\n\n\tcfg.Bool(\"debug\", true, log.DefaultLogger.Debug, &c.log.Debug)\n\n\t_, err = cfg.Process()\n\treturn err\n}\n\nfunc (c *danePolicy) StartDelivery(*module.MsgMetadata) module.DeliveryMXAuthPolicy {\n\treturn &daneDelivery{c: c}\n}\n\nfunc (c *daneDelivery) PrepareDomain(ctx context.Context, domain string) {}\n\nfunc (c *daneDelivery) discoverTLSA(ctx context.Context, mx string) ([]dns.TLSA, error) {\n\tadA, rname, err := c.c.extResolver.CheckCNAMEAD(ctx, mx)\n\tif err != nil {\n\t\t// This may indicate a bogus DNSSEC signature or other lookup issue\n\t\t// (including non-existing domain).\n\t\t// Per RFC 7672, any I/O errors (including SERVFAIL) should\n\t\t// cause delivery to be delayed.\n\t\treturn nil, err\n\t}\n\tif rname == \"\" {\n\t\t// No A/AAAA records, short-circuit discovery instead of doing useless\n\t\t// queries.\n\t\treturn nil, errors.New(\"no address associated with the host\")\n\t}\n\tif !adA {\n\t\t// If A lookup is not DNSSEC-authenticated we assume the server cannot\n\t\t// have TLSA record and skip trying to actually lookup TLSA\n\t\t// to avoid hitting weird errors like SERVFAIL, NOTIMP\n\t\t// e.g. see https://github.com/foxcpp/maddy/issues/287\n\t\tif rname == mx {\n\t\t\tc.c.log.Debugln(\"skipping DANE for\", mx, \"due to non-authenticated A records\")\n\t\t\treturn nil, nil\n\t\t}\n\n\t\t// But if it is CNAME'd then we may not want to skip it and actually\n\t\t// consider initial name since it may be signed. To confirm the\n\t\t// initial name is signed, do CNAME lookup.\n\t\tcnameAD, _, err := c.c.extResolver.AuthLookupCNAME(ctx, mx)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif !cnameAD {\n\t\t\tc.c.log.Debugln(\"skipping DANE for\", mx, \"due to non-authenticated CNAME record\")\n\t\t\treturn nil, nil\n\t\t}\n\t}\n\n\t// If there was a CNAME - try it first.\n\tif rname != mx {\n\t\tad, recs, err := c.c.extResolver.AuthLookupTLSA(ctx, \"25\", \"tcp\", rname)\n\t\tif err != nil && !dns.IsNotFound(err) {\n\t\t\treturn nil, err\n\t\t}\n\t\tif ad && len(recs) != 0 {\n\t\t\t// recs may be empty or contain only unusable records - this is\n\t\t\t// okay per RFC 7672, no fallback to initial name is done.\n\t\t\tc.c.log.Debugln(\"using\", len(recs), \"DANE records at\", rname, \"to authenticate\", mx)\n\t\t\treturn recs, nil\n\t\t}\n\t\t// Per RFC 7672 Section 2.2 we interpret a non-authenticated RRset just\n\t\t// like an empty RRset and fallback to trying original name.\n\t\tc.c.log.Debugln(\"ignoring non-authenticated TLSA records for\", rname)\n\t}\n\n\t// If initial name is not a CNAME or final canonical name is not \"secure\"\n\t// - we consider TLSA under the initial name.\n\tad, recs, err := c.c.extResolver.AuthLookupTLSA(ctx, \"25\", \"tcp\", mx)\n\tif err != nil && !dns.IsNotFound(err) {\n\t\treturn nil, err\n\t}\n\tif !ad {\n\t\tc.c.log.Debugln(\"ignoring non-authenticated TLSA records for\", mx)\n\t\treturn nil, nil\n\t}\n\n\tc.c.log.Debugln(\"using\", len(recs), \"DANE records at original name to authenticate\", mx)\n\treturn recs, nil\n}\n\nfunc (c *daneDelivery) PrepareConn(ctx context.Context, mx string) {\n\t// No DNSSEC support.\n\tif c.c.extResolver == nil {\n\t\treturn\n\t}\n\n\tc.tlsaFut = future.New()\n\n\tgo func() {\n\t\tdefer func() {\n\t\t\tif err := recover(); err != nil {\n\t\t\t\tstack := debug.Stack()\n\t\t\t\tlog.Printf(\"panic during extended resolver lookup: %v\\n%s\", err, stack)\n\t\t\t}\n\t\t}()\n\n\t\tc.tlsaFut.Set(c.discoverTLSA(ctx, dns.FQDN(mx)))\n\t}()\n}\n\nfunc (c *daneDelivery) CheckMX(ctx context.Context, mxLevel module.MXLevel, domain, mx string, dnssec bool) (module.MXLevel, error) {\n\treturn module.MXNone, nil\n}\n\nfunc (c *daneDelivery) CheckConn(ctx context.Context, mxLevel module.MXLevel, tlsLevel module.TLSLevel, domain, mx string, tlsState tls.ConnectionState) (module.TLSLevel, error) {\n\t// No DNSSEC support.\n\tif c.c.extResolver == nil {\n\t\treturn module.TLSNone, nil\n\t}\n\n\trecsI, err := c.tlsaFut.GetContext(ctx)\n\tif err != nil {\n\t\t// No records.\n\t\tif dns.IsNotFound(err) {\n\t\t\treturn module.TLSNone, nil\n\t\t}\n\n\t\t// Lookup error here indicates a resolution failure or may also\n\t\t// indicate a bogus DNSSEC signature.\n\t\t// There is a big problem with differentiating these two.\n\t\t//\n\t\t// We assume DANE failure in both cases as a safety measure.\n\t\t// However, there is a possibility of a temporary error condition,\n\t\t// so we mark it as such.\n\t\treturn module.TLSNone, exterrors.WithTemporary(err, true)\n\t}\n\trecs := recsI.([]dns.TLSA)\n\n\toverridePKIX, err := verifyDANE(recs, tlsState)\n\tif err != nil {\n\t\treturn module.TLSNone, err\n\t}\n\tif overridePKIX {\n\t\treturn module.TLSAuthenticated, nil\n\t}\n\treturn module.TLSNone, nil\n}\n\nfunc (c *daneDelivery) Reset(*module.MsgMetadata) {}\n\ntype (\n\tlocalPolicy struct {\n\t\tinstName    string\n\t\tminTLSLevel module.TLSLevel\n\t\tminMXLevel  module.MXLevel\n\t}\n)\n\nfunc NewLocalPolicy(_ *container.C, _, instName string) (module.Module, error) {\n\treturn &localPolicy{\n\t\tinstName: instName,\n\t}, nil\n}\n\nfunc (c *localPolicy) Name() string {\n\treturn \"mx_auth.local_policy\"\n}\n\nfunc (c *localPolicy) InstanceName() string {\n\treturn c.instName\n}\n\nfunc (c *localPolicy) Weight() int {\n\treturn 1000\n}\n\nfunc (c *localPolicy) Configure(inlineArgs []string, cfg *config.Map) error {\n\tvar (\n\t\tminTLSLevel string\n\t\tminMXLevel  string\n\t)\n\n\tcfg.Enum(\"min_tls_level\", false, false,\n\t\t[]string{\"none\", \"encrypted\", \"authenticated\"}, \"encrypted\", &minTLSLevel)\n\tcfg.Enum(\"min_mx_level\", false, false,\n\t\t[]string{\"none\", \"mtasts\", \"dnssec\"}, \"none\", &minMXLevel)\n\tif _, err := cfg.Process(); err != nil {\n\t\treturn err\n\t}\n\n\t// Enum checks the value against allowed list, no 'default' necessary.\n\tswitch minTLSLevel {\n\tcase \"none\":\n\t\tc.minTLSLevel = module.TLSNone\n\tcase \"encrypted\":\n\t\tc.minTLSLevel = module.TLSEncrypted\n\tcase \"authenticated\":\n\t\tc.minTLSLevel = module.TLSAuthenticated\n\t}\n\tswitch minMXLevel {\n\tcase \"none\":\n\t\tc.minMXLevel = module.MXNone\n\tcase \"mtasts\":\n\t\tc.minMXLevel = module.MX_MTASTS\n\tcase \"dnssec\":\n\t\tc.minMXLevel = module.MX_DNSSEC\n\t}\n\n\treturn nil\n}\n\nfunc (l *localPolicy) StartDelivery(msgMeta *module.MsgMetadata) module.DeliveryMXAuthPolicy {\n\treturn l\n}\n\nfunc (l *localPolicy) Reset(*module.MsgMetadata)                        {}\nfunc (l *localPolicy) PrepareDomain(ctx context.Context, domain string) {}\nfunc (l *localPolicy) PrepareConn(ctx context.Context, mx string)       {}\n\nfunc (l *localPolicy) CheckMX(ctx context.Context, mxLevel module.MXLevel, domain, mx string, dnssec bool) (module.MXLevel, error) {\n\tif mxLevel < l.minMXLevel {\n\t\treturn module.MXNone, &exterrors.SMTPError{\n\t\t\t// Err on the side of caution if policy evaluation was messed up by\n\t\t\t// a temporary error (we can't know with the current design).\n\t\t\tCode:         451,\n\t\t\tEnhancedCode: exterrors.EnhancedCode{4, 7, 0},\n\t\t\tMessage:      \"Failed to establish the MX record authenticity\",\n\t\t\tMisc: map[string]interface{}{\n\t\t\t\t\"mx_level\":          mxLevel,\n\t\t\t\t\"required_mx_level\": l.minMXLevel,\n\t\t\t},\n\t\t}\n\t}\n\treturn module.MXNone, nil\n}\n\nfunc (l *localPolicy) CheckConn(ctx context.Context, mxLevel module.MXLevel, tlsLevel module.TLSLevel, domain, mx string, tlsState tls.ConnectionState) (module.TLSLevel, error) {\n\tif tlsLevel < l.minTLSLevel {\n\t\treturn module.TLSNone, &exterrors.SMTPError{\n\t\t\tCode:         451,\n\t\t\tEnhancedCode: exterrors.EnhancedCode{4, 7, 1},\n\t\t\tMessage:      \"TLS it not available or unauthenticated but required\",\n\t\t\tMisc: map[string]interface{}{\n\t\t\t\t\"tls_level\":          tlsLevel,\n\t\t\t\t\"required_tls_level\": l.minTLSLevel,\n\t\t\t},\n\t\t}\n\t}\n\treturn module.TLSNone, nil\n}\n\nfunc init() {\n\tmodules.Register(\"mx_auth.mtasts\", NewMTASTSPolicy)\n\tmodules.Register(\"mx_auth.sts_preload\", NewSTSPreload)\n\tmodules.Register(\"mx_auth.dnssec\", NewDNSSECPolicy)\n\tmodules.Register(\"mx_auth.dane\", NewDANEPolicy)\n\tmodules.Register(\"mx_auth.local_policy\", NewLocalPolicy)\n}\n"
  },
  {
    "path": "internal/target/skeleton.go",
    "content": "//go:build ignore\n// +build ignore\n\n// Copy that file into target/ subdirectory.\n\npackage target_name\n\n/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2021 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\nimport (\n\t\"context\"\n\n\t\"github.com/emersion/go-message/textproto\"\n\t\"github.com/foxcpp/maddy/framework/buffer\"\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/log\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n)\n\nconst modName = \"target.target_name\"\n\ntype Target struct {\n\tinstName string\n\tlog      log.Logger\n}\n\nfunc New(_, instName string, _, inlineArgs []string) (module.Module, error) {\n\t// If wanted, extract any values from inlineArgs (these values:\n\t// deliver_to target_name ARG1 ARG2 { ... }\n\n\treturn &Target{\n\t\tinstName: instName,\n\t\tlog:      log.Logger{Name: instName},\n\t}, nil\n}\n\nfunc (t *Target) Init(cfg *config.Map) error {\n\tcfg.Bool(\"debug\", true, false, &t.log.Debug)\n\n\t// Read any config directives into Target variables here.\n\n\tif _, err := cfg.Process(); err != nil {\n\t\treturn err\n\t}\n\n\t// Finish setup using obtained values.\n\n\treturn nil\n}\n\nfunc (t *Target) Name() string {\n\treturn modName\n}\n\nfunc (t *Target) InstanceName() string {\n\treturn t.instName\n}\n\n// If it necessary to have any server shutdown cleanup - implement Close.\n\nfunc (t *Target) Close() error {\n\treturn nil\n}\n\ntype delivery struct {\n\tt        *Target\n\tmailFrom string\n\tlog      log.Logger\n\tmsgMeta  *module.MsgMetadata\n}\n\n/*\nSee module.DeliveryTarget and module.Delivery docs for details on each method.\n*/\n\nfunc (t *Target) Start(ctx context.Context, msgMeta *module.MsgMetadata, mailFrom string) (module.Delivery, error) {\n\treturn &delivery{\n\t\tt:        t,\n\t\tmailFrom: mailFrom,\n\t\tlog:      DeliveryLogger(t.log, msgMeta),\n\t\tmsgMeta:  msgMeta,\n\t}, nil\n}\n\nfunc (d *delivery) AddRcpt(ctx context.Context, rcptTo string) error {\n\t// Corresponds to SMTP RCPT command.\n\tpanic(\"implement me\")\n}\n\nfunc (d *delivery) Body(ctx context.Context, header textproto.Header, body buffer.Buffer) error {\n\t// Corresponds to SMTP DATA command.\n\tpanic(\"implement me\")\n}\n\n/*\nIf Body call can fail partially (either success or fail for each recipient passed to AddRcpt)\n- implement BodyNonAtomic and signal status for each recipient using StatusCollector callback.\n\nfunc (d *delivery) BodyNonAtomic(ctx context.Context, sc module.StatusCollector, header textproto.Header, body buffer.Buffer) {\n\n}\n*/\n\nfunc (d *delivery) Abort(ctx context.Context) error {\n\tpanic(\"implement me\")\n}\n\nfunc (d *delivery) Commit(ctx context.Context) error {\n\tpanic(\"implement me\")\n}\n\nfunc init() {\n\tmodules.Register(modName, New)\n}\n"
  },
  {
    "path": "internal/target/smtp/sasl.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage smtp_downstream\n\nimport (\n\t\"github.com/emersion/go-sasl\"\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/exterrors\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n)\n\ntype saslClientFactory = func(msgMeta *module.MsgMetadata) (sasl.Client, error)\n\n// saslAuthDirective returns saslClientFactory function used to create sasl.Client.\n// for use in outbound connections.\n//\n// Authentication information of the current client should be passed in arguments.\nfunc saslAuthDirective(_ *config.Map, node config.Node) (interface{}, error) {\n\tif len(node.Children) != 0 {\n\t\treturn nil, config.NodeErr(node, \"can't declare a block here\")\n\t}\n\tif len(node.Args) == 0 {\n\t\treturn nil, config.NodeErr(node, \"at least one argument required\")\n\t}\n\tswitch node.Args[0] {\n\tcase \"off\":\n\t\treturn nil, nil\n\tcase \"forward\":\n\t\tif len(node.Args) > 1 {\n\t\t\treturn nil, config.NodeErr(node, \"no additional arguments required\")\n\t\t}\n\t\treturn func(msgMeta *module.MsgMetadata) (sasl.Client, error) {\n\t\t\tif msgMeta.Conn == nil || msgMeta.Conn.AuthUser == \"\" || msgMeta.Conn.AuthPassword == \"\" {\n\t\t\t\treturn nil, &exterrors.SMTPError{\n\t\t\t\t\tCode:         530,\n\t\t\t\t\tEnhancedCode: exterrors.EnhancedCode{5, 7, 0},\n\t\t\t\t\tMessage:      \"Authentication is required\",\n\t\t\t\t\tTargetName:   \"target.smtp\",\n\t\t\t\t\tReason:       \"Credentials forwarding is requested but the client is not authenticated\",\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn sasl.NewPlainClient(\"\", msgMeta.Conn.AuthUser, msgMeta.Conn.AuthPassword), nil\n\t\t}, nil\n\tcase \"plain\", \"login\":\n\t\tif len(node.Args) != 3 {\n\t\t\treturn nil, config.NodeErr(node, \"two additional arguments are required (username, password)\")\n\t\t}\n\t\treturn func(*module.MsgMetadata) (sasl.Client, error) {\n\t\t\tif node.Args[0] == \"plain\" {\n\t\t\t\treturn sasl.NewPlainClient(\"\", node.Args[1], node.Args[2]), nil\n\t\t\t}\n\t\t\tif node.Args[0] == \"login\" {\n\t\t\t\treturn sasl.NewLoginClient(node.Args[1], node.Args[2]), nil\n\t\t\t}\n\t\t\treturn nil, config.NodeErr(node, \"unknown authentication mechanism: %s\", node.Args[0])\n\t\t}, nil\n\tcase \"external\":\n\t\tif len(node.Args) > 1 {\n\t\t\treturn nil, config.NodeErr(node, \"no additional arguments required\")\n\t\t}\n\t\treturn func(*module.MsgMetadata) (sasl.Client, error) {\n\t\t\treturn sasl.NewExternalClient(\"\"), nil\n\t\t}, nil\n\tdefault:\n\t\treturn nil, config.NodeErr(node, \"unknown authentication mechanism: %s\", node.Args[0])\n\t}\n}\n"
  },
  {
    "path": "internal/target/smtp/sasl_test.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage smtp_downstream\n\nimport (\n\t\"testing\"\n\n\t\"github.com/emersion/go-smtp\"\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/internal/testutils\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc testSaslFactory(t *testing.T, args ...string) saslClientFactory {\n\tfactory, err := saslAuthDirective(&config.Map{}, config.Node{\n\t\tName: \"auth\",\n\t\tArgs: args,\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn factory.(saslClientFactory)\n}\n\nfunc TestSASL_Plain(t *testing.T) {\n\tbe, srv := testutils.SMTPServer(t, \"127.0.0.1:\"+testPort)\n\tdefer func() {\n\t\trequire.NoError(t, srv.Close())\n\t}()\n\tdefer testutils.CheckSMTPConnLeak(t, srv)\n\n\tmod := &Downstream{\n\t\thostname: \"mx.example.invalid\",\n\t\tendpoints: []config.Endpoint{\n\t\t\t{\n\t\t\t\tScheme: \"tcp\",\n\t\t\t\tHost:   \"127.0.0.1\",\n\t\t\t\tPort:   testPort,\n\t\t\t},\n\t\t},\n\t\tsaslFactory: testSaslFactory(t, \"plain\", \"test\", \"testpass\"),\n\t\tlog:         testutils.Logger(t, \"target.smtp\"),\n\t}\n\n\ttestutils.DoTestDelivery(t, mod, \"test@example.invalid\", []string{\"rcpt@example.invalid\"})\n\tbe.CheckMsg(t, 0, \"test@example.invalid\", []string{\"rcpt@example.invalid\"})\n\tif be.Messages[0].AuthUser != \"test\" {\n\t\tt.Errorf(\"Wrong AuthUser: %v\", be.Messages[0].AuthUser)\n\t}\n\tif be.Messages[0].AuthPass != \"testpass\" {\n\t\tt.Errorf(\"Wrong AuthPass: %v\", be.Messages[0].AuthPass)\n\t}\n}\n\nfunc TestSASL_Plain_AuthFail(t *testing.T) {\n\tbe, srv := testutils.SMTPServer(t, \"127.0.0.1:\"+testPort)\n\tdefer func() {\n\t\trequire.NoError(t, srv.Close())\n\t}()\n\tdefer testutils.CheckSMTPConnLeak(t, srv)\n\n\tbe.AuthErr = &smtp.SMTPError{\n\t\tCode:         550,\n\t\tEnhancedCode: smtp.EnhancedCode{5, 1, 2},\n\t\tMessage:      \"Hey\",\n\t}\n\n\tmod := &Downstream{\n\t\thostname: \"mx.example.invalid\",\n\t\tendpoints: []config.Endpoint{\n\t\t\t{\n\t\t\t\tScheme: \"tcp\",\n\t\t\t\tHost:   \"127.0.0.1\",\n\t\t\t\tPort:   testPort,\n\t\t\t},\n\t\t},\n\t\tsaslFactory: testSaslFactory(t, \"plain\", \"test\", \"testpass\"),\n\t\tlog:         testutils.Logger(t, \"target.smtp\"),\n\t}\n\n\t_, err := testutils.DoTestDeliveryErr(t, mod, \"test@example.invalid\", []string{\"rcpt@example.invalid\"})\n\tif err == nil {\n\t\tt.Error(\"Expected an error, got none\")\n\t}\n}\n\nfunc TestSASL_Login_Directive(t *testing.T) {\n\tfactory := testSaslFactory(t, \"login\", \"test\", \"testpass\")\n\tclient, err := factory(nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tmech, _, err := client.Start()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tif mech != \"LOGIN\" {\n\t\tt.Fatalf(\"expected LOGIN mechanism, got %q\", mech)\n\t}\n}\n\nfunc TestSASL_Forward(t *testing.T) {\n\tbe, srv := testutils.SMTPServer(t, \"127.0.0.1:\"+testPort)\n\tdefer func() {\n\t\trequire.NoError(t, srv.Close())\n\t}()\n\tdefer testutils.CheckSMTPConnLeak(t, srv)\n\n\tmod := &Downstream{\n\t\thostname: \"mx.example.invalid\",\n\t\tendpoints: []config.Endpoint{\n\t\t\t{\n\t\t\t\tScheme: \"tcp\",\n\t\t\t\tHost:   \"127.0.0.1\",\n\t\t\t\tPort:   testPort,\n\t\t\t},\n\t\t},\n\t\tsaslFactory: testSaslFactory(t, \"forward\"),\n\t\tlog:         testutils.Logger(t, \"target.smtp\"),\n\t}\n\n\ttestutils.DoTestDeliveryMeta(t, mod, \"test@example.invalid\", []string{\"rcpt@example.invalid\"}, &module.MsgMetadata{\n\t\tConn: &module.ConnState{\n\t\t\tAuthUser:     \"test\",\n\t\t\tAuthPassword: \"testpass\",\n\t\t},\n\t})\n\tbe.CheckMsg(t, 0, \"test@example.invalid\", []string{\"rcpt@example.invalid\"})\n\tif be.Messages[0].AuthUser != \"test\" {\n\t\tt.Errorf(\"Wrong AuthUser: %v\", be.Messages[0].AuthUser)\n\t}\n\tif be.Messages[0].AuthPass != \"testpass\" {\n\t\tt.Errorf(\"Wrong AuthPass: %v\", be.Messages[0].AuthPass)\n\t}\n}\n\nfunc TestSASL_Forward_NoCreds(t *testing.T) {\n\t_, srv := testutils.SMTPServer(t, \"127.0.0.1:\"+testPort)\n\tdefer func() {\n\t\trequire.NoError(t, srv.Close())\n\t}()\n\tdefer testutils.CheckSMTPConnLeak(t, srv)\n\n\tmod := &Downstream{\n\t\thostname: \"mx.example.invalid\",\n\t\tendpoints: []config.Endpoint{\n\t\t\t{\n\t\t\t\tScheme: \"tcp\",\n\t\t\t\tHost:   \"127.0.0.1\",\n\t\t\t\tPort:   testPort,\n\t\t\t},\n\t\t},\n\t\tsaslFactory: testSaslFactory(t, \"forward\"),\n\t\tlog:         testutils.Logger(t, \"target.smtp\"),\n\t}\n\n\t_, err := testutils.DoTestDeliveryErr(t, mod, \"test@example.invalid\", []string{\"rcpt@example.invalid\"})\n\tif err == nil {\n\t\tt.Error(\"Expected an error, got none\")\n\t}\n}\n"
  },
  {
    "path": "internal/target/smtp/smtp_downstream.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\n// Package smtp_downstream provides target.smtp module that implements\n// transparent forwarding or messages to configured list of SMTP servers.\n//\n// Like remote module, this implementation doesn't handle atomic\n// delivery properly since it is impossible to do with SMTP protocol\n//\n// Interfaces implemented:\n// - module.DeliveryTarget\npackage smtp_downstream\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"net\"\n\t\"runtime/trace\"\n\t\"time\"\n\n\t\"github.com/emersion/go-message/textproto\"\n\t\"github.com/emersion/go-smtp\"\n\t\"github.com/foxcpp/maddy/framework/buffer\"\n\t\"github.com/foxcpp/maddy/framework/config\"\n\ttls2 \"github.com/foxcpp/maddy/framework/config/tls\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/exterrors\"\n\t\"github.com/foxcpp/maddy/framework/log\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n\t\"github.com/foxcpp/maddy/internal/smtpconn\"\n\t\"github.com/foxcpp/maddy/internal/target\"\n\t\"golang.org/x/net/idna\"\n)\n\ntype Downstream struct {\n\tmodName  string\n\tinstName string\n\tlmtp     bool\n\n\tstarttls    bool\n\thostname    string\n\tendpoints   []config.Endpoint\n\tsaslFactory saslClientFactory\n\ttlsConfig   *tls.Config\n\n\tconnectTimeout    time.Duration\n\tcommandTimeout    time.Duration\n\tsubmissionTimeout time.Duration\n\n\tlog *log.Logger\n}\n\nfunc (u *Downstream) moduleError(err error) error {\n\tif err == nil {\n\t\treturn nil\n\t}\n\n\treturn exterrors.WithFields(err, map[string]interface{}{\n\t\t\"target\": u.modName,\n\t})\n}\n\nfunc New(c *container.C, modName, instName string) (module.Module, error) {\n\treturn &Downstream{\n\t\tmodName:  modName,\n\t\tinstName: instName,\n\t\tlmtp:     modName == \"target.lmtp\",\n\t\tlog:      c.DefaultLogger.Sublogger(modName),\n\t}, nil\n}\n\nfunc (u *Downstream) Configure(inlineArgs []string, cfg *config.Map) error {\n\tvar attemptTLS *bool\n\n\ttargetsArg := make([]string, 0, len(inlineArgs))\n\tcfg.Bool(\"debug\", true, false, &u.log.Debug)\n\tcfg.Callback(\"require_tls\", func(m *config.Map, node config.Node) error {\n\t\tu.log.Msg(\"require_tls directive is deprecated and ignored\")\n\t\treturn nil\n\t})\n\tcfg.Callback(\"attempt_starttls\", func(m *config.Map, node config.Node) error {\n\t\tu.log.Msg(\"attempt_starttls directive is deprecated and equivalent to starttls\")\n\n\t\tif len(node.Args) == 0 {\n\t\t\ttrueVal := true\n\t\t\tattemptTLS = &trueVal\n\t\t\treturn nil\n\t\t}\n\t\tif len(node.Args) != 1 {\n\t\t\treturn config.NodeErr(node, \"expected exactly 1 argument\")\n\t\t}\n\n\t\tb, err := config.ParseBool(node.Args[0])\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tattemptTLS = &b\n\t\treturn nil\n\t})\n\tcfg.Bool(\"starttls\", false, !u.lmtp, &u.starttls)\n\tcfg.String(\"hostname\", true, true, \"\", &u.hostname)\n\tcfg.StringList(\"targets\", false, false, nil, &targetsArg)\n\tcfg.Custom(\"auth\", false, false, func() (interface{}, error) {\n\t\treturn nil, nil\n\t}, saslAuthDirective, &u.saslFactory)\n\tcfg.Custom(\"tls_client\", true, false, func() (interface{}, error) {\n\t\treturn &tls.Config{}, nil\n\t}, tls2.TLSClientBlock, &u.tlsConfig)\n\tcfg.Duration(\"connect_timeout\", false, false, 5*time.Minute, &u.connectTimeout)\n\tcfg.Duration(\"command_timeout\", false, false, 5*time.Minute, &u.commandTimeout)\n\tcfg.Duration(\"submission_timeout\", false, false, 5*time.Minute, &u.submissionTimeout)\n\n\tif _, err := cfg.Process(); err != nil {\n\t\treturn err\n\t}\n\n\tif attemptTLS != nil {\n\t\tu.starttls = *attemptTLS\n\t}\n\n\t// INTERNATIONALIZATION: See RFC 6531 Section 3.7.1.\n\tvar err error\n\tu.hostname, err = idna.ToASCII(u.hostname)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"%s: cannot represent the hostname as an A-label name: %w\", u.modName, err)\n\t}\n\n\ttargetsArg = append(targetsArg, inlineArgs...)\n\tfor _, tgt := range targetsArg {\n\t\tendp, err := config.ParseEndpoint(tgt)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tu.endpoints = append(u.endpoints, endp)\n\t}\n\n\tif len(u.endpoints) == 0 {\n\t\treturn fmt.Errorf(\"%s: at least one target endpoint is required\", u.modName)\n\t}\n\n\treturn nil\n}\n\nfunc (u *Downstream) Name() string {\n\treturn u.modName\n}\n\nfunc (u *Downstream) InstanceName() string {\n\treturn u.instName\n}\n\ntype delivery struct {\n\tu   *Downstream\n\tlog *log.Logger\n\n\tmsgMeta  *module.MsgMetadata\n\tmailFrom string\n\trcpts    []string\n\n\tconn *smtpconn.C\n}\n\n// lmtpDelivery implements module.PartialDelivery\ntype lmtpDelivery struct {\n\t*delivery\n}\n\nfunc (u *Downstream) StartDelivery(ctx context.Context, msgMeta *module.MsgMetadata, mailFrom string) (module.Delivery, error) {\n\tdefer trace.StartRegion(ctx, \"target.smtp/StartDelivery\").End()\n\n\td := &delivery{\n\t\tu:        u,\n\t\tlog:      target.DeliveryLogger(u.log, msgMeta),\n\t\tmsgMeta:  msgMeta,\n\t\tmailFrom: mailFrom,\n\t}\n\tif err := d.connect(ctx); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err := d.conn.Mail(ctx, mailFrom, msgMeta.SMTPOpts); err != nil {\n\t\tif err := d.conn.Close(); err != nil {\n\t\t\tu.log.Error(\"failed to close smtp connection\", err)\n\t\t}\n\t\treturn nil, err\n\t}\n\n\tif u.lmtp {\n\t\treturn &lmtpDelivery{delivery: d}, nil\n\t}\n\n\treturn d, nil\n}\n\nfunc (d *delivery) closeConn(c *smtpconn.C) {\n\tif err := c.Close(); err != nil {\n\t\td.log.Error(\"failed to close SMTP connection\", err)\n\t}\n}\n\nfunc (d *delivery) connect(ctx context.Context) error {\n\t// TODO: Review possibility of connection pooling here.\n\tvar lastErr error\n\n\tconn := smtpconn.New()\n\tconn.Log = d.log\n\tconn.Hostname = d.u.hostname\n\tconn.AddrInSMTPMsg = false\n\tif d.u.connectTimeout != 0 {\n\t\tconn.ConnectTimeout = d.u.connectTimeout\n\t}\n\tif d.u.commandTimeout != 0 {\n\t\tconn.CommandTimeout = d.u.commandTimeout\n\t}\n\tif d.u.submissionTimeout != 0 {\n\t\tconn.SubmissionTimeout = d.u.submissionTimeout\n\t}\n\n\tfor _, endp := range d.u.endpoints {\n\t\tvar err error\n\t\tif d.u.lmtp {\n\t\t\t_, err = conn.ConnectLMTP(ctx, endp, d.u.starttls, d.u.tlsConfig)\n\t\t} else {\n\t\t\t_, err = conn.Connect(ctx, endp, d.u.starttls, d.u.tlsConfig)\n\t\t}\n\t\tif err != nil {\n\t\t\tif len(d.u.endpoints) != 1 {\n\t\t\t\td.log.Msg(\"connect error\", err, \"downstream_server\", net.JoinHostPort(endp.Host, endp.Port))\n\t\t\t}\n\t\t\tlastErr = err\n\t\t\tcontinue\n\t\t}\n\n\t\td.log.DebugMsg(\"connected\", \"downstream_server\", conn.ServerName())\n\n\t\tlastErr = nil\n\t\tbreak\n\t}\n\tif lastErr != nil {\n\t\treturn d.u.moduleError(lastErr)\n\t}\n\n\tif d.u.saslFactory != nil {\n\t\tsaslClient, err := d.u.saslFactory(d.msgMeta)\n\t\tif err != nil {\n\t\t\td.closeConn(conn)\n\t\t\treturn err\n\t\t}\n\n\t\tif err := conn.Client().Auth(saslClient); err != nil {\n\t\t\td.closeConn(conn)\n\t\t\treturn err\n\t\t}\n\t}\n\n\td.conn = conn\n\n\treturn nil\n}\n\nfunc (d *delivery) AddRcpt(ctx context.Context, rcptTo string, opts smtp.RcptOptions) error {\n\terr := d.conn.Rcpt(ctx, rcptTo, opts)\n\tif err != nil {\n\t\treturn d.u.moduleError(err)\n\t}\n\n\td.rcpts = append(d.rcpts, rcptTo)\n\treturn nil\n}\n\nfunc (d *delivery) Body(ctx context.Context, header textproto.Header, body buffer.Buffer) error {\n\tr, err := body.Open()\n\tif err != nil {\n\t\treturn exterrors.WithFields(err, map[string]interface{}{\"target\": d.u.modName})\n\t}\n\n\tdefer func() {\n\t\tif err := r.Close(); err != nil {\n\t\t\td.log.Msg(\"failed to close body buffer\", err)\n\t\t}\n\t}()\n\treturn d.u.moduleError(d.conn.Data(ctx, header, r))\n}\n\nfunc (d *lmtpDelivery) BodyNonAtomic(ctx context.Context, sc module.StatusCollector, header textproto.Header, body buffer.Buffer) {\n\tr, err := body.Open()\n\tif err != nil {\n\t\tmodErr := d.u.moduleError(err)\n\t\tfor _, rcpt := range d.rcpts {\n\t\t\tsc.SetStatus(rcpt, modErr)\n\t\t}\n\t}\n\tdefer func() {\n\t\tif err := r.Close(); err != nil {\n\t\t\td.log.Msg(\"failed to close body buffer\", err)\n\t\t}\n\t}()\n\n\trcptIndx := 0\n\terr = d.conn.LMTPData(ctx, header, r, func(rcpt string, err *smtp.SMTPError) {\n\t\tif err == nil {\n\t\t\tsc.SetStatus(rcpt, nil)\n\t\t} else {\n\t\t\tsc.SetStatus(rcpt, &exterrors.SMTPError{\n\t\t\t\tCode:         err.Code,\n\t\t\t\tEnhancedCode: exterrors.EnhancedCode(err.EnhancedCode),\n\t\t\t\tMessage:      err.Message,\n\t\t\t\tTargetName:   d.u.modName,\n\t\t\t\tErr:          err,\n\t\t\t})\n\t\t}\n\t\trcptIndx++\n\t})\n\tif err != nil {\n\t\tmodErr := d.u.moduleError(err)\n\t\tfor _, rcpt := range d.rcpts[rcptIndx:] {\n\t\t\tsc.SetStatus(rcpt, modErr)\n\t\t}\n\t}\n}\n\nfunc (d *delivery) Abort(ctx context.Context) error {\n\treturn d.conn.Close()\n}\n\nfunc (d *delivery) Commit(ctx context.Context) error {\n\treturn d.conn.Close()\n}\n\nfunc init() {\n\tmodules.Register(\"target.smtp\", New)\n\tmodules.Register(\"target.lmtp\", New)\n}\n"
  },
  {
    "path": "internal/target/smtp/smtp_downstream_test.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage smtp_downstream\n\nimport (\n\t\"errors\"\n\t\"flag\"\n\t\"math/rand\"\n\t\"os\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"github.com/emersion/go-smtp\"\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/exterrors\"\n\t\"github.com/foxcpp/maddy/internal/testutils\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nvar testPort string\n\nfunc TestDownstreamDelivery(t *testing.T) {\n\tbe, srv := testutils.SMTPServer(t, \"127.0.0.1:\"+testPort)\n\tdefer func() {\n\t\trequire.NoError(t, srv.Close())\n\t}()\n\tdefer testutils.CheckSMTPConnLeak(t, srv)\n\n\ttarpit := testutils.FailOnConn(t, \"127.0.0.2:\"+testPort)\n\tdefer func() {\n\t\trequire.NoError(t, tarpit.Close())\n\t}()\n\n\tmod := &Downstream{\n\t\thostname: \"mx.example.invalid\",\n\t\tendpoints: []config.Endpoint{\n\t\t\t{\n\t\t\t\tScheme: \"tcp\",\n\t\t\t\tHost:   \"127.0.0.1\",\n\t\t\t\tPort:   testPort,\n\t\t\t},\n\t\t\t{\n\t\t\t\tScheme: \"tcp\",\n\t\t\t\tHost:   \"127.0.0.2\",\n\t\t\t\tPort:   testPort,\n\t\t\t},\n\t\t},\n\t\tlog: testutils.Logger(t, \"target.smtp\"),\n\t}\n\n\ttestutils.DoTestDelivery(t, mod, \"test@example.invalid\", []string{\"rcpt@example.invalid\"})\n\tbe.CheckMsg(t, 0, \"test@example.invalid\", []string{\"rcpt@example.invalid\"})\n}\n\nfunc TestDownstreamDelivery_LMTP(t *testing.T) {\n\tbe, srv := testutils.SMTPServer(t, \"127.0.0.1:\"+testPort, func(srv *smtp.Server) {\n\t\tsrv.LMTP = true\n\t})\n\tbe.LMTPDataErr = []error{\n\t\tnil,\n\t\t&smtp.SMTPError{\n\t\t\tCode:    501,\n\t\t\tMessage: \"nop\",\n\t\t},\n\t}\n\tdefer func() {\n\t\trequire.NoError(t, srv.Close())\n\t}()\n\tdefer testutils.CheckSMTPConnLeak(t, srv)\n\n\tmod := &Downstream{\n\t\thostname: \"mx.example.invalid\",\n\t\tendpoints: []config.Endpoint{\n\t\t\t{\n\t\t\t\tScheme: \"tcp\",\n\t\t\t\tHost:   \"127.0.0.1\",\n\t\t\t\tPort:   testPort,\n\t\t\t},\n\t\t},\n\t\tmodName: \"target.lmtp\",\n\t\tlmtp:    true,\n\t\tlog:     testutils.Logger(t, \"lmtp_downstream\"),\n\t}\n\n\tsc := make(statusCollector)\n\n\ttestutils.DoTestDeliveryNonAtomic(t, &sc, mod, \"test@example.invalid\", []string{\"rcpt1@example.invalid\", \"rcpt2@example.invalid\"})\n\tbe.CheckMsg(t, 0, \"test@example.invalid\", []string{\"rcpt1@example.invalid\", \"rcpt2@example.invalid\"})\n\n\tif len(sc) != 2 {\n\t\tt.Fatal(\"Two statuses should be set\")\n\t}\n\tif err := sc[\"rcpt1@example.invalid\"]; err != nil {\n\t\tt.Fatal(\"Unexpected error for rcpt1:\", err)\n\t}\n\tif sc[\"rcpt2@example.invalid\"] == nil {\n\t\tt.Fatal(\"Expected an error for rcpt2\")\n\t}\n\tvar rcptErr *exterrors.SMTPError\n\tif !errors.As(sc[\"rcpt2@example.invalid\"], &rcptErr) {\n\t\tt.Fatalf(\"Not SMTPError: %T\", rcptErr)\n\t}\n\tif rcptErr.Code != 501 {\n\t\tt.Fatal(\"Wrong SMTP code:\", rcptErr.Code)\n\t}\n}\n\nfunc TestDownstreamDelivery_LMTP_ErrorCoerce(t *testing.T) {\n\tbe, srv := testutils.SMTPServer(t, \"127.0.0.1:\"+testPort, func(srv *smtp.Server) {\n\t\tsrv.LMTP = true\n\t})\n\tbe.LMTPDataErr = []error{\n\t\tnil,\n\t\t&smtp.SMTPError{\n\t\t\tCode:    501,\n\t\t\tMessage: \"nop\",\n\t\t},\n\t}\n\tdefer func() {\n\t\trequire.NoError(t, srv.Close())\n\t}()\n\tdefer testutils.CheckSMTPConnLeak(t, srv)\n\n\tmod := &Downstream{\n\t\thostname: \"mx.example.invalid\",\n\t\tendpoints: []config.Endpoint{\n\t\t\t{\n\t\t\t\tScheme: \"tcp\",\n\t\t\t\tHost:   \"127.0.0.1\",\n\t\t\t\tPort:   testPort,\n\t\t\t},\n\t\t},\n\t\tmodName: \"target.lmtp\",\n\t\tlmtp:    true,\n\t\tlog:     testutils.Logger(t, \"lmtp_downstream\"),\n\t}\n\n\t_, err := testutils.DoTestDeliveryErr(t, mod, \"test@example.invalid\", []string{\"rcpt1@example.invalid\", \"rcpt2@example.invalid\"})\n\tif err == nil {\n\t\tt.Error(\"expected failure\")\n\t}\n}\n\ntype statusCollector map[string]error\n\nfunc (sc *statusCollector) SetStatus(rcptTo string, err error) {\n\t(*sc)[rcptTo] = err\n}\n\nfunc TestDownstreamDelivery_Fallback(t *testing.T) {\n\tbe, srv := testutils.SMTPServer(t, \"127.0.0.2:\"+testPort)\n\tdefer func() {\n\t\trequire.NoError(t, srv.Close())\n\t}()\n\tdefer testutils.CheckSMTPConnLeak(t, srv)\n\n\tmod := &Downstream{\n\t\thostname: \"mx.example.invalid\",\n\t\tendpoints: []config.Endpoint{\n\t\t\t{\n\t\t\t\tScheme: \"tcp\",\n\t\t\t\tHost:   \"127.0.0.1\",\n\t\t\t\tPort:   testPort,\n\t\t\t},\n\t\t\t{\n\t\t\t\tScheme: \"tcp\",\n\t\t\t\tHost:   \"127.0.0.2\",\n\t\t\t\tPort:   testPort,\n\t\t\t},\n\t\t},\n\t\tlog: testutils.Logger(t, \"target.smtp\"),\n\t}\n\n\ttestutils.DoTestDelivery(t, mod, \"test@example.invalid\", []string{\"rcpt@example.invalid\"})\n\tbe.CheckMsg(t, 0, \"test@example.invalid\", []string{\"rcpt@example.invalid\"})\n}\n\nfunc TestDownstreamDelivery_MAILErr(t *testing.T) {\n\tbe, srv := testutils.SMTPServer(t, \"127.0.0.1:\"+testPort)\n\tdefer func() {\n\t\trequire.NoError(t, srv.Close())\n\t}()\n\tdefer testutils.CheckSMTPConnLeak(t, srv)\n\n\tbe.MailErr = &smtp.SMTPError{\n\t\tCode:         550,\n\t\tEnhancedCode: smtp.EnhancedCode{5, 1, 2},\n\t\tMessage:      \"Hey\",\n\t}\n\n\tmod := &Downstream{\n\t\thostname: \"mx.example.invalid\",\n\t\tendpoints: []config.Endpoint{\n\t\t\t{\n\t\t\t\tScheme: \"tcp\",\n\t\t\t\tHost:   \"127.0.0.1\",\n\t\t\t\tPort:   testPort,\n\t\t\t},\n\t\t},\n\t\tlog: testutils.Logger(t, \"target.smtp\"),\n\t}\n\n\t_, err := testutils.DoTestDeliveryErr(t, mod, \"test@example.invalid\", []string{\"rcpt@example.invalid\"})\n\ttestutils.CheckSMTPErr(t, err, 550, exterrors.EnhancedCode{5, 1, 2}, \"Hey\")\n}\n\nfunc TestDownstreamDelivery_StartTLS(t *testing.T) {\n\tclientCfg, be, srv := testutils.SMTPServerSTARTTLS(t, \"127.0.0.1:\"+testPort)\n\tdefer func() {\n\t\trequire.NoError(t, srv.Close())\n\t}()\n\tdefer testutils.CheckSMTPConnLeak(t, srv)\n\n\tmod := &Downstream{\n\t\thostname: \"mx.example.invalid\",\n\t\tendpoints: []config.Endpoint{\n\t\t\t{\n\t\t\t\tScheme: \"tcp\",\n\t\t\t\tHost:   \"127.0.0.1\",\n\t\t\t\tPort:   testPort,\n\t\t\t},\n\t\t},\n\t\ttlsConfig: clientCfg.Clone(),\n\t\tstarttls:  true,\n\t\tlog:       testutils.Logger(t, \"target.smtp\"),\n\t}\n\n\ttestutils.DoTestDelivery(t, mod, \"test@example.invalid\", []string{\"rcpt@example.invalid\"})\n\tbe.CheckMsg(t, 0, \"test@example.invalid\", []string{\"rcpt@example.invalid\"})\n\n\ttlsState, ok := be.Messages[0].Conn.TLSConnectionState()\n\tif !ok || !tlsState.HandshakeComplete {\n\t\tt.Fatal(\"Message was not delivered over TLS\")\n\t}\n}\n\nfunc TestDownstreamDelivery_StartTLS_NoFallback(t *testing.T) {\n\t_, srv := testutils.SMTPServer(t, \"127.0.0.1:\"+testPort)\n\tdefer func() {\n\t\trequire.NoError(t, srv.Close())\n\t}()\n\tdefer testutils.CheckSMTPConnLeak(t, srv)\n\n\tmod := &Downstream{\n\t\thostname: \"mx.example.invalid\",\n\t\tendpoints: []config.Endpoint{\n\t\t\t{\n\t\t\t\tScheme: \"tcp\",\n\t\t\t\tHost:   \"127.0.0.1\",\n\t\t\t\tPort:   testPort,\n\t\t\t},\n\t\t},\n\t\tstarttls: true,\n\t\tlog:      testutils.Logger(t, \"target.smtp\"),\n\t}\n\n\t_, err := testutils.DoTestDeliveryErr(t, mod, \"test@example.invalid\", []string{\"rcpt@example.invalid\"})\n\tif err == nil {\n\t\tt.Error(\"Expected an error, got none\")\n\t}\n}\n\nfunc TestMain(m *testing.M) {\n\tremoteSmtpPort := flag.String(\"test.smtpport\", \"random\", \"(maddy) SMTP port to use for connections in tests\")\n\tflag.Parse()\n\n\tif *remoteSmtpPort == \"random\" {\n\t\t*remoteSmtpPort = strconv.Itoa(rand.Intn(65536-10000) + 10000)\n\t}\n\n\ttestPort = *remoteSmtpPort\n\tos.Exit(m.Run())\n}\n"
  },
  {
    "path": "internal/target/smtp/smtputf8_test.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage smtp_downstream\n\nimport (\n\t\"testing\"\n\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/internal/testutils\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestDownstreamDelivery_EHLO_ALabel(t *testing.T) {\n\tbe, srv := testutils.SMTPServer(t, \"127.0.0.1:\"+testPort)\n\tdefer func() {\n\t\trequire.NoError(t, srv.Close())\n\t}()\n\tdefer testutils.CheckSMTPConnLeak(t, srv)\n\n\tmod, err := New(container.New(), \"\", \"\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := mod.Configure([]string{\"tcp://127.0.0.1:\" + testPort}, config.NewMap(nil, config.Node{\n\t\tChildren: []config.Node{\n\t\t\t{\n\t\t\t\tName: \"hostname\",\n\t\t\t\tArgs: []string{\"тест.invalid\"},\n\t\t\t},\n\t\t\t{\n\t\t\t\tName: \"starttls\",\n\t\t\t\tArgs: []string{\"no\"},\n\t\t\t},\n\t\t},\n\t})); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\ttgt := mod.(*Downstream)\n\ttgt.log = testutils.Logger(t, \"remote\")\n\n\ttestutils.DoTestDelivery(t, tgt, \"test@example.com\", []string{\"test@example.invalid\"})\n\n\tbe.CheckMsg(t, 0, \"test@example.com\", []string{\"test@example.invalid\"})\n\tif be.Messages[0].Conn.Hostname() != \"xn--e1aybc.invalid\" {\n\t\tt.Error(\"target/remote should use use Punycode in EHLO\")\n\t}\n}\n"
  },
  {
    "path": "internal/testutils/bench_delivery.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage testutils\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"crypto/sha1\"\n\t\"encoding/hex\"\n\t\"io\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/emersion/go-message/textproto\"\n\t\"github.com/emersion/go-smtp\"\n\t\"github.com/foxcpp/maddy/framework/buffer\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n)\n\n// Empirically observed \"around average\" values.\nconst (\n\tMessageBodySize             = 100 * 1024\n\tExtraMessageHeaderFields    = 10\n\tExtraMessageHeaderFieldSize = 50\n)\n\nconst testHeaderString = \"Content-Type: multipart/mixed; boundary=message-boundary\\r\\n\" +\n\t\"Date: Sat, 19 Jun 2016 12:00:00 +0900\\r\\n\" +\n\t\"From: Mitsuha Miyamizu <mitsuha.miyamizu@example.org>\\r\\n\" +\n\t\"Reply-To: Mitsuha Miyamizu <mitsuha.miyamizu+replyto@example.org>\\r\\n\" +\n\t\"Message-Id: 42@example.org\\r\\n\" +\n\t\"MIME-Version: 1.0\\r\\n\" +\n\t\"Content-Transfer-Encoding: 8but\\r\\n\" +\n\t\"Subject: Your Name.\\r\\n\" +\n\t\"To: Taki Tachibana <taki.tachibana@example.org>\\r\\n\" +\n\t\"\\r\\n\"\n\nconst testAltHeaderString = \"Content-Type: multipart/alternative; boundary=b2\\r\\n\" +\n\t\"\\r\\n\"\n\nconst testTextHeaderString = \"Content-Disposition: inline\\r\\n\" +\n\t\"Content-Type: text/plain\\r\\n\" +\n\t\"\\r\\n\"\n\nconst testTextBodyString = \"What's your name?\"\n\nconst testTextString = testTextHeaderString + testTextBodyString\n\nconst testHTMLHeaderString = \"Content-Disposition: inline\\r\\n\" +\n\t\"Content-Type: text/html\\r\\n\" +\n\t\"\\r\\n\"\n\nconst testHTMLBodyString = \"<div>What's <i>your</i> name?</div>\"\n\nconst testHTMLString = testHTMLHeaderString + testHTMLBodyString\n\nconst testAttachmentHeaderString = \"Content-Disposition: attachment; filename=note.txt\\r\\n\" +\n\t\"Content-Type: text/plain\\r\\n\" +\n\t\"\\r\\n\"\n\nconst testAttachmentBodyString = \"My name is Mitsuha.\"\n\nconst testAttachmentString = testAttachmentHeaderString + testAttachmentBodyString\n\nconst testBodyString = \"--message-boundary\\r\\n\" +\n\ttestAltHeaderString +\n\t\"\\r\\n--b2\\r\\n\" +\n\ttestTextString +\n\t\"\\r\\n--b2\\r\\n\" +\n\ttestHTMLString +\n\t\"\\r\\n--b2--\\r\\n\" +\n\t\"\\r\\n--message-boundary\\r\\n\" +\n\ttestAttachmentString +\n\t\"\\r\\n--message-boundary--\\r\\n\"\n\nvar testMailString = testHeaderString + testBodyString + strings.Repeat(\"A\", MessageBodySize)\n\nfunc RandomMsg(b *testing.B) (module.MsgMetadata, textproto.Header, buffer.Buffer) {\n\tIDRaw := sha1.Sum([]byte(b.Name()))\n\tencodedID := hex.EncodeToString(IDRaw[:])\n\n\tbody := bufio.NewReader(strings.NewReader(testMailString))\n\thdr, _ := textproto.ReadHeader(body)\n\tfor i := 0; i < ExtraMessageHeaderFields; i++ {\n\t\thdr.Add(\"AAAAAAAAAAAA-\"+strconv.Itoa(i), strings.Repeat(\"A\", ExtraMessageHeaderFieldSize))\n\t}\n\tbodyBlob, _ := io.ReadAll(body)\n\n\treturn module.MsgMetadata{\n\t\tDontTraceSender: true,\n\t\tID:              encodedID,\n\t}, hdr, buffer.MemoryBuffer{Slice: bodyBlob}\n}\n\nfunc BenchDelivery(b *testing.B, target module.DeliveryTarget, sender string, recipientTemplates []string) {\n\tmeta, header, body := RandomMsg(b)\n\n\tbenchCtx := context.Background()\n\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\tdelivery, err := target.StartDelivery(benchCtx, &meta, sender)\n\t\tif err != nil {\n\t\t\tb.Fatal(err)\n\t\t}\n\n\t\tfor i, rcptTemplate := range recipientTemplates {\n\t\t\trcpt := strings.ReplaceAll(rcptTemplate, \"X\", strconv.Itoa(i))\n\n\t\t\tif err := delivery.AddRcpt(benchCtx, rcpt, smtp.RcptOptions{}); err != nil {\n\t\t\t\tb.Fatal(err)\n\t\t\t}\n\t\t}\n\n\t\tif err := delivery.Body(benchCtx, header, body); err != nil {\n\t\t\tb.Fatal(err)\n\t\t}\n\n\t\tif err := delivery.Commit(benchCtx); err != nil {\n\t\t\tb.Fatal(err)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/testutils/buffer.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage testutils\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"io\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/emersion/go-message/textproto\"\n\t\"github.com/foxcpp/maddy/framework/buffer\"\n)\n\nfunc BodyFromStr(t *testing.T, literal string) (textproto.Header, buffer.MemoryBuffer) {\n\tt.Helper()\n\n\tbufr := bufio.NewReader(strings.NewReader(literal))\n\thdr, err := textproto.ReadHeader(bufr)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tbody, err := io.ReadAll(bufr)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\treturn hdr, buffer.MemoryBuffer{Slice: body}\n}\n\ntype errorReader struct {\n\tr   io.Reader\n\terr error\n}\n\nfunc (r *errorReader) Read(b []byte) (int, error) {\n\tn, err := r.r.Read(b)\n\tif err == io.EOF {\n\t\treturn n, r.err\n\t}\n\treturn n, err\n}\n\ntype FailingBuffer struct {\n\tBlob []byte\n\n\tOpenError error\n\tIOError   error\n}\n\nfunc (fb FailingBuffer) Open() (io.ReadCloser, error) {\n\tr := io.NopCloser(bytes.NewReader(fb.Blob))\n\n\tif fb.IOError != nil {\n\t\treturn io.NopCloser(&errorReader{r, fb.IOError}), fb.OpenError\n\t}\n\n\treturn r, fb.OpenError\n}\n\nfunc (fb FailingBuffer) Len() int {\n\treturn len(fb.Blob)\n}\n\nfunc (fb FailingBuffer) Remove() error {\n\treturn nil\n}\n"
  },
  {
    "path": "internal/testutils/check.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage testutils\n\nimport (\n\t\"context\"\n\n\t\"github.com/emersion/go-message/textproto\"\n\t\"github.com/foxcpp/maddy/framework/buffer\"\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n)\n\ntype Check struct {\n\tInitErr   error\n\tEarlyErr  error\n\tConnRes   module.CheckResult\n\tSenderRes module.CheckResult\n\tRcptRes   module.CheckResult\n\tBodyRes   module.CheckResult\n\n\tConnCalls   int\n\tSenderCalls int\n\tRcptCalls   int\n\tBodyCalls   int\n\n\tUnclosedStates int\n\n\tInstName string\n}\n\nfunc (c *Check) CheckStateForMsg(ctx context.Context, msgMeta *module.MsgMetadata) (module.CheckState, error) {\n\tif c.InitErr != nil {\n\t\treturn nil, c.InitErr\n\t}\n\n\tc.UnclosedStates++\n\treturn &checkState{msgMeta, c}, nil\n}\n\nfunc (c *Check) Configure([]string, *config.Map) error {\n\treturn nil\n}\n\nfunc (c *Check) Name() string {\n\treturn \"test_check\"\n}\n\nfunc (c *Check) InstanceName() string {\n\tif c.InstName != \"\" {\n\t\treturn c.InstName\n\t}\n\treturn \"test_check\"\n}\n\nfunc (c *Check) CheckConnection(ctx context.Context, state *module.ConnState) error {\n\treturn c.EarlyErr\n}\n\ntype checkState struct {\n\tmsgMeta *module.MsgMetadata\n\tcheck   *Check\n}\n\nfunc (cs *checkState) CheckConnection(ctx context.Context) module.CheckResult {\n\tcs.check.ConnCalls++\n\treturn cs.check.ConnRes\n}\n\nfunc (cs *checkState) CheckSender(ctx context.Context, from string) module.CheckResult {\n\tcs.check.SenderCalls++\n\treturn cs.check.SenderRes\n}\n\nfunc (cs *checkState) CheckRcpt(ctx context.Context, to string) module.CheckResult {\n\tcs.check.RcptCalls++\n\treturn cs.check.RcptRes\n}\n\nfunc (cs *checkState) CheckBody(ctx context.Context, header textproto.Header, body buffer.Buffer) module.CheckResult {\n\tcs.check.BodyCalls++\n\treturn cs.check.BodyRes\n}\n\nfunc (cs *checkState) Close() error {\n\tcs.check.UnclosedStates--\n\treturn nil\n}\n\nfunc init() {\n\tmodules.Register(\"test_check\", func(_ *container.C, _, _ string) (module.Module, error) {\n\t\treturn &Check{}, nil\n\t})\n}\n"
  },
  {
    "path": "internal/testutils/filesystem.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage testutils\n\nimport (\n\t\"os\"\n\t\"testing\"\n)\n\n// Dir is a wrapper for os.MkdirTemp that\n// fails the test on errors.\nfunc Dir(t *testing.T) string {\n\tdir, err := os.MkdirTemp(\"\", \"maddy-tests-\")\n\tif err != nil {\n\t\tt.Fatalf(\"can't create test dir: %v\", err)\n\t}\n\treturn dir\n}\n"
  },
  {
    "path": "internal/testutils/logger.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage testutils\n\nimport (\n\t\"flag\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/foxcpp/maddy/framework/log\"\n)\n\nvar (\n\tdebugLog  = flag.Bool(\"test.debuglog\", false, \"(maddy) Turn on debug log messages\")\n\tdirectLog = flag.Bool(\"test.directlog\", false, \"(maddy) Log to stderr instead of test log\")\n)\n\nfunc Logger(t *testing.T, name string) *log.Logger {\n\tif *directLog {\n\t\treturn &log.Logger{\n\t\t\tParent: &log.DefaultLogger, // silence \"no parent\" warning\n\t\t\tOut:    log.WriterOutput(os.Stderr, true),\n\t\t\tName:   name,\n\t\t\tDebug:  *debugLog,\n\t\t}\n\t}\n\n\treturn &log.Logger{\n\t\tParent: &log.DefaultLogger,\n\t\tOut: log.FuncOutput(func(_ time.Time, debug bool, str string) {\n\t\t\tt.Helper()\n\t\t\tstr = strings.TrimSuffix(str, \"\\n\")\n\t\t\tif debug {\n\t\t\t\tstr = \"[debug] \" + str\n\t\t\t}\n\t\t\tt.Log(str)\n\t\t}, func() error {\n\t\t\treturn nil\n\t\t}),\n\t\tName:  name,\n\t\tDebug: *debugLog,\n\t}\n}\n"
  },
  {
    "path": "internal/testutils/modifier.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage testutils\n\nimport (\n\t\"context\"\n\n\t\"github.com/emersion/go-message/textproto\"\n\t\"github.com/foxcpp/maddy/framework/buffer\"\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n)\n\ntype Modifier struct {\n\tInstName string\n\n\tInitErr     error\n\tMailFromErr error\n\tRcptToErr   error\n\tBodyErr     error\n\n\tMailFrom map[string]string\n\tRcptTo   map[string][]string\n\tAddHdr   textproto.Header\n\n\tUnclosedStates int\n}\n\nfunc (m Modifier) Configure([]string, *config.Map) error {\n\treturn nil\n}\n\nfunc (m Modifier) Name() string {\n\treturn \"test_modifier\"\n}\n\nfunc (m Modifier) InstanceName() string {\n\treturn m.InstName\n}\n\ntype modifierState struct {\n\tm *Modifier\n}\n\nfunc (m Modifier) ModStateForMsg(ctx context.Context, msgMeta *module.MsgMetadata) (module.ModifierState, error) {\n\tif m.InitErr != nil {\n\t\treturn nil, m.InitErr\n\t}\n\n\tm.UnclosedStates++\n\treturn modifierState{&m}, nil\n}\n\nfunc (ms modifierState) RewriteSender(ctx context.Context, mailFrom string) (string, error) {\n\tif ms.m.MailFromErr != nil {\n\t\treturn \"\", ms.m.MailFromErr\n\t}\n\tif ms.m.MailFrom == nil {\n\t\treturn mailFrom, nil\n\t}\n\n\tnewMailFrom, ok := ms.m.MailFrom[mailFrom]\n\tif ok {\n\t\treturn newMailFrom, nil\n\t}\n\treturn mailFrom, nil\n}\n\nfunc (ms modifierState) RewriteRcpt(ctx context.Context, rcptTo string) ([]string, error) {\n\tif ms.m.RcptToErr != nil {\n\t\treturn []string{\"\"}, ms.m.RcptToErr\n\t}\n\n\tif ms.m.RcptTo == nil {\n\t\treturn []string{rcptTo}, nil\n\t}\n\n\tnewRcptTo, ok := ms.m.RcptTo[rcptTo]\n\tif ok {\n\t\treturn newRcptTo, nil\n\t}\n\treturn []string{rcptTo}, nil\n}\n\nfunc (ms modifierState) RewriteBody(ctx context.Context, h *textproto.Header, body buffer.Buffer) error {\n\tif ms.m.BodyErr != nil {\n\t\treturn ms.m.BodyErr\n\t}\n\n\tfor field := ms.m.AddHdr.Fields(); field.Next(); {\n\t\th.Add(field.Key(), field.Value())\n\t}\n\treturn nil\n}\n\nfunc (ms modifierState) Close() error {\n\tms.m.UnclosedStates--\n\treturn nil\n}\n\nfunc init() {\n\tmodules.Register(\"test_modifier\", func(_ *container.C, _, _ string) (module.Module, error) {\n\t\treturn &Modifier{}, nil\n\t})\n}\n"
  },
  {
    "path": "internal/testutils/multitable.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage testutils\n\nimport \"context\"\n\ntype MultiTable struct {\n\tM   map[string][]string\n\tErr error\n}\n\nfunc (m MultiTable) LookupMulti(_ context.Context, a string) ([]string, error) {\n\tb, ok := m.M[a]\n\tif ok {\n\t\treturn b, m.Err\n\t} else {\n\t\treturn []string{}, m.Err\n\t}\n}\n"
  },
  {
    "path": "internal/testutils/smtp_server.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage testutils\n\nimport (\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"reflect\"\n\t\"sort\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/emersion/go-sasl\"\n\t\"github.com/emersion/go-smtp\"\n\t\"github.com/foxcpp/maddy/framework/exterrors\"\n\t\"github.com/stretchr/testify/require\"\n)\n\ntype SMTPMessage struct {\n\tFrom     string\n\tOpts     smtp.MailOptions\n\tTo       []string\n\tData     []byte\n\tConn     *smtp.Conn\n\tAuthUser string\n\tAuthPass string\n}\n\ntype SMTPBackend struct {\n\tMessages        []*SMTPMessage\n\tMailFromCounter int\n\tSessionCounter  int\n\tSourceEndpoints map[string]struct{}\n\n\tAuthErr     error\n\tMailErr     error\n\tRcptErr     map[string]error\n\tDataErr     error\n\tLMTPDataErr []error\n\n\tActiveSessionsCounter atomic.Int32\n}\n\nfunc (be *SMTPBackend) NewSession(conn *smtp.Conn) (smtp.Session, error) {\n\tbe.SessionCounter++\n\tbe.ActiveSessionsCounter.Add(1)\n\tif be.SourceEndpoints == nil {\n\t\tbe.SourceEndpoints = make(map[string]struct{})\n\t}\n\tbe.SourceEndpoints[conn.Conn().RemoteAddr().String()] = struct{}{}\n\treturn &session{\n\t\tbackend: be,\n\t\tconn:    conn,\n\t}, nil\n}\n\nfunc (be *SMTPBackend) ConnectionCount() int {\n\treturn int(be.ActiveSessionsCounter.Load())\n}\n\nfunc (be *SMTPBackend) CheckMsg(t *testing.T, indx int, from string, rcptTo []string) {\n\tt.Helper()\n\n\tif len(be.Messages) <= indx {\n\t\tt.Errorf(\"Expected at least %d messages in mailbox, got %d\", indx+1, len(be.Messages))\n\t\treturn\n\t}\n\n\tmsg := be.Messages[indx]\n\tif msg.From != from {\n\t\tt.Errorf(\"Wrong MAIL FROM: %v\", msg.From)\n\t}\n\n\tsort.Strings(msg.To)\n\tsort.Strings(rcptTo)\n\n\tif !reflect.DeepEqual(msg.To, rcptTo) {\n\t\tt.Errorf(\"Wrong RCPT TO: %v\", msg.To)\n\t}\n\tif string(msg.Data) != DeliveryData {\n\t\tt.Errorf(\"Wrong DATA payload: %v (%v)\", string(msg.Data), msg.Data)\n\t}\n}\n\ntype session struct {\n\tbackend  *SMTPBackend\n\tuser     string\n\tpassword string\n\tconn     *smtp.Conn\n\tmsg      *SMTPMessage\n}\n\nfunc (s *session) AuthMechanisms() []string {\n\treturn []string{sasl.Plain}\n}\n\nfunc (s *session) Auth(mech string) (sasl.Server, error) {\n\tif mech != sasl.Plain {\n\t\treturn nil, fmt.Errorf(\"mechanisms other than plain are unsupported\")\n\t}\n\treturn sasl.NewPlainServer(func(identity, username, password string) error {\n\t\tif s.backend.AuthErr != nil {\n\t\t\treturn s.backend.AuthErr\n\t\t}\n\t\ts.user = username\n\t\ts.password = password\n\t\treturn nil\n\t}), nil\n}\n\nfunc (s *session) Reset() {\n\ts.msg = &SMTPMessage{}\n}\n\nfunc (s *session) Logout() error {\n\ts.backend.ActiveSessionsCounter.Add(-1)\n\treturn nil\n}\n\nfunc (s *session) Mail(from string, opts *smtp.MailOptions) error {\n\ts.backend.MailFromCounter++\n\n\tif s.backend.MailErr != nil {\n\t\treturn s.backend.MailErr\n\t}\n\n\ts.Reset()\n\ts.msg.From = from\n\ts.msg.Opts = *opts\n\treturn nil\n}\n\nfunc (s *session) Rcpt(to string, _ *smtp.RcptOptions) error {\n\tif err := s.backend.RcptErr[to]; err != nil {\n\t\treturn err\n\t}\n\n\ts.msg.To = append(s.msg.To, to)\n\treturn nil\n}\n\nfunc (s *session) Data(r io.Reader) error {\n\tif s.backend.DataErr != nil {\n\t\treturn s.backend.DataErr\n\t}\n\n\tb, err := io.ReadAll(r)\n\tif err != nil {\n\t\treturn err\n\t}\n\ts.msg.Data = b\n\ts.msg.Conn = s.conn\n\ts.msg.AuthUser = s.user\n\ts.msg.AuthPass = s.password\n\ts.backend.Messages = append(s.backend.Messages, s.msg)\n\treturn nil\n}\n\nfunc (s *session) LMTPData(r io.Reader, status smtp.StatusCollector) error {\n\tif s.backend.DataErr != nil {\n\t\treturn s.backend.DataErr\n\t}\n\n\tb, err := io.ReadAll(r)\n\tif err != nil {\n\t\treturn err\n\t}\n\ts.msg.Data = b\n\ts.msg.Conn = s.conn\n\ts.msg.AuthUser = s.user\n\ts.msg.AuthPass = s.password\n\ts.backend.Messages = append(s.backend.Messages, s.msg)\n\n\tfor i, rcpt := range s.msg.To {\n\t\tstatus.SetStatus(rcpt, s.backend.LMTPDataErr[i])\n\t}\n\n\treturn nil\n}\n\ntype SMTPServerConfigureFunc func(*smtp.Server)\n\nfunc SMTPServer(t *testing.T, addr string, fn ...SMTPServerConfigureFunc) (*SMTPBackend, *smtp.Server) {\n\tt.Helper()\n\n\tl, err := net.Listen(\"tcp\", addr)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tbe := new(SMTPBackend)\n\ts := smtp.NewServer(be)\n\ts.Domain = \"localhost\"\n\ts.AllowInsecureAuth = true\n\tfor _, f := range fn {\n\t\tf(s)\n\t}\n\n\tgo func() {\n\t\tif err := s.Serve(l); err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\t}()\n\n\t// Dial it once it make sure Server completes its initialization before\n\t// we try to use it. Notably, if test fails before connecting to the server,\n\t// it will call Server.Close which will call Server.listener.Close with a\n\t// nil Server.listener (Serve sets it to a non-nil value, so it is racy and\n\t// happens only sometimes).\n\ttestConn, err := net.Dial(\"tcp\", addr)\n\trequire.NoError(t, err)\n\trequire.NoError(t, testConn.Close())\n\n\treturn be, s\n}\n\n// RSA 1024, valid for *.example.invalid, 127.0.0.1, 127.0.0.2,, 127.0.0.3\n// until Nov 18 17:13:45 2029 GMT.\nconst testServerCert = `-----BEGIN CERTIFICATE-----\nMIICDzCCAXigAwIBAgIRAJ1x+qCW7L+Hs6sRU8BHmWkwDQYJKoZIhvcNAQELBQAw\nEjEQMA4GA1UEChMHQWNtZSBDbzAeFw0xOTExMTgxNzEzNDVaFw0yOTExMTUxNzEz\nNDVaMBIxEDAOBgNVBAoTB0FjbWUgQ28wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ\nAoGBAPINKMyuu3AvzndLDS2/BroA+DRUcAhWPBxMxG1b1BkkHisAZWteKajKmwdO\nO13N8HHBRPPOD56AAPLZGNxYLHn6nel7AiH8k40/xC5tDOthqA82+00fwJHDFCnW\noDLOLcO17HulPvfCSWfefc+uee4kajPa+47hutqZH2bGMTXhAgMBAAGjZTBjMA4G\nA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAMBgNVHRMBAf8EAjAA\nMC4GA1UdEQQnMCWCESouZXhhbXBsZS5pbnZhbGlkhwR/AAABhwR/AAAChwR/AAAD\nMA0GCSqGSIb3DQEBCwUAA4GBAGRn3C2NbwR4cyQmTRm5jcaqi1kAYyEu6U8Q9PJW\nQ15BXMKUTx2lw//QScK9MH2JpKxDuzWDSvaxZMnTxgri2uiplqpe8ydsWj6Wl0q9\n2XMGJ9LIxTZk5+cyZP2uOolvmSP/q8VFTyk9Udl6KUZPQyoiiDq4rBFUIxUyb+bX\npHkR\n-----END CERTIFICATE-----`\n\nconst testServerKey = `-----BEGIN PRIVATE KEY-----\nMIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBAPINKMyuu3AvzndL\nDS2/BroA+DRUcAhWPBxMxG1b1BkkHisAZWteKajKmwdOO13N8HHBRPPOD56AAPLZ\nGNxYLHn6nel7AiH8k40/xC5tDOthqA82+00fwJHDFCnWoDLOLcO17HulPvfCSWfe\nfc+uee4kajPa+47hutqZH2bGMTXhAgMBAAECgYEAgPjSDH3uEdDnSlkLJJzskJ+D\noR58s3R/gvTElSCg2uSLzo3ffF4oBHAwOqxMpabdvz8j5mSdne7Gkp9qx72TtEG2\nwt6uX1tZhm2UTAkInH8IQDthj98P8vAWQsS6HHEIMErsrW2CyUrAt/+o1BRg/hWW\nzixA3CLTthhZTJkaUCECQQD5EM16UcTAKfhr3IZppgq+ZsAOMkeCl3XVV9gHo32i\nDL6UFAb27BAYyjfcZB1fPou4RszX0Ryu9yU0P5qm6N47AkEA+MpdAPkaPziY0ok4\ne9Tcee6P0mIR+/AHk9GliVX2P74DDoOHyMXOSRBwdb+z2tYjrdjkNEL1Txe+sHny\nk/EukwJBAOBqlmqPwNNRPeiaRHZvSSD0XjqsbSirJl48D4gadPoNt66fOQNGAt8D\nXj/z6U9HgQdiq/IOFmVEhT5FzSh1jL8CQQD3Myth8iGQO84tM0c6U3CWfuHMqsEv\n0XnV+HNAmHdLMqOa4joi1dh4ZKs5dDdi828UJ/PnsbhI1FEWzLSpJvWdAkAkVWqf\nAC/TvWvEZLA6Z5CllyNzZJ7XvtIaNOosxHDolyZ1HMWMlfEb2K2ZXWLy5foKPeoY\nXi3olS9rB0J+Rvjz\n-----END PRIVATE KEY-----`\n\n// SMTPServerSTARTTLS starts a server listening on the specified addr with the\n// STARTTLS extension supported.\n//\n// Returned *tls.Config is for the client and is set to trust the server\n// certificate.\nfunc SMTPServerSTARTTLS(t *testing.T, addr string, fn ...SMTPServerConfigureFunc) (*tls.Config, *SMTPBackend, *smtp.Server) {\n\tt.Helper()\n\n\tcert, err := tls.X509KeyPair([]byte(testServerCert), []byte(testServerKey))\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tl, err := net.Listen(\"tcp\", addr)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tbe := new(SMTPBackend)\n\ts := smtp.NewServer(be)\n\ts.Domain = \"localhost\"\n\ts.AllowInsecureAuth = true\n\ts.TLSConfig = &tls.Config{\n\t\tCertificates: []tls.Certificate{cert},\n\t}\n\tfor _, f := range fn {\n\t\tf(s)\n\t}\n\n\tpool := x509.NewCertPool()\n\tpool.AppendCertsFromPEM([]byte(testServerCert))\n\n\tclientCfg := &tls.Config{\n\t\tServerName: \"127.0.0.1\",\n\t\tTime: func() time.Time {\n\t\t\treturn time.Date(2019, time.November, 18, 17, 59, 41, 0, time.UTC)\n\t\t},\n\t\tRootCAs: pool,\n\t}\n\n\tgo func() {\n\t\tif err := s.Serve(l); err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\t}()\n\n\t// Dial it once it make sure Server completes its initialization before\n\t// we try to use it. Notably, if test fails before connecting to the server,\n\t// it will call Server.Close which will call Server.listener.Close with a\n\t// nil Server.listener (Serve sets it to a non-nil value, so it is racy and\n\t// happens only sometimes).\n\ttestConn, err := net.Dial(\"tcp\", addr)\n\trequire.NoError(t, err)\n\trequire.NoError(t, testConn.Close())\n\n\treturn clientCfg, be, s\n}\n\n// SMTPServerTLS starts a SMTP server listening on the specified addr with\n// Implicit TLS.\nfunc SMTPServerTLS(t *testing.T, addr string, fn ...SMTPServerConfigureFunc) (*tls.Config, *SMTPBackend, *smtp.Server) {\n\tt.Helper()\n\n\tcert, err := tls.X509KeyPair([]byte(testServerCert), []byte(testServerKey))\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tl, err := tls.Listen(\"tcp\", addr, &tls.Config{\n\t\tCertificates: []tls.Certificate{cert},\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tbe := new(SMTPBackend)\n\ts := smtp.NewServer(be)\n\ts.Domain = \"localhost\"\n\tfor _, f := range fn {\n\t\tf(s)\n\t}\n\n\tpool := x509.NewCertPool()\n\tpool.AppendCertsFromPEM([]byte(testServerCert))\n\n\tclientCfg := &tls.Config{\n\t\tServerName: \"127.0.0.1\",\n\t\tTime: func() time.Time {\n\t\t\treturn time.Date(2019, time.November, 18, 17, 59, 41, 0, time.UTC)\n\t\t},\n\t\tRootCAs: pool,\n\t}\n\n\tgo func() {\n\t\tif err := s.Serve(l); err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\t}()\n\n\t// Dial it once it make sure Server completes its initialization before\n\t// we try to use it. Notably, if test fails before connecting to the server,\n\t// it will call Server.Close which will call Server.listener.Close with a\n\t// nil Server.listener (Serve sets it to a non-nil value, so it is racy and\n\t// happens only sometimes).\n\ttestConn, err := net.Dial(\"tcp\", addr)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\trequire.NoError(t, testConn.Close())\n\n\treturn clientCfg, be, s\n}\n\ntype smtpBackendConnCounter interface {\n\tConnectionCount() int\n}\n\nfunc CheckSMTPConnLeak(t *testing.T, srv *smtp.Server) {\n\tt.Helper()\n\n\tccb, ok := srv.Backend.(smtpBackendConnCounter)\n\tif !ok {\n\t\tt.Error(\"CheckSMTPConnLeak used for smtp.Server with backend without ConnectionCount method\")\n\t\treturn\n\t}\n\n\t// Connection closure is handled asynchronously, so before failing\n\t// wait a bit for handleQuit in go-smtp to do its work.\n\tfor i := 0; i < 10; i++ {\n\t\tif ccb.ConnectionCount() == 0 {\n\t\t\treturn\n\t\t}\n\t\ttime.Sleep(100 * time.Millisecond)\n\t}\n\tt.Error(\"Non-closed connections present after test completion\")\n}\n\nfunc WaitForConnsClose(t *testing.T, srv *smtp.Server) {\n\tt.Helper()\n\tCheckSMTPConnLeak(t, srv)\n}\n\n// FailOnConn fails the test if attempt is made to connect the\n// specified endpoint.\nfunc FailOnConn(t *testing.T, addr string) net.Listener {\n\tt.Helper()\n\n\ttarpit, err := net.Listen(\"tcp\", addr)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tgo func() {\n\t\tt.Helper()\n\n\t\t_, err := tarpit.Accept()\n\t\tif err == nil {\n\t\t\tt.Error(\"No connection expected\")\n\t\t}\n\t}()\n\treturn tarpit\n}\n\nfunc CheckSMTPErr(t *testing.T, err error, code int, enchCode exterrors.EnhancedCode, msg string) {\n\tt.Helper()\n\n\tif err == nil {\n\t\tt.Error(\"Expected an error, got none\")\n\t\treturn\n\t}\n\n\tfields := exterrors.Fields(err)\n\tif val, _ := fields[\"smtp_code\"].(int); val != code {\n\t\tt.Errorf(\"Wrong smtp_code: %v\", val)\n\t}\n\tif val, _ := fields[\"smtp_enchcode\"].(exterrors.EnhancedCode); val != enchCode {\n\t\tt.Errorf(\"Wrong smtp_enchcode: %v\", val)\n\t}\n\tif val, _ := fields[\"smtp_msg\"].(string); val != msg {\n\t\tt.Errorf(\"Wrong smtp_msg: %v\", val)\n\t}\n}\n"
  },
  {
    "path": "internal/testutils/table.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage testutils\n\nimport \"context\"\n\ntype Table struct {\n\tM   map[string]string\n\tErr error\n}\n\nfunc (m Table) Lookup(_ context.Context, a string) (string, bool, error) {\n\tb, ok := m.M[a]\n\treturn b, ok, m.Err\n}\n"
  },
  {
    "path": "internal/testutils/target.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage testutils\n\nimport (\n\t\"context\"\n\t\"crypto/sha1\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"io\"\n\t\"reflect\"\n\t\"sort\"\n\t\"testing\"\n\n\t\"github.com/emersion/go-message/textproto\"\n\t\"github.com/emersion/go-smtp\"\n\t\"github.com/foxcpp/maddy/framework/buffer\"\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/exterrors\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n)\n\ntype Msg struct {\n\tMsgMeta  *module.MsgMetadata\n\tMailFrom string\n\tRcptTo   []string\n\tBody     []byte\n\tHeader   textproto.Header\n}\n\ntype Target struct {\n\tMessages        []Msg\n\tDiscardMessages bool\n\n\tStartErr       error\n\tRcptErr        map[string]error\n\tBodyErr        error\n\tPartialBodyErr map[string]error\n\tAbortErr       error\n\tCommitErr      error\n\n\tInstName string\n}\n\n/*\nmodule.Module is implemented with dummy functions for logging done by MsgPipeline code.\n*/\n\nfunc (dt *Target) Configure([]string, *config.Map) error {\n\treturn nil\n}\n\nfunc (dt *Target) InstanceName() string {\n\tif dt.InstName != \"\" {\n\t\treturn dt.InstName\n\t}\n\treturn \"test_instance\"\n}\n\nfunc (dt *Target) Name() string {\n\treturn \"test_target\"\n}\n\ntype testTargetDelivery struct {\n\tmsg Msg\n\ttgt *Target\n}\n\ntype testTargetDeliveryPartial struct {\n\ttestTargetDelivery\n}\n\nfunc (dt *Target) StartDelivery(ctx context.Context, msgMeta *module.MsgMetadata, mailFrom string) (module.Delivery, error) {\n\tif dt.PartialBodyErr != nil {\n\t\treturn &testTargetDeliveryPartial{\n\t\t\ttestTargetDelivery: testTargetDelivery{\n\t\t\t\ttgt: dt,\n\t\t\t\tmsg: Msg{MsgMeta: msgMeta, MailFrom: mailFrom},\n\t\t\t},\n\t\t}, dt.StartErr\n\t}\n\treturn &testTargetDelivery{\n\t\ttgt: dt,\n\t\tmsg: Msg{MsgMeta: msgMeta, MailFrom: mailFrom},\n\t}, dt.StartErr\n}\n\nfunc (dtd *testTargetDelivery) AddRcpt(ctx context.Context, to string, _ smtp.RcptOptions) error {\n\tif dtd.tgt.RcptErr != nil {\n\t\tif err := dtd.tgt.RcptErr[to]; err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tdtd.msg.RcptTo = append(dtd.msg.RcptTo, to)\n\treturn nil\n}\n\nfunc (dtd *testTargetDeliveryPartial) BodyNonAtomic(ctx context.Context, c module.StatusCollector, header textproto.Header, buf buffer.Buffer) {\n\tif dtd.tgt.PartialBodyErr != nil {\n\t\tfor rcpt, err := range dtd.tgt.PartialBodyErr {\n\t\t\tc.SetStatus(rcpt, err)\n\t\t}\n\t\treturn\n\t}\n\n\tdtd.msg.Header = header\n\n\tbody, err := buf.Open()\n\tif err != nil {\n\t\tfor rcpt, err := range dtd.tgt.PartialBodyErr {\n\t\t\tc.SetStatus(rcpt, err)\n\t\t}\n\t\treturn\n\t}\n\tdefer func() {\n\t\tif err := body.Close(); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}()\n\n\tdtd.msg.Body, err = io.ReadAll(body)\n\tif err != nil {\n\t\tfor rcpt, err := range dtd.tgt.PartialBodyErr {\n\t\t\tc.SetStatus(rcpt, err)\n\t\t}\n\t}\n}\n\nfunc (dtd *testTargetDelivery) Body(ctx context.Context, header textproto.Header, buf buffer.Buffer) error {\n\tif dtd.tgt.PartialBodyErr != nil {\n\t\treturn errors.New(\"partial failure occurred, no additional information available\")\n\t}\n\tif dtd.tgt.BodyErr != nil {\n\t\treturn dtd.tgt.BodyErr\n\t}\n\n\tdtd.msg.Header = header\n\n\tbody, err := buf.Open()\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer func() {\n\t\tif err := body.Close(); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}()\n\n\tif dtd.tgt.DiscardMessages {\n\t\t// Don't bother.\n\t\t_, err = io.Copy(io.Discard, body)\n\t\treturn err\n\t}\n\n\tdtd.msg.Body, err = io.ReadAll(body)\n\treturn err\n}\n\nfunc (dtd *testTargetDelivery) Abort(ctx context.Context) error {\n\treturn dtd.tgt.AbortErr\n}\n\nfunc (dtd *testTargetDelivery) Commit(ctx context.Context) error {\n\tif dtd.tgt.CommitErr != nil {\n\t\treturn dtd.tgt.CommitErr\n\t}\n\tif dtd.tgt.DiscardMessages {\n\t\treturn nil\n\t}\n\tdtd.tgt.Messages = append(dtd.tgt.Messages, dtd.msg)\n\treturn nil\n}\n\nfunc DoTestDelivery(t *testing.T, tgt module.DeliveryTarget, from string, to []string) string {\n\tt.Helper()\n\treturn DoTestDeliveryMeta(t, tgt, from, to, &module.MsgMetadata{\n\t\tOriginalFrom: from,\n\t})\n}\n\nfunc DoTestDeliveryMeta(t *testing.T, tgt module.DeliveryTarget, from string, to []string, msgMeta *module.MsgMetadata) string {\n\tt.Helper()\n\n\tid, err := DoTestDeliveryErrMeta(t, tgt, from, to, msgMeta)\n\tif err != nil {\n\t\tt.Fatalf(\"Unexpected error: %v\", err)\n\t}\n\treturn id\n}\n\nfunc DoTestDeliveryNonAtomic(t *testing.T, c module.StatusCollector, tgt module.DeliveryTarget, from string, to []string) string {\n\tt.Helper()\n\n\tIDRaw := sha1.Sum([]byte(t.Name()))\n\tencodedID := hex.EncodeToString(IDRaw[:])\n\n\ttestCtx := context.Background()\n\n\tbody := buffer.MemoryBuffer{Slice: []byte(\"foobar\\r\\n\")}\n\tmsgMeta := module.MsgMetadata{\n\t\tDontTraceSender: true,\n\t\tID:              encodedID,\n\t\tOriginalFrom:    from,\n\t}\n\tt.Log(\"-- tgt.StartDelivery\", from)\n\tdelivery, err := tgt.StartDelivery(testCtx, &msgMeta, from)\n\tif err != nil {\n\t\tt.Log(\"-- ... tgt.StartDelivery\", from, err, exterrors.Fields(err))\n\t\tt.Fatalf(\"Unexpected err: %v %+v\", err, exterrors.Fields(err))\n\t\treturn encodedID\n\t}\n\tfor _, rcpt := range to {\n\t\tt.Log(\"-- delivery.AddRcpt\", rcpt)\n\t\tif err := delivery.AddRcpt(testCtx, rcpt, smtp.RcptOptions{}); err != nil {\n\t\t\tt.Log(\"-- ... delivery.AddRcpt\", rcpt, err, exterrors.Fields(err))\n\t\t\tt.Log(\"-- delivery.Abort\")\n\t\t\tif err := delivery.Abort(testCtx); err != nil {\n\t\t\t\tt.Log(\"-- delivery.Abort:\", err, exterrors.Fields(err))\n\t\t\t}\n\t\t\tt.Fatalf(\"Unexpected err: %v %+v\", err, exterrors.Fields(err))\n\t\t\treturn encodedID\n\t\t}\n\t}\n\tt.Log(\"-- delivery.BodyNonAtomic\")\n\thdr := textproto.Header{}\n\thdr.Add(\"B\", \"2\")\n\thdr.Add(\"A\", \"1\")\n\tdelivery.(module.PartialDelivery).BodyNonAtomic(testCtx, c, hdr, body)\n\tt.Log(\"-- delivery.Commit\")\n\tif err := delivery.Commit(testCtx); err != nil {\n\t\tt.Fatalf(\"Unexpected err: %v %+v\", err, exterrors.Fields(err))\n\t}\n\n\treturn encodedID\n}\n\nconst DeliveryData = \"A: 1\\r\\n\" +\n\t\"B: 2\\r\\n\" +\n\t\"\\r\\n\" +\n\t\"foobar\\r\\n\"\n\nfunc DoTestDeliveryErr(t *testing.T, tgt module.DeliveryTarget, from string, to []string) (string, error) {\n\treturn DoTestDeliveryErrMeta(t, tgt, from, to, &module.MsgMetadata{})\n}\n\nfunc DoTestDeliveryErrMeta(t *testing.T, tgt module.DeliveryTarget, from string, to []string, msgMeta *module.MsgMetadata) (string, error) {\n\tt.Helper()\n\n\tIDRaw := sha1.Sum([]byte(t.Name()))\n\tencodedID := hex.EncodeToString(IDRaw[:])\n\ttestCtx := context.Background()\n\n\tbody := buffer.MemoryBuffer{Slice: []byte(\"foobar\\r\\n\")}\n\tmsgMeta.DontTraceSender = true\n\tmsgMeta.ID = encodedID\n\tt.Log(\"-- tgt.StartDelivery\", from)\n\tdelivery, err := tgt.StartDelivery(testCtx, msgMeta, from)\n\tif err != nil {\n\t\tt.Log(\"-- ... tgt.StartDelivery\", from, err, exterrors.Fields(err))\n\t\treturn encodedID, err\n\t}\n\tfor _, rcpt := range to {\n\t\tt.Log(\"-- delivery.AddRcpt\", rcpt)\n\t\tif err := delivery.AddRcpt(testCtx, rcpt, smtp.RcptOptions{}); err != nil {\n\t\t\tt.Log(\"-- ... delivery.AddRcpt\", rcpt, err, exterrors.Fields(err))\n\t\t\tt.Log(\"-- delivery.Abort\")\n\t\t\tif err := delivery.Abort(testCtx); err != nil {\n\t\t\t\tt.Log(\"-- delivery.Abort:\", err, exterrors.Fields(err))\n\t\t\t}\n\t\t\treturn encodedID, err\n\t\t}\n\t}\n\tt.Log(\"-- delivery.Body\")\n\thdr := textproto.Header{}\n\thdr.Add(\"B\", \"2\")\n\thdr.Add(\"A\", \"1\")\n\tif err := delivery.Body(testCtx, hdr, body); err != nil {\n\t\tt.Log(\"-- ... delivery.Body\", err, exterrors.Fields(err))\n\t\tt.Log(\"-- delivery.Abort\")\n\t\tif err := delivery.Abort(testCtx); err != nil {\n\t\t\tt.Log(\"-- ... delivery.Abort:\", err, exterrors.Fields(err))\n\t\t}\n\t\treturn encodedID, err\n\t}\n\tt.Log(\"-- delivery.Commit\")\n\tif err := delivery.Commit(testCtx); err != nil {\n\t\tt.Log(\"-- ... delivery.Commit\", err, exterrors.Fields(err))\n\t\treturn encodedID, err\n\t}\n\n\treturn encodedID, err\n}\n\nfunc CheckTestMessage(t *testing.T, tgt *Target, indx int, sender string, rcpt []string) {\n\tt.Helper()\n\n\tif len(tgt.Messages) <= indx {\n\t\tt.Errorf(\"wrong amount of messages received, want at least %d, got %d\", indx+1, len(tgt.Messages))\n\t\treturn\n\t}\n\tmsg := tgt.Messages[indx]\n\n\tCheckMsg(t, &msg, sender, rcpt)\n}\n\nfunc CheckMsg(t *testing.T, msg *Msg, sender string, rcpt []string) {\n\tt.Helper()\n\n\tidRaw := sha1.Sum([]byte(t.Name()))\n\tencodedId := hex.EncodeToString(idRaw[:])\n\n\tCheckMsgID(t, msg, sender, rcpt, encodedId)\n}\n\nfunc CheckMsgID(t *testing.T, msg *Msg, sender string, rcpt []string, id string) string {\n\tt.Helper()\n\n\tif msg.MsgMeta.ID != id && id != \"\" {\n\t\tt.Errorf(\"empty or wrong delivery context for passed message? %+v\", msg.MsgMeta)\n\t}\n\tif msg.MailFrom != sender {\n\t\tt.Errorf(\"wrong sender, want %s, got %s\", sender, msg.MailFrom)\n\t}\n\n\tsort.Strings(rcpt)\n\tsort.Strings(msg.RcptTo)\n\tif !reflect.DeepEqual(msg.RcptTo, rcpt) {\n\t\tt.Errorf(\"wrong recipients, want %v, got %v\", rcpt, msg.RcptTo)\n\t}\n\tif string(msg.Body) != \"foobar\\r\\n\" {\n\t\tt.Errorf(\"wrong body, want '%s', got '%s' (%v)\", \"foobar\\r\\n\", string(msg.Body), msg.Body)\n\t}\n\n\treturn msg.MsgMeta.ID\n}\n"
  },
  {
    "path": "internal/tls/acme/acme.go",
    "content": "package acme\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"path/filepath\"\n\n\t\"github.com/caddyserver/certmagic\"\n\t\"github.com/foxcpp/maddy/framework/config\"\n\tmodconfig \"github.com/foxcpp/maddy/framework/config/module\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/hooks\"\n\t\"github.com/foxcpp/maddy/framework/log\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n)\n\nconst modName = \"tls.loader.acme\"\n\ntype Loader struct {\n\tinstName string\n\n\tnames        []string\n\tstore        certmagic.Storage\n\tcache        *certmagic.Cache\n\tcfg          *certmagic.Config\n\tcancelManage context.CancelFunc\n\n\tlog *log.Logger\n}\n\nfunc New(c *container.C, _, instName string) (module.Module, error) {\n\treturn &Loader{\n\t\tinstName: instName,\n\t\tlog:      c.DefaultLogger.Sublogger(modName),\n\t}, nil\n}\n\nfunc (l *Loader) Configure(inlineArgs []string, cfg *config.Map) error {\n\tif len(inlineArgs) != 0 {\n\t\treturn fmt.Errorf(\"%s: no inline args expected\", modName)\n\t}\n\n\tvar (\n\t\thostname       string\n\t\textraNames     []string\n\t\tstorePath      string\n\t\tcaPath         string\n\t\ttestCAPath     string\n\t\temail          string\n\t\tagreed         bool\n\t\tchallenge      string\n\t\toverrideDomain string\n\t\tprovider       certmagic.DNSProvider\n\t)\n\tcfg.Bool(\"debug\", true, false, &l.log.Debug)\n\tcfg.String(\"hostname\", true, true, \"\", &hostname)\n\tcfg.StringList(\"extra_names\", false, false, nil, &extraNames)\n\tcfg.String(\"store_path\", false, false,\n\t\tfilepath.Join(config.StateDirectory, \"acme\"), &storePath)\n\tcfg.String(\"ca\", false, false,\n\t\tcertmagic.LetsEncryptProductionCA, &caPath)\n\tcfg.String(\"test_ca\", false, false,\n\t\tcertmagic.LetsEncryptStagingCA, &testCAPath)\n\tcfg.String(\"email\", false, false,\n\t\t\"\", &email)\n\tcfg.String(\"override_domain\", false, false,\n\t\t\"\", &overrideDomain)\n\tcfg.Bool(\"agreed\", false, false, &agreed)\n\tcfg.Enum(\"challenge\", false, true,\n\t\t[]string{\"dns-01\"}, \"dns-01\", &challenge)\n\tcfg.Custom(\"dns\", false, false, func() (interface{}, error) {\n\t\treturn nil, nil\n\t}, func(m *config.Map, node config.Node) (interface{}, error) {\n\t\tvar p certmagic.DNSProvider\n\t\terr := modconfig.ModuleFromNode(\"libdns\", node.Args, node, m.Globals, &p)\n\t\treturn p, err\n\t}, &provider)\n\tif _, err := cfg.Process(); err != nil {\n\t\treturn err\n\t}\n\n\tcmLog := l.log.Zap()\n\n\tl.store = &certmagic.FileStorage{Path: storePath}\n\tl.cache = certmagic.NewCache(certmagic.CacheOptions{\n\t\tLogger: cmLog,\n\t\tGetConfigForCert: func(c certmagic.Certificate) (*certmagic.Config, error) {\n\t\t\treturn l.cfg, nil\n\t\t},\n\t})\n\n\tl.cfg = certmagic.New(l.cache, certmagic.Config{\n\t\tStorage:           l.store, // not sure if it is necessary to set these twice\n\t\tLogger:            cmLog,\n\t\tDefaultServerName: hostname,\n\t})\n\tissuer := certmagic.NewACMEIssuer(l.cfg, certmagic.ACMEIssuer{\n\t\tLogger: cmLog,\n\t\tCA:     caPath,\n\t\tTestCA: testCAPath,\n\t\tEmail:  email,\n\t\tAgreed: agreed,\n\t})\n\n\tswitch challenge {\n\tcase \"dns-01\":\n\t\tissuer.DisableTLSALPNChallenge = true\n\t\tissuer.DisableHTTPChallenge = true\n\t\tif provider == nil {\n\t\t\treturn fmt.Errorf(\"tls.loader.acme: dns-01 challenge requires a configured DNS provider\")\n\t\t}\n\t\tissuer.DNS01Solver = &certmagic.DNS01Solver{\n\t\t\tDNSManager: certmagic.DNSManager{\n\t\t\t\tDNSProvider:    provider,\n\t\t\t\tOverrideDomain: overrideDomain,\n\t\t\t},\n\t\t}\n\tdefault:\n\t\treturn fmt.Errorf(\"tls.loader.acme: challenge not supported\")\n\t}\n\tl.cfg.Issuers = []certmagic.Issuer{issuer}\n\n\tl.names = append([]string{hostname}, extraNames...)\n\n\treturn nil\n}\n\nfunc (l *Loader) ConfigureTLS(c *tls.Config) error {\n\tc.GetCertificate = l.cfg.GetCertificate\n\treturn nil\n}\n\nfunc (l *Loader) Start() error {\n\tmanageCtx, cancelManage := context.WithCancel(context.Background())\n\terr := l.cfg.ManageAsync(manageCtx, l.names)\n\tif err != nil {\n\t\tcancelManage()\n\t\treturn err\n\t}\n\tl.cancelManage = cancelManage\n\treturn nil\n}\n\nfunc (l *Loader) Stop() error {\n\tl.cancelManage()\n\tl.cache.Stop()\n\treturn nil\n}\n\nfunc (l *Loader) Name() string {\n\treturn modName\n}\n\nfunc (l *Loader) InstanceName() string {\n\treturn l.instName\n}\n\nfunc init() {\n\thooks.AddHook(hooks.EventShutdown, func() {\n\t\tcertmagic.CleanUpOwnLocks(context.TODO(), log.DefaultLogger.Zap())\n\t})\n}\n\nfunc init() {\n\tvar _ module.TLSLoader = &Loader{}\n\tmodules.Register(modName, New)\n}\n"
  },
  {
    "path": "internal/tls/file.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage tls\n\nimport (\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/log\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n)\n\ntype FileLoader struct {\n\tinstName  string\n\tcertPaths []string\n\tkeyPaths  []string\n\tlog       *log.Logger\n\n\tcerts     []tls.Certificate\n\tcertsLock sync.RWMutex\n\n\treloadTick *time.Ticker\n\tstopTick   chan struct{}\n}\n\nfunc NewFileLoader(c *container.C, modName, instName string) (module.Module, error) {\n\treturn &FileLoader{\n\t\tinstName: instName,\n\t\tlog:      c.DefaultLogger.Sublogger(modName),\n\t\tstopTick: make(chan struct{}),\n\t}, nil\n}\n\nfunc (f *FileLoader) Configure(inlineArgs []string, cfg *config.Map) error {\n\tcfg.StringList(\"certs\", false, false, nil, &f.certPaths)\n\tcfg.StringList(\"keys\", false, false, nil, &f.keyPaths)\n\tif _, err := cfg.Process(); err != nil {\n\t\treturn err\n\t}\n\n\tif len(f.certPaths) != len(f.keyPaths) {\n\t\treturn errors.New(\"tls.loader.file: mismatch in certs and keys count\")\n\t}\n\n\tif len(inlineArgs)%2 != 0 {\n\t\treturn errors.New(\"tls.loader.file: odd amount of arguments\")\n\t}\n\tfor i := 0; i < len(inlineArgs); i += 2 {\n\t\tf.certPaths = append(f.certPaths, inlineArgs[i])\n\t\tf.keyPaths = append(f.keyPaths, inlineArgs[i+1])\n\t}\n\n\tfor _, certPath := range f.certPaths {\n\t\tif !filepath.IsAbs(certPath) {\n\t\t\treturn fmt.Errorf(\"tls.loader.file: only absolute paths allowed in certificate paths: sorry :(\")\n\t\t}\n\t}\n\n\tif err := f.loadCerts(); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (f *FileLoader) Start() error {\n\tf.reloadTick = time.NewTicker(time.Minute)\n\tgo f.reloadTicker()\n\treturn nil\n}\n\nfunc (f *FileLoader) Reload() error {\n\tf.log.Println(\"reloading certificates\")\n\treturn f.loadCerts()\n}\n\nfunc (f *FileLoader) Stop() error {\n\tf.reloadTick.Stop()\n\tf.stopTick <- struct{}{}\n\treturn nil\n}\n\nfunc (f *FileLoader) Name() string {\n\treturn \"tls.loader.file\"\n}\n\nfunc (f *FileLoader) InstanceName() string {\n\treturn f.instName\n}\n\nfunc (f *FileLoader) reloadTicker() {\n\tfor {\n\t\tselect {\n\t\tcase <-f.reloadTick.C:\n\t\t\tf.log.Debugln(\"reloading certs\")\n\t\t\tif err := f.loadCerts(); err != nil {\n\t\t\t\tf.log.Error(\"reload failed\", err)\n\t\t\t}\n\t\tcase <-f.stopTick:\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc (f *FileLoader) loadCerts() error {\n\tif len(f.certPaths) != len(f.keyPaths) {\n\t\treturn errors.New(\"mismatch in certs and keys count\")\n\t}\n\n\tif len(f.certPaths) == 0 {\n\t\treturn errors.New(\"tls.loader.file: at least one certificate required\")\n\t}\n\n\tcerts := make([]tls.Certificate, 0, len(f.certPaths))\n\n\tfor i := range f.certPaths {\n\t\tcertPath := f.certPaths[i]\n\t\tkeyPath := f.keyPaths[i]\n\n\t\tcert, err := tls.LoadX509KeyPair(certPath, keyPath)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to load %s and %s: %v\", certPath, keyPath, err)\n\t\t}\n\t\tcerts = append(certs, cert)\n\t}\n\n\tf.certsLock.Lock()\n\tdefer f.certsLock.Unlock()\n\tf.certs = certs\n\n\treturn nil\n}\n\nfunc (f *FileLoader) ConfigureTLS(c *tls.Config) error {\n\t// Loader function replaces only the whole slice.\n\tf.certsLock.RLock()\n\tdefer f.certsLock.RUnlock()\n\n\tc.Certificates = f.certs\n\treturn nil\n}\n\nfunc init() {\n\tvar _ module.TLSLoader = &FileLoader{}\n\tmodules.Register(\"tls.loader.file\", NewFileLoader)\n}\n"
  },
  {
    "path": "internal/tls/self_signed.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage tls\n\nimport (\n\t\"crypto/ecdsa\"\n\t\"crypto/elliptic\"\n\t\"crypto/rand\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"crypto/x509/pkix\"\n\t\"math/big\"\n\t\"net\"\n\t\"time\"\n\n\t\"github.com/foxcpp/maddy/framework/config\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/module\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n)\n\ntype SelfSignedLoader struct {\n\tinstName    string\n\tserverNames []string\n\n\tcert tls.Certificate\n}\n\nfunc NewSelfSignedLoader(_ *container.C, _, instName string) (module.Module, error) {\n\treturn &SelfSignedLoader{\n\t\tinstName: instName,\n\t}, nil\n}\n\nfunc (f *SelfSignedLoader) Configure(inlineArgs []string, cfg *config.Map) error {\n\tf.serverNames = inlineArgs\n\tif _, err := cfg.Process(); err != nil {\n\t\treturn err\n\t}\n\n\tprivKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tnotBefore := time.Now()\n\tnotAfter := notBefore.Add(24 * time.Hour * 7)\n\tserialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)\n\tserialNumber, err := rand.Int(rand.Reader, serialNumberLimit)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcert := &x509.Certificate{\n\t\tSerialNumber: serialNumber,\n\t\tSubject:      pkix.Name{Organization: []string{\"Maddy Self-Signed\"}},\n\t\tNotBefore:    notBefore,\n\t\tNotAfter:     notAfter,\n\t\tKeyUsage:     x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,\n\t\tExtKeyUsage:  []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},\n\t}\n\n\tfor _, name := range f.serverNames {\n\t\tif ip := net.ParseIP(name); ip != nil {\n\t\t\tcert.IPAddresses = append(cert.IPAddresses, ip)\n\t\t} else {\n\t\t\tcert.DNSNames = append(cert.DNSNames, name)\n\t\t}\n\t}\n\tderBytes, err := x509.CreateCertificate(rand.Reader, cert, cert, &privKey.PublicKey, privKey)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tf.cert = tls.Certificate{\n\t\tCertificate: [][]byte{derBytes},\n\t\tPrivateKey:  privKey,\n\t\tLeaf:        cert,\n\t}\n\treturn nil\n}\n\nfunc (f *SelfSignedLoader) Name() string {\n\treturn \"tls.loader.self_signed\"\n}\n\nfunc (f *SelfSignedLoader) InstanceName() string {\n\treturn f.instName\n}\n\nfunc (f *SelfSignedLoader) ConfigureTLS(c *tls.Config) error {\n\tc.Certificates = []tls.Certificate{f.cert}\n\treturn nil\n}\n\nfunc init() {\n\tvar _ module.TLSLoader = &SelfSignedLoader{}\n\tmodules.Register(\"tls.loader.self_signed\", NewSelfSignedLoader)\n}\n"
  },
  {
    "path": "internal/updatepipe/backend.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage updatepipe\n\ntype BackendMode int\n\nconst (\n\t// ModeReplicate configures backend to both send and receive updates over\n\t// the pipe.\n\tModeReplicate BackendMode = iota\n\n\t// ModePush configures backend to send updates over the pipe only.\n\t//\n\t// If EnableUpdatePipe(ModePush) is called for backend, its Updates()\n\t// channel will never receive any updates.\n\tModePush BackendMode = iota\n)\n\n// The Backend interface is implemented by storage backends that support both\n// updates serialization using the internal updatepipe.P implementation.\n// To activate this implementation, EnableUpdatePipe should be called.\ntype Backend interface {\n\t// EnableUpdatePipe enables the internal update pipe implementation.\n\t// The mode argument selects the pipe behavior. EnableUpdatePipe must be\n\t// called before the first call to the Updates() method.\n\t//\n\t// This method is idempotent. All calls after a successful one do nothing.\n\tEnableUpdatePipe(mode BackendMode) error\n}\n"
  },
  {
    "path": "internal/updatepipe/pubsub/pq.go",
    "content": "package pubsub\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"time\"\n\n\t\"github.com/foxcpp/maddy/framework/log\"\n\t\"github.com/lib/pq\"\n)\n\ntype Msg struct {\n\tKey     string\n\tPayload string\n}\n\ntype PqPubSub struct {\n\tNotify chan Msg\n\n\tL      *pq.Listener\n\tsender *sql.DB\n\n\tLog *log.Logger\n}\n\nfunc NewPQ(dsn string) (*PqPubSub, error) {\n\tl := &PqPubSub{\n\t\tLog:    log.DefaultLogger.Sublogger(\"pgpubsub\"),\n\t\tNotify: make(chan Msg),\n\t}\n\tl.L = pq.NewListener(dsn, 10*time.Second, time.Minute, l.eventHandler)\n\tvar err error\n\tl.sender, err = sql.Open(\"postgres\", dsn)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tgo func() {\n\t\tdefer close(l.Notify)\n\t\tfor n := range l.L.Notify {\n\t\t\tif n == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tl.Notify <- Msg{Key: n.Channel, Payload: n.Extra}\n\t\t}\n\t}()\n\n\treturn l, nil\n}\n\nfunc (l *PqPubSub) Close() error {\n\tif err := l.sender.Close(); err != nil {\n\t\tl.Log.Error(\"failed to close sender socket\", err)\n\t}\n\tif err := l.L.Close(); err != nil {\n\t\tl.Log.Error(\"failed to close listener\", err)\n\t}\n\treturn nil\n}\n\nfunc (l *PqPubSub) eventHandler(ev pq.ListenerEventType, err error) {\n\tswitch ev {\n\tcase pq.ListenerEventConnected:\n\t\tl.Log.DebugMsg(\"connected\")\n\tcase pq.ListenerEventReconnected:\n\t\tl.Log.Msg(\"connection reestablished\")\n\tcase pq.ListenerEventConnectionAttemptFailed:\n\t\tl.Log.Error(\"connection attempt failed\", err)\n\tcase pq.ListenerEventDisconnected:\n\t\tl.Log.Msg(\"connection closed\", \"err\", err)\n\t}\n}\n\nfunc (l *PqPubSub) Subscribe(_ context.Context, key string) error {\n\treturn l.L.Listen(key)\n}\n\nfunc (l *PqPubSub) Unsubscribe(_ context.Context, key string) error {\n\treturn l.L.Unlisten(key)\n}\n\nfunc (l *PqPubSub) Publish(key, payload string) error {\n\t_, err := l.sender.Exec(`SELECT pg_notify($1, $2)`, key, payload)\n\treturn err\n}\n\nfunc (l *PqPubSub) Listener() chan Msg {\n\treturn l.Notify\n}\n"
  },
  {
    "path": "internal/updatepipe/pubsub/pubsub.go",
    "content": "package pubsub\n\nimport \"context\"\n\ntype PubSub interface {\n\tSubscribe(ctx context.Context, key string) error\n\tUnsubscribe(ctx context.Context, key string) error\n\tPublish(key, payload string) error\n\tListener() chan Msg\n\tClose() error\n}\n"
  },
  {
    "path": "internal/updatepipe/pubsub_pipe.go",
    "content": "package updatepipe\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"strconv\"\n\n\tmess \"github.com/foxcpp/go-imap-mess\"\n\t\"github.com/foxcpp/maddy/framework/log\"\n\t\"github.com/foxcpp/maddy/internal/updatepipe/pubsub\"\n)\n\ntype PubSubPipe struct {\n\tPubSub pubsub.PubSub\n\tLog    *log.Logger\n}\n\nfunc (p *PubSubPipe) Listen(upds chan<- mess.Update) error {\n\tgo func() {\n\t\tfor m := range p.PubSub.Listener() {\n\t\t\tid, upd, err := parseUpdate(m.Payload)\n\t\t\tif err != nil {\n\t\t\t\tp.Log.Error(\"failed to parse update\", err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif id == p.myID() {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tupds <- *upd\n\t\t}\n\t}()\n\treturn nil\n}\n\nfunc (p *PubSubPipe) InitPush() error {\n\treturn nil\n}\n\nfunc (p *PubSubPipe) myID() string {\n\treturn fmt.Sprintf(\"%d-%p\", os.Getpid(), p)\n}\n\nfunc (p *PubSubPipe) channel(key interface{}) (string, error) {\n\tvar psKey string\n\tswitch k := key.(type) {\n\tcase string:\n\t\tpsKey = k\n\tcase uint64:\n\t\tpsKey = \"__uint64_\" + strconv.FormatUint(k, 10)\n\tdefault:\n\t\treturn \"\", fmt.Errorf(\"updatepipe: key type must be either string or uint64\")\n\t}\n\treturn psKey, nil\n}\n\nfunc (p *PubSubPipe) Subscribe(key interface{}) {\n\tpsKey, err := p.channel(key)\n\tif err != nil {\n\t\tp.Log.Error(\"invalid key passed to Subscribe\", err)\n\t\treturn\n\t}\n\n\tif err := p.PubSub.Subscribe(context.TODO(), psKey); err != nil {\n\t\tp.Log.Error(\"pubsub subscribe failed\", err)\n\t} else {\n\t\tp.Log.DebugMsg(\"subscribed to pubsub\", \"channel\", psKey)\n\t}\n}\n\nfunc (p *PubSubPipe) Unsubscribe(key interface{}) {\n\tpsKey, err := p.channel(key)\n\tif err != nil {\n\t\tp.Log.Error(\"invalid key passed to Unsubscribe\", err)\n\t\treturn\n\t}\n\n\tif err := p.PubSub.Unsubscribe(context.TODO(), psKey); err != nil {\n\t\tp.Log.Error(\"pubsub unsubscribe failed\", err)\n\t} else {\n\t\tp.Log.DebugMsg(\"unsubscribed from pubsub\", \"channel\", psKey)\n\t}\n}\n\nfunc (p *PubSubPipe) Push(upd mess.Update) error {\n\tpsKey, err := p.channel(upd.Key)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tupdBlob, err := formatUpdate(p.myID(), upd)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn p.PubSub.Publish(psKey, updBlob)\n}\n\nfunc (p *PubSubPipe) Close() error {\n\treturn p.PubSub.Close()\n}\n"
  },
  {
    "path": "internal/updatepipe/serialize.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage updatepipe\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\n\tmess \"github.com/foxcpp/go-imap-mess\"\n)\n\nfunc unescapeName(s string) string {\n\treturn strings.ReplaceAll(s, \"\\x10\", \";\")\n}\n\nfunc escapeName(s string) string {\n\treturn strings.ReplaceAll(s, \";\", \"\\x10\")\n}\n\nfunc parseUpdate(s string) (id string, upd *mess.Update, err error) {\n\tparts := strings.SplitN(s, \";\", 2)\n\tif len(parts) != 2 {\n\t\treturn \"\", nil, errors.New(\"updatepipe: mismatched parts count\")\n\t}\n\n\tupd = &mess.Update{}\n\tdec := json.NewDecoder(strings.NewReader(unescapeName(parts[1])))\n\tdec.UseNumber()\n\terr = dec.Decode(upd)\n\tif err != nil {\n\t\treturn \"\", nil, fmt.Errorf(\"parseUpdate: %w\", err)\n\t}\n\n\tif val, ok := upd.Key.(json.Number); ok {\n\t\tupd.Key, _ = strconv.ParseUint(val.String(), 10, 64)\n\t}\n\n\treturn parts[0], upd, nil\n}\n\nfunc formatUpdate(myID string, upd mess.Update) (string, error) {\n\tupdBlob, err := json.Marshal(upd)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"formatUpdate: %w\", err)\n\t}\n\treturn strings.Join([]string{\n\t\tmyID,\n\t\tescapeName(string(updBlob)),\n\t}, \";\") + \"\\n\", nil\n}\n"
  },
  {
    "path": "internal/updatepipe/unix_pipe.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage updatepipe\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"os\"\n\n\tmess \"github.com/foxcpp/go-imap-mess\"\n\t\"github.com/foxcpp/maddy/framework/log\"\n\t\"github.com/foxcpp/maddy/framework/resource/netresource\"\n)\n\n// UnixSockPipe implements the UpdatePipe interface by serializating updates\n// to/from a Unix domain socket. Due to the way Unix sockets work, only one\n// Listen goroutine can be running.\n//\n// The socket is stream-oriented and consists of the following messages:\n//\n//\tSENDER_ID;JSON_SERIALIZED_INTERNAL_OBJECT\\n\n//\n// And SENDER_ID is Process ID and UnixSockPipe address concated as a string.\n// It is used to deduplicate updates sent to Push and recevied via Listen.\n//\n// The SockPath field specifies the socket path to use. The actual socket\n// is initialized on the first call to Listen or (Init)Push.\ntype UnixSockPipe struct {\n\tSockPath string\n\tLog      *log.Logger\n\n\tlistener net.Listener\n\tsender   net.Conn\n}\n\nvar _ P = &UnixSockPipe{}\n\nfunc (usp *UnixSockPipe) myID() string {\n\treturn fmt.Sprintf(\"%d-%p\", os.Getpid(), usp)\n}\n\nfunc (usp *UnixSockPipe) readUpdates(conn net.Conn, updCh chan<- mess.Update) {\n\tscnr := bufio.NewScanner(conn)\n\tfor scnr.Scan() {\n\t\tid, upd, err := parseUpdate(scnr.Text())\n\t\tif err != nil {\n\t\t\tusp.Log.Error(\"malformed update received\", err, \"str\", scnr.Text())\n\t\t}\n\n\t\t// It is our own update, skip.\n\t\tif id == usp.myID() {\n\t\t\tcontinue\n\t\t}\n\n\t\tupdCh <- *upd\n\t}\n}\n\nfunc (usp *UnixSockPipe) Listen(upd chan<- mess.Update) error {\n\tl, err := netresource.Listen(\"unix\", usp.SockPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\tusp.listener = l\n\tgo func() {\n\t\tfor {\n\t\t\tconn, err := l.Accept()\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tgo usp.readUpdates(conn, upd)\n\t\t}\n\t}()\n\treturn nil\n}\n\nfunc (usp *UnixSockPipe) InitPush() error {\n\tsock, err := net.Dial(\"unix\", usp.SockPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tusp.sender = sock\n\treturn nil\n}\n\nfunc (usp *UnixSockPipe) Push(upd mess.Update) error {\n\tif usp.sender == nil {\n\t\tif err := usp.InitPush(); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tupdStr, err := formatUpdate(usp.myID(), upd)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, err = io.WriteString(usp.sender, updStr)\n\treturn err\n}\n\nfunc (usp *UnixSockPipe) Close() error {\n\tif usp.sender != nil {\n\t\tif err := usp.sender.Close(); err != nil {\n\t\t\tusp.Log.Error(\"failed to close sender socket\", err)\n\t\t}\n\t}\n\tif usp.listener != nil {\n\t\tif err := usp.listener.Close(); err != nil {\n\t\t\tusp.Log.Error(\"failed to close listener\", err)\n\t\t}\n\t\tif err := os.Remove(usp.SockPath); err != nil {\n\t\t\tusp.Log.Error(\"failed to remove socket\", err)\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/updatepipe/update_pipe.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\n// Package updatepipe implements utilities for serialization and transport of\n// IMAP update objects between processes and machines.\n//\n// Its main goal is provide maddy command with ability to properly notify the\n// server about changes without relying on it to coordinate access in the\n// first place (so maddy command can work without a running server or with a\n// broken server instance).\n//\n// Additionally, it can be used to transfer IMAP updates between replicated\n// nodes.\npackage updatepipe\n\nimport (\n\tmess \"github.com/foxcpp/go-imap-mess\"\n)\n\n// The P interface represents the handle for a transport medium used for IMAP\n// updates.\ntype P interface {\n\t// Listen starts the \"pull\" goroutine that reads updates from the pipe and\n\t// sends them to the channel.\n\t//\n\t// Usually it is not possible to call Listen multiple times for the same\n\t// pipe.\n\t//\n\t// Updates sent using the same UpdatePipe object using Push are not\n\t// duplicates to the channel passed to Listen.\n\tListen(upds chan<- mess.Update) error\n\n\t// InitPush prepares the UpdatePipe to be used as updates source (Push\n\t// method).\n\t//\n\t// It is called implicitly on the first Push call, but calling it\n\t// explicitly allows to detect initialization errors early.\n\tInitPush() error\n\n\t// Push writes the update to the pipe.\n\t//\n\t// The update will not be duplicated if the UpdatePipe is also listening\n\t// for updates.\n\tPush(upd mess.Update) error\n\n\tClose() error\n}\n"
  },
  {
    "path": "maddy.conf",
    "content": "## Maddy Mail Server - default configuration file (2022-06-18)\n# Suitable for small-scale deployments. Uses its own format for local users DB,\n# should be managed via maddy subcommands.\n#\n# See tutorials at https://maddy.email for guidance on typical\n# configuration changes.\n\n# ----------------------------------------------------------------------------\n# Base variables\n\n$(hostname) = example.org\n$(primary_domain) = example.org\n$(local_domains) = $(primary_domain)\n\ntls file /etc/maddy/certs/$(hostname)/fullchain.pem /etc/maddy/certs/$(hostname)/privkey.pem\n\n# ----------------------------------------------------------------------------\n# Local storage & authentication\n\n# pass_table provides local hashed passwords storage for authentication of\n# users. It can be configured to use any \"table\" module, in default\n# configuration a table in SQLite DB is used.\n# Table can be replaced to use e.g. a file for passwords. Or pass_table module\n# can be replaced altogether to use some external source of credentials (e.g.\n# PAM, /etc/shadow file).\n#\n# If table module supports it (sql_table does) - credentials can be managed\n# using 'maddy creds' command.\n\nauth.pass_table local_authdb {\n    table sql_table {\n        driver sqlite3\n        dsn credentials.db\n        table_name passwords\n    }\n}\n\n# imapsql module stores all indexes and metadata necessary for IMAP using a\n# relational database. It is used by IMAP endpoint for mailbox access and\n# also by SMTP & Submission endpoints for delivery of local messages.\n#\n# IMAP accounts, mailboxes and all message metadata can be inspected using\n# imap-* subcommands of maddy.\n\nstorage.imapsql local_mailboxes {\n    driver sqlite3\n    dsn imapsql.db\n}\n\n# ----------------------------------------------------------------------------\n# SMTP endpoints + message routing\n\nhostname $(hostname)\n\ntable.chain local_rewrites {\n    optional_step regexp \"(.+)\\+(.+)@(.+)\" \"$1@$3\"\n    optional_step static {\n        entry postmaster postmaster@$(primary_domain)\n    }\n    optional_step file /etc/maddy/aliases\n}\n\nmsgpipeline local_routing {\n    # Insert handling for special-purpose local domains here.\n    # e.g.\n    # destination lists.example.org {\n    #     deliver_to lmtp tcp://127.0.0.1:8024\n    # }\n\n    destination postmaster $(local_domains) {\n        modify {\n            replace_rcpt &local_rewrites\n        }\n\n        deliver_to &local_mailboxes\n    }\n\n    default_destination {\n        reject 550 5.1.1 \"User doesn't exist\"\n    }\n}\n\nsmtp tcp://0.0.0.0:25 {\n    limits {\n        # Up to 20 msgs/sec across max. 10 SMTP connections.\n        all rate 20 1s\n        all concurrency 10\n    }\n\n    dmarc yes\n    check {\n        require_mx_record\n        dkim\n        spf\n    }\n\n    source $(local_domains) {\n        reject 501 5.1.8 \"Use Submission for outgoing SMTP\"\n    }\n    default_source {\n        destination postmaster $(local_domains) {\n            deliver_to &local_routing\n        }\n        default_destination {\n            reject 550 5.1.1 \"User doesn't exist\"\n        }\n    }\n}\n\nsubmission tls://0.0.0.0:465 tcp://0.0.0.0:587 {\n    limits {\n        # Up to 50 msgs/sec across any amount of SMTP connections.\n        all rate 50 1s\n    }\n\n    auth &local_authdb\n\n    source $(local_domains) {\n        check {\n            authorize_sender {\n                prepare_email &local_rewrites\n                user_to_email identity\n            }\n        }\n\n        destination postmaster $(local_domains) {\n            deliver_to &local_routing\n        }\n        default_destination {\n            modify {\n                dkim $(primary_domain) $(local_domains) default\n            }\n            deliver_to &remote_queue\n        }\n    }\n    default_source {\n        reject 501 5.1.8 \"Non-local sender domain\"\n    }\n}\n\ntarget.remote outbound_delivery {\n    limits {\n        # Up to 20 msgs/sec across max. 10 SMTP connections\n        # for each recipient domain.\n        destination rate 20 1s\n        destination concurrency 10\n    }\n    mx_auth {\n        dane\n        mtasts {\n            cache fs\n            fs_dir mtasts_cache/\n        }\n        local_policy {\n            min_tls_level encrypted\n            min_mx_level none\n        }\n    }\n}\n\ntarget.queue remote_queue {\n    target &outbound_delivery\n\n    autogenerated_msg_domain $(primary_domain)\n    bounce {\n        destination postmaster $(local_domains) {\n            deliver_to &local_routing\n        }\n        default_destination {\n            reject 550 5.0.0 \"Refusing to send DSNs to non-local addresses\"\n        }\n    }\n}\n\n# ----------------------------------------------------------------------------\n# IMAP endpoints\n\nimap tls://0.0.0.0:993 tcp://0.0.0.0:143 {\n    auth &local_authdb\n    storage &local_mailboxes\n}\n"
  },
  {
    "path": "maddy.conf.docker",
    "content": "## Maddy Mail Server - default configuration file (2022-06-18)\n## This is the copy of maddy.conf with changes necessary to run it in Docker.\n# Suitable for small-scale deployments. Uses its own format for local users DB,\n# should be managed via maddy subcommands.\n#\n# See tutorials at https://maddy.email for guidance on typical\n# configuration changes.\n\n# ----------------------------------------------------------------------------\n# Base variables\n\n$(hostname) = {env:MADDY_HOSTNAME}\n$(primary_domain) = {env:MADDY_DOMAIN}\n$(local_domains) = $(primary_domain)\n\ntls file /data/tls/fullchain.pem /data/tls/privkey.pem\n\n# ----------------------------------------------------------------------------\n# Local storage & authentication\n\n# pass_table provides local hashed passwords storage for authentication of\n# users. It can be configured to use any \"table\" module, in default\n# configuration a table in SQLite DB is used.\n# Table can be replaced to use e.g. a file for passwords. Or pass_table module\n# can be replaced altogether to use some external source of credentials (e.g.\n# PAM, /etc/shadow file).\n#\n# If table module supports it (sql_table does) - credentials can be managed\n# using 'maddy creds' command.\n\nauth.pass_table local_authdb {\n    table sql_table {\n        driver sqlite3\n        dsn credentials.db\n        table_name passwords\n    }\n}\n\n# imapsql module stores all indexes and metadata necessary for IMAP using a\n# relational database. It is used by IMAP endpoint for mailbox access and\n# also by SMTP & Submission endpoints for delivery of local messages.\n#\n# IMAP accounts, mailboxes and all message metadata can be inspected using\n# imap-* subcommands of maddy.\n\nstorage.imapsql local_mailboxes {\n    driver sqlite3\n    dsn imapsql.db\n}\n\n# ----------------------------------------------------------------------------\n# SMTP endpoints + message routing\n\nhostname $(hostname)\n\ntable.chain local_rewrites {\n    optional_step regexp \"(.+)\\+(.+)@(.+)\" \"$1@$3\"\n    optional_step static {\n        entry postmaster postmaster@$(primary_domain)\n    }\n    optional_step file /etc/maddy/aliases\n}\n\nmsgpipeline local_routing {\n    # Insert handling for special-purpose local domains here.\n    # e.g.\n    # destination lists.example.org {\n    #     deliver_to lmtp tcp://127.0.0.1:8024\n    # }\n\n    destination postmaster $(local_domains) {\n        modify {\n            replace_rcpt &local_rewrites\n        }\n\n        deliver_to &local_mailboxes\n    }\n\n    default_destination {\n        reject 550 5.1.1 \"User doesn't exist\"\n    }\n}\n\nsmtp tcp://0.0.0.0:25 {\n    limits {\n        # Up to 20 msgs/sec across max. 10 SMTP connections.\n        all rate 20 1s\n        all concurrency 10\n    }\n\n    dmarc yes\n    check {\n        require_mx_record\n        dkim\n        spf\n    }\n\n    source $(local_domains) {\n        reject 501 5.1.8 \"Use Submission for outgoing SMTP\"\n    }\n    default_source {\n        destination postmaster $(local_domains) {\n            deliver_to &local_routing\n        }\n        default_destination {\n            reject 550 5.1.1 \"User doesn't exist\"\n        }\n    }\n}\n\nsubmission tls://0.0.0.0:465 tcp://0.0.0.0:587 {\n    limits {\n        # Up to 50 msgs/sec across any amount of SMTP connections.\n        all rate 50 1s\n    }\n\n    auth &local_authdb\n\n    source $(local_domains) {\n        check {\n            authorize_sender {\n                prepare_email &local_rewrites\n                user_to_email identity\n            }\n        }\n\n        destination postmaster $(local_domains) {\n            deliver_to &local_routing\n        }\n        default_destination {\n            modify {\n                dkim $(primary_domain) $(local_domains) default\n            }\n            deliver_to &remote_queue\n        }\n    }\n    default_source {\n        reject 501 5.1.8 \"Non-local sender domain\"\n    }\n}\n\ntarget.remote outbound_delivery {\n    limits {\n        # Up to 20 msgs/sec across max. 10 SMTP connections\n        # for each recipient domain.\n        destination rate 20 1s\n        destination concurrency 10\n    }\n    mx_auth {\n        dane\n        mtasts {\n            cache fs\n            fs_dir mtasts_cache/\n        }\n        local_policy {\n            min_tls_level encrypted\n            min_mx_level none\n        }\n    }\n}\n\ntarget.queue remote_queue {\n    target &outbound_delivery\n\n    autogenerated_msg_domain $(primary_domain)\n    bounce {\n        destination postmaster $(local_domains) {\n            deliver_to &local_routing\n        }\n        default_destination {\n            reject 550 5.0.0 \"Refusing to send DSNs to non-local addresses\"\n        }\n    }\n}\n\n# ----------------------------------------------------------------------------\n# IMAP endpoints\n\nimap tls://0.0.0.0:993 tcp://0.0.0.0:143 {\n    auth &local_authdb\n    storage &local_mailboxes\n}\n"
  },
  {
    "path": "maddy.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage maddy\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"runtime/debug\"\n\t\"sync\"\n\n\t\"github.com/caddyserver/certmagic\"\n\tparser \"github.com/foxcpp/maddy/framework/cfgparser\"\n\t\"github.com/foxcpp/maddy/framework/config\"\n\tmodconfig \"github.com/foxcpp/maddy/framework/config/module\"\n\t\"github.com/foxcpp/maddy/framework/config/tls\"\n\t\"github.com/foxcpp/maddy/framework/container\"\n\t\"github.com/foxcpp/maddy/framework/hooks\"\n\t\"github.com/foxcpp/maddy/framework/log\"\n\t\"github.com/foxcpp/maddy/framework/module/modules\"\n\t\"github.com/foxcpp/maddy/framework/resource/netresource\"\n\t\"github.com/foxcpp/maddy/internal/authz\"\n\tmaddycli \"github.com/foxcpp/maddy/internal/cli\"\n\t\"github.com/urfave/cli/v2\"\n\n\t// Import packages for side-effect of module registration.\n\t_ \"github.com/foxcpp/maddy/internal/auth/dovecot_sasl\"\n\t_ \"github.com/foxcpp/maddy/internal/auth/external\"\n\t_ \"github.com/foxcpp/maddy/internal/auth/ldap\"\n\t_ \"github.com/foxcpp/maddy/internal/auth/netauth\"\n\t_ \"github.com/foxcpp/maddy/internal/auth/pam\"\n\t_ \"github.com/foxcpp/maddy/internal/auth/pass_table\"\n\t_ \"github.com/foxcpp/maddy/internal/auth/plain_separate\"\n\t_ \"github.com/foxcpp/maddy/internal/auth/shadow\"\n\t_ \"github.com/foxcpp/maddy/internal/check/authorize_sender\"\n\t_ \"github.com/foxcpp/maddy/internal/check/command\"\n\t_ \"github.com/foxcpp/maddy/internal/check/dkim\"\n\t_ \"github.com/foxcpp/maddy/internal/check/dns\"\n\t_ \"github.com/foxcpp/maddy/internal/check/dnsbl\"\n\t_ \"github.com/foxcpp/maddy/internal/check/milter\"\n\t_ \"github.com/foxcpp/maddy/internal/check/requiretls\"\n\t_ \"github.com/foxcpp/maddy/internal/check/rspamd\"\n\t_ \"github.com/foxcpp/maddy/internal/check/spf\"\n\t_ \"github.com/foxcpp/maddy/internal/endpoint/dovecot_sasld\"\n\t_ \"github.com/foxcpp/maddy/internal/endpoint/imap\"\n\t_ \"github.com/foxcpp/maddy/internal/endpoint/openmetrics\"\n\t_ \"github.com/foxcpp/maddy/internal/endpoint/smtp\"\n\t_ \"github.com/foxcpp/maddy/internal/imap_filter\"\n\t_ \"github.com/foxcpp/maddy/internal/imap_filter/command\"\n\t_ \"github.com/foxcpp/maddy/internal/libdns\"\n\t_ \"github.com/foxcpp/maddy/internal/modify\"\n\t_ \"github.com/foxcpp/maddy/internal/modify/dkim\"\n\t_ \"github.com/foxcpp/maddy/internal/storage/blob/fs\"\n\t_ \"github.com/foxcpp/maddy/internal/storage/blob/s3\"\n\t_ \"github.com/foxcpp/maddy/internal/storage/imapsql\"\n\t_ \"github.com/foxcpp/maddy/internal/table\"\n\t_ \"github.com/foxcpp/maddy/internal/target/queue\"\n\t_ \"github.com/foxcpp/maddy/internal/target/remote\"\n\t_ \"github.com/foxcpp/maddy/internal/target/smtp\"\n\t_ \"github.com/foxcpp/maddy/internal/tls\"\n\t_ \"github.com/foxcpp/maddy/internal/tls/acme\"\n)\n\nvar (\n\tVersion = \"go-build\"\n\n\tenableDebugFlags = false\n)\n\nfunc BuildInfo() string {\n\tversion := Version\n\tif info, ok := debug.ReadBuildInfo(); ok && info.Main.Version != \"(devel)\" {\n\t\tversion = info.Main.Version\n\t}\n\n\treturn fmt.Sprintf(`%s %s/%s %s\n\ndefault config: %s\ndefault state_dir: %s\ndefault runtime_dir: %s`,\n\t\tversion, runtime.GOOS, runtime.GOARCH, runtime.Version(),\n\t\tfilepath.Join(ConfigDirectory, \"maddy.conf\"),\n\t\tDefaultStateDirectory,\n\t\tDefaultRuntimeDirectory)\n}\n\nfunc init() {\n\tmaddycli.AddGlobalFlag(\n\t\t&cli.PathFlag{\n\t\t\tName:    \"config\",\n\t\t\tUsage:   \"Configuration file to use\",\n\t\t\tEnvVars: []string{\"MADDY_CONFIG\"},\n\t\t\tValue:   filepath.Join(ConfigDirectory, \"maddy.conf\"),\n\t\t},\n\t)\n\tmaddycli.AddGlobalFlag(&cli.BoolFlag{\n\t\tName:        \"debug\",\n\t\tUsage:       \"enable debug logging early\",\n\t\tDestination: &log.DefaultLogger.Debug,\n\t})\n\tmaddycli.AddSubcommand(&cli.Command{\n\t\tName:  \"verify-config\",\n\t\tUsage: \"Check configuration file for errors\",\n\t\tFlags: []cli.Flag{\n\t\t\t&cli.BoolFlag{\n\t\t\t\tName:        \"debug\",\n\t\t\t\tUsage:       \"enable debug logging early\",\n\t\t\t\tDestination: &log.DefaultLogger.Debug,\n\t\t\t},\n\t\t},\n\t\tAction: VerifyConfig,\n\t})\n\tmaddycli.AddSubcommand(&cli.Command{\n\t\tName:  \"run\",\n\t\tUsage: \"Start the server\",\n\t\tFlags: []cli.Flag{\n\t\t\t&cli.StringFlag{\n\t\t\t\tName:        \"libexec\",\n\t\t\t\tValue:       DefaultLibexecDirectory,\n\t\t\t\tUsage:       \"path to the libexec directory\",\n\t\t\t\tDestination: &config.LibexecDirectory,\n\t\t\t},\n\t\t\t&cli.StringSliceFlag{\n\t\t\t\tName:  \"log\",\n\t\t\t\tUsage: \"default logging target(s)\",\n\t\t\t\tValue: cli.NewStringSlice(\"stderr\"),\n\t\t\t},\n\t\t\t&cli.BoolFlag{\n\t\t\t\tName:   \"v\",\n\t\t\t\tUsage:  \"print version and build metadata, then exit\",\n\t\t\t\tHidden: true,\n\t\t\t},\n\t\t},\n\t\tAction: Run,\n\t})\n\tmaddycli.AddSubcommand(&cli.Command{\n\t\tName:  \"version\",\n\t\tUsage: \"Print version and build metadata, then exit\",\n\t\tAction: func(c *cli.Context) error {\n\t\t\tfmt.Println(BuildInfo())\n\t\t\treturn nil\n\t\t},\n\t})\n\n\tif enableDebugFlags {\n\t\tmaddycli.AddGlobalFlag(&cli.StringFlag{\n\t\t\tName:  \"debug.pprof\",\n\t\t\tUsage: \"enable live profiler HTTP endpoint and listen on the specified address\",\n\t\t})\n\t\tmaddycli.AddGlobalFlag(&cli.IntFlag{\n\t\t\tName:  \"debug.blockprofrate\",\n\t\t\tUsage: \"set blocking profile rate\",\n\t\t})\n\t\tmaddycli.AddGlobalFlag(&cli.IntFlag{\n\t\t\tName:  \"debug.mutexproffract\",\n\t\t\tUsage: \"set mutex profile fraction\",\n\t\t})\n\t}\n}\n\n// Run is the entry point for all server-running code. It takes care of command line arguments processing,\n// logging initialization, directives setup, configuration reading. After all that, it\n// calls moduleMain to initialize and run modules.\nfunc Run(c *cli.Context) error {\n\tcertmagic.UserAgent = \"maddy/\" + Version\n\n\tif c.NArg() != 0 {\n\t\treturn cli.Exit(fmt.Sprintln(\"usage:\", os.Args[0], \"[options]\"), 2)\n\t}\n\n\tif c.Bool(\"v\") {\n\t\tfmt.Println(\"maddy\", BuildInfo())\n\t\treturn nil\n\t}\n\n\tvar err error\n\tlog.DefaultLogger.Out, err = LogOutputOption(c.StringSlice(\"log\"))\n\tif err != nil {\n\t\tsystemdStatusErr(err)\n\t\treturn cli.Exit(err.Error(), 2)\n\t}\n\n\tinitDebug(c)\n\n\terr = os.Setenv(\"PATH\", config.LibexecDirectory+string(filepath.ListSeparator)+os.Getenv(\"PATH\"))\n\tif err != nil {\n\t\tsystemdStatusErr(err)\n\t\treturn cli.Exit(err.Error(), 1)\n\t}\n\n\thooks.AddHook(hooks.EventLogRotate, reinitLogging)\n\tdefer func(out log.Output) {\n\t\tif err := out.Close(); err != nil {\n\t\t\tlog.Println(\"failed to close default logger output:\", err)\n\t\t}\n\t}(log.DefaultLogger.Out)\n\tdefer hooks.RunHooks(hooks.EventShutdown)\n\n\tdefer func() {\n\t\tif err := netresource.CloseAllListeners(); err != nil {\n\t\t\tlog.DefaultLogger.Error(\"CloseAllListeners failed\", err)\n\t\t}\n\t}()\n\n\tif err := moduleMain(c.Path(\"config\")); err != nil {\n\t\tsystemdStatusErr(err)\n\t\treturn cli.Exit(err.Error(), 1)\n\t}\n\n\treturn nil\n}\n\nfunc VerifyConfig(c *cli.Context) error {\n\terr := os.Setenv(\"PATH\", config.LibexecDirectory+string(filepath.ListSeparator)+os.Getenv(\"PATH\"))\n\tif err != nil {\n\t\treturn cli.Exit(err.Error(), 1)\n\t}\n\n\tif _, err := moduleConfigure(c.Path(\"config\")); err != nil {\n\t\treturn cli.Exit(err.Error(), 2)\n\t}\n\n\t_, _ = fmt.Fprintln(os.Stderr, \"No errors detected\")\n\n\treturn nil\n}\n\nfunc initDebug(c *cli.Context) {\n\tif !enableDebugFlags {\n\t\treturn\n\t}\n\n\tif c.IsSet(\"debug.pprof\") {\n\t\tprofileEndpoint := c.String(\"debug.pprof\")\n\t\tgo func() {\n\t\t\tlog.Println(\"listening on\", \"http://\"+profileEndpoint, \"for profiler requests\")\n\t\t\tlog.Println(\"failed to listen on profiler endpoint:\", http.ListenAndServe(profileEndpoint, nil))\n\t\t}()\n\t}\n\n\t// These values can also be affected by environment so set them\n\t// only if argument is specified.\n\tif c.IsSet(\"debug.mutexproffract\") {\n\t\truntime.SetMutexProfileFraction(c.Int(\"debug.mutexproffract\"))\n\t}\n\tif c.IsSet(\"debug.blockprofrate\") {\n\t\truntime.SetBlockProfileRate(c.Int(\"debug.blockprofrate\"))\n\t}\n}\n\nfunc InitDirs(c *container.C) error {\n\tif c.Config.StateDirectory == \"\" {\n\t\tc.Config.StateDirectory = DefaultStateDirectory\n\t}\n\tif c.Config.RuntimeDirectory == \"\" {\n\t\tc.Config.RuntimeDirectory = DefaultRuntimeDirectory\n\t}\n\tif c.Config.LibexecDirectory == \"\" {\n\t\tc.Config.LibexecDirectory = DefaultLibexecDirectory\n\t}\n\n\tif err := ensureDirectoryWritable(c.Config.StateDirectory); err != nil {\n\t\treturn err\n\t}\n\tif err := ensureDirectoryWritable(c.Config.RuntimeDirectory); err != nil {\n\t\treturn err\n\t}\n\n\t// Make sure all paths we are going to use are absolute\n\t// before we change the working directory.\n\tif !filepath.IsAbs(c.Config.StateDirectory) {\n\t\treturn errors.New(\"statedir should be absolute\")\n\t}\n\tif !filepath.IsAbs(c.Config.RuntimeDirectory) {\n\t\treturn errors.New(\"runtimedir should be absolute\")\n\t}\n\tif !filepath.IsAbs(c.Config.LibexecDirectory) {\n\t\treturn errors.New(\"-libexec should be absolute\")\n\t}\n\n\t// Change the working directory to make all relative paths\n\t// in configuration relative to state directory.\n\tif err := os.Chdir(c.Config.StateDirectory); err != nil {\n\t\tlog.Println(err)\n\t}\n\n\tconfig.StateDirectory = c.Config.StateDirectory\n\tconfig.RuntimeDirectory = c.Config.RuntimeDirectory\n\tconfig.LibexecDirectory = c.Config.LibexecDirectory\n\n\treturn nil\n}\n\nfunc ensureDirectoryWritable(path string) error {\n\tif err := os.MkdirAll(path, 0o700); err != nil {\n\t\treturn err\n\t}\n\n\ttestFile, err := os.Create(filepath.Join(path, \"writeable-test\"))\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err := testFile.Close(); err != nil {\n\t\tlog.Println(\"failed to close writeable-test file:\", err)\n\t}\n\treturn os.RemoveAll(testFile.Name())\n}\n\nfunc ReadGlobals(c *container.C, cfg []config.Node) (map[string]interface{}, []config.Node, error) {\n\tglobals := config.NewMap(nil, config.Node{Children: cfg})\n\tglobals.String(\"state_dir\", false, false, DefaultStateDirectory, &c.Config.StateDirectory)\n\tglobals.String(\"runtime_dir\", false, false, DefaultRuntimeDirectory, &c.Config.RuntimeDirectory)\n\tglobals.String(\"hostname\", false, false, \"\", nil)\n\tglobals.String(\"autogenerated_msg_domain\", false, false, \"\", nil)\n\tglobals.Custom(\"tls\", false, false, nil, tls.TLSDirective, nil)\n\tglobals.Custom(\"tls_client\", false, false, nil, tls.TLSClientBlock, nil)\n\tglobals.Bool(\"storage_perdomain\", false, false, nil)\n\tglobals.Bool(\"auth_perdomain\", false, false, nil)\n\tglobals.StringList(\"auth_domains\", false, false, nil, nil)\n\tglobals.Custom(\"log\", false, false, defaultLogOutput, logOutput, &c.DefaultLogger.Out)\n\tglobals.Bool(\"debug\", false, false, &c.DefaultLogger.Debug)\n\tconfig.EnumMapped(globals, \"auth_map_normalize\", true, false, authz.NormalizeFuncs, authz.NormalizeAuto, nil)\n\tmodconfig.Table(globals, \"auth_map\", true, false, nil, nil)\n\tglobals.AllowUnknown()\n\tunknown, err := globals.Process()\n\treturn globals.Values, unknown, err\n}\n\nfunc ReadConfig(path string) ([]config.Node, error) {\n\tf, err := os.Open(path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer func() {\n\t\tif err := f.Close(); err != nil {\n\t\t\tlog.Println(\"failed to close config file:\", err)\n\t\t}\n\t}()\n\n\treturn parser.Read(f, path)\n}\n\nfunc moduleConfigure(configPath string) (*container.C, error) {\n\tc := container.New()\n\tcontainer.Global = c\n\n\tcfg, err := ReadConfig(configPath)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read config %s: %w\", configPath, err)\n\t}\n\n\tglobals, modBlocks, err := ReadGlobals(c, cfg)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// ReadGlobals will configure c.DefaultLogger.\n\tif c.DefaultLogger.Out != nil {\n\t\tlog.DefaultLogger.Out = c.DefaultLogger.Out\n\t}\n\n\tif err := InitDirs(c); err != nil {\n\t\treturn nil, err\n\t}\n\n\terr = RegisterModules(c, globals, modBlocks)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, inst := range c.Modules.NotInitialized() {\n\t\treturn nil, fmt.Errorf(\"unused configuration block %s (%s)\",\n\t\t\tinst.InstanceName(), inst.Name())\n\t}\n\n\treturn c, nil\n}\n\nfunc moduleStart(c *container.C) error {\n\treturn c.Lifetime.StartAll()\n}\n\nfunc moduleStop(c *container.C, earlyStop bool) error {\n\tif earlyStop {\n\t\tif err := c.Lifetime.EarlyStopAll(); err != nil {\n\t\t\tc.DefaultLogger.Error(\"early stop failed\", err)\n\t\t}\n\t}\n\n\treturn c.Lifetime.StopAll()\n}\n\nfunc moduleMain(configPath string) error {\n\tlog.DefaultLogger.Msg(\"loading configuration...\")\n\n\t// Make path absolute to make sure we can still read it if current directory changes (in moduleConfigure).\n\tconfigPath, err := filepath.Abs(configPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tc, err := moduleConfigure(configPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tc.DefaultLogger.Msg(\"configuration loaded\")\n\n\tif err := moduleStart(c); err != nil {\n\t\treturn err\n\t}\n\tc.DefaultLogger.Msg(\"server started\", \"version\", Version)\n\n\tsystemdStatus(SDReady, \"Configuration running.\")\n\tasyncStopWg := sync.WaitGroup{} // Some containers might still be waiting on moduleStop\n\tfor handleSignals() {\n\t\thooks.RunHooks(hooks.EventReload)\n\n\t\tc = moduleReload(c, configPath, &asyncStopWg)\n\t}\n\n\tc.DefaultLogger.Msg(\"server stopping...\")\n\n\tsystemdStatus(SDStopping, \"Waiting for old configuration to stop...\")\n\tasyncStopWg.Wait()\n\n\tsystemdStatus(SDStopping, \"Waiting for current configuration to stop...\")\n\tif err := moduleStop(c, true); err != nil {\n\t\tc.DefaultLogger.Msg(\"moduleStop failed\", err)\n\t}\n\tc.DefaultLogger.Msg(\"server stopped\")\n\n\tif c.DefaultLogger.Out != nil {\n\t\tif err := c.DefaultLogger.Out.Close(); err != nil {\n\t\t\tlog.DefaultLogger.Error(\"failed to close output logger\", err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc moduleReload(oldContainer *container.C, configPath string, asyncStopWg *sync.WaitGroup) *container.C {\n\toldContainer.DefaultLogger.Msg(\"reloading server...\")\n\tsystemdStatus(SDReloading, \"Reloading server...\")\n\n\trollbackReload := func() {\n\t\t// Restore DefaultLogger config that might be set by moduleConfig\n\t\tlog.DefaultLogger.Out = oldContainer.DefaultLogger.Out\n\t}\n\n\toldContainer.DefaultLogger.Msg(\"loading new configuration...\")\n\tnewContainer, err := moduleConfigure(configPath)\n\tif err != nil {\n\t\trollbackReload()\n\t\toldContainer.DefaultLogger.Error(\"failed to load new configuration\", err)\n\n\t\treturn oldContainer\n\t}\n\n\toldContainer.DefaultLogger.Msg(\"configuration loaded\")\n\trollbackReload = func() {\n\t\t// Restore DefaultLogger config that might be set by moduleConfig\n\t\tlog.DefaultLogger.Out = oldContainer.DefaultLogger.Out\n\t\tcontainer.Global = oldContainer\n\t}\n\n\tif err := oldContainer.Lifetime.EarlyStopAll(); err != nil {\n\t\trollbackReload()\n\t\toldContainer.DefaultLogger.Error(\"failed to early-stop old server\", err)\n\n\t\treturn oldContainer\n\t}\n\n\tnetresource.ResetListenersUsage()\n\toldContainer.DefaultLogger.Msg(\"starting new server\")\n\tif err := moduleStart(newContainer); err != nil {\n\t\trollbackReload()\n\t\toldContainer.DefaultLogger.Error(\"failed to start new server\", err)\n\n\t\treturn oldContainer\n\t}\n\n\tnewContainer.DefaultLogger.Msg(\"new server started\", \"version\", Version)\n\n\tsystemdStatus(SDReloading, \"New configuration running. Waiting for old connections and transactions to finish...\")\n\n\tasyncStopWg.Add(1)\n\tgo func() {\n\t\tdefer asyncStopWg.Done()\n\t\tdefer func() {\n\t\t\tif err := netresource.CloseUnusedListeners(); err != nil {\n\t\t\t\toldContainer.DefaultLogger.Error(\"CloseUnusedListeners failed\", err)\n\t\t\t}\n\t\t}()\n\n\t\toldContainer.DefaultLogger.Msg(\"stopping old server\")\n\t\tif err := moduleStop(oldContainer, false); err != nil {\n\t\t\toldContainer.DefaultLogger.Error(\"moduleStop failed\", err)\n\t\t}\n\t\toldContainer.DefaultLogger.Msg(\"old server stopped\")\n\t\tif err := oldContainer.DefaultLogger.Out.Close(); err != nil {\n\t\t\tnewContainer.DefaultLogger.Error(\"failed to close old server log\", err)\n\t\t}\n\n\t\tsystemdStatus(SDReloading, \"Configuration running.\")\n\t}()\n\n\treturn newContainer\n}\n\nfunc RegisterModules(c *container.C, globals map[string]interface{}, nodes []config.Node) (err error) {\n\tvar endpoints []struct {\n\t\tEndpoint container.LifetimeModule\n\t\tCfg      *config.Map\n\t}\n\n\tfor _, block := range nodes {\n\t\tvar instName string\n\t\tvar modAliases []string\n\t\tif len(block.Args) == 0 {\n\t\t\tinstName = block.Name\n\t\t} else {\n\t\t\tinstName = block.Args[0]\n\t\t\tmodAliases = block.Args[1:]\n\t\t}\n\n\t\tmodName := block.Name\n\n\t\tendpFactory := modules.GetEndpoint(modName)\n\t\tif endpFactory != nil {\n\t\t\tinst, err := endpFactory(c, modName, block.Args)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tendpoints = append(endpoints, struct {\n\t\t\t\tEndpoint container.LifetimeModule\n\t\t\t\tCfg      *config.Map\n\t\t\t}{Endpoint: inst, Cfg: config.NewMap(globals, block)})\n\t\t\tcontinue\n\t\t}\n\n\t\tfactory := modules.Get(modName)\n\t\tif factory == nil {\n\t\t\treturn config.NodeErr(block, \"unknown module or global directive: %s\", modName)\n\t\t}\n\n\t\tinst, err := factory(c, modName, instName)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\terr = c.Modules.Register(inst, func() error {\n\t\t\terr := inst.Configure(nil, config.NewMap(globals, block))\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif lt, ok := inst.(container.LifetimeModule); ok {\n\t\t\t\tc.Lifetime.Add(lt)\n\t\t\t}\n\t\t\treturn nil\n\t\t})\n\t\tif err != nil {\n\t\t\tif errors.Is(err, container.ErrInstanceNameDuplicate) {\n\t\t\t\treturn config.NodeErr(block, \"config block named %s already exists\", inst.InstanceName())\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\n\t\tfor _, alias := range modAliases {\n\t\t\tif err := c.Modules.AddAlias(instName, alias); err != nil {\n\t\t\t\tif errors.Is(err, container.ErrInstanceNameDuplicate) {\n\t\t\t\t\treturn config.NodeErr(block, \"config block named %s already exists\", alias)\n\t\t\t\t}\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\tlog.Debugf(\"%v:%v: register config block %v %v\", block.File, block.Line, instName, modAliases)\n\t}\n\n\tif len(endpoints) == 0 {\n\t\treturn fmt.Errorf(\"at least one endpoint should be configured\")\n\t}\n\n\t// Endpoints are configured directly after registration.\n\tfor _, endp := range endpoints {\n\t\tif err := endp.Endpoint.Configure(nil, endp.Cfg); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tc.Lifetime.Add(endp.Endpoint)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "maddy_debug.go",
    "content": "//go:build debugflags\n// +build debugflags\n\n/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage maddy\n\nimport (\n\t_ \"net/http/pprof\"\n)\n\nfunc init() {\n\tenableDebugFlags = true\n}\n"
  },
  {
    "path": "signal.go",
    "content": "//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris\n// +build darwin dragonfly freebsd linux netbsd openbsd solaris\n\n/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage maddy\n\nimport (\n\t\"os\"\n\t\"os/signal\"\n\t\"syscall\"\n\n\t\"github.com/foxcpp/maddy/framework/hooks\"\n\t\"github.com/foxcpp/maddy/framework/log\"\n)\n\n// handleSignals function creates and listens on OS signals channel.\n//\n// OS-specific signals that correspond to the program termination\n// (SIGTERM, SIGHUP, SIGINT) will cause this function to return.\n//\n// SIGUSR1 will call reinitLogging without returning.\nfunc handleSignals() (reload bool) {\n\tsig := make(chan os.Signal, 5)\n\tsignal.Notify(sig, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGINT, syscall.SIGUSR1, syscall.SIGUSR2)\n\tdefer signal.Stop(sig)\n\n\tfor {\n\t\tswitch s := <-sig; s {\n\t\tcase syscall.SIGUSR1:\n\t\t\tlog.Printf(\"signal received (%s), rotating logs\", s.String())\n\t\t\tsystemdStatus(SDReloading, \"Reopening logs...\")\n\t\t\thooks.RunHooks(hooks.EventLogRotate)\n\t\t\tsystemdStatus(SDReady, \"Listening for incoming connections...\")\n\t\tcase syscall.SIGUSR2:\n\t\t\tlog.Printf(\"signal received (%s), reloading configuration\", s.String())\n\t\t\treturn true\n\t\tdefault:\n\t\t\tgo func() {\n\t\t\t\ts := handleSignals()\n\t\t\t\tlog.Printf(\"forced shutdown due to signal (%v)!\", s)\n\t\t\t\tos.Exit(1)\n\t\t\t}()\n\n\t\t\tlog.Printf(\"signal received (%v), next signal will force immediate shutdown.\", s)\n\t\t\treturn false\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "signal_nonposix.go",
    "content": "//go:build windows || plan9\n// +build windows plan9\n\n/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage maddy\n\nimport (\n\t\"os\"\n\t\"os/signal\"\n\t\"syscall\"\n\n\t\"github.com/foxcpp/maddy/framework/log\"\n)\n\nfunc handleSignals() os.Signal {\n\tsig := make(chan os.Signal, 5)\n\tsignal.Notify(sig, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGINT)\n\n\ts := <-sig\n\tgo func() {\n\t\ts := handleSignals()\n\t\tlog.Printf(\"forced shutdown due to signal (%v)!\", s)\n\t\tos.Exit(1)\n\t}()\n\n\tlog.Printf(\"signal received (%v)\", s)\n\treturn s\n}\n"
  },
  {
    "path": "systemd.go",
    "content": "//go:build linux\n// +build linux\n\n/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage maddy\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"os\"\n\t\"strings\"\n\t\"syscall\"\n\n\t\"github.com/foxcpp/maddy/framework/log\"\n)\n\ntype SDStatus string\n\nconst (\n\tSDReady     = \"READY=1\"\n\tSDReloading = \"RELOADING=1\"\n\tSDStopping  = \"STOPPING=1\"\n)\n\nvar ErrNoNotifySock = errors.New(\"no systemd socket\")\n\nfunc sdNotifySock() (*net.UnixConn, error) {\n\tsockAddr := os.Getenv(\"NOTIFY_SOCKET\")\n\tif sockAddr == \"\" {\n\t\treturn nil, ErrNoNotifySock\n\t}\n\tif strings.HasPrefix(sockAddr, \"@\") {\n\t\tsockAddr = \"\\x00\" + sockAddr[1:]\n\t}\n\n\treturn net.DialUnix(\"unixgram\", nil, &net.UnixAddr{\n\t\tName: sockAddr,\n\t\tNet:  \"unixgram\",\n\t})\n}\n\nfunc setScmPassCred(sock *net.UnixConn) error {\n\tsConn, err := sock.SyscallConn()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar sockoptErr error\n\tif err := sConn.Control(func(fd uintptr) {\n\t\tsockoptErr = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_PASSCRED, 1)\n\t}); err != nil {\n\t\treturn err\n\t}\n\tif sockoptErr != nil {\n\t\treturn sockoptErr\n\t}\n\treturn nil\n}\n\nfunc systemdStatus(status SDStatus, desc string) {\n\tsock, err := sdNotifySock()\n\tif err != nil {\n\t\tif !errors.Is(err, ErrNoNotifySock) {\n\t\t\tlog.Println(\"systemd: failed to acquire notify socket:\", err)\n\t\t}\n\t\treturn\n\t}\n\tdefer func() {\n\t\tif err := sock.Close(); err != nil {\n\t\t\tlog.Println(\"systemd: failed to close systemd socket:\", err)\n\t\t}\n\t}()\n\n\tif err := setScmPassCred(sock); err != nil {\n\t\tlog.Println(\"systemd: failed to set SCM_PASSCRED on the socket:\", err)\n\t}\n\n\tif desc != \"\" {\n\t\tif _, err := io.WriteString(sock, fmt.Sprintf(\"%s\\nSTATUS=%s\", status, desc)); err != nil {\n\t\t\tlog.Println(\"systemd: I/O error:\", err)\n\t\t}\n\t\tlog.Debugf(`systemd: %s STATUS=\"%s\"`, status, desc)\n\t} else {\n\t\tif _, err := io.WriteString(sock, string(status)); err != nil {\n\t\t\tlog.Println(\"systemd: I/O error:\", err)\n\t\t}\n\t\tlog.Debugf(`systemd: %s`, status)\n\t}\n}\n\nfunc systemdStatusErr(reportedErr error) {\n\tsock, err := sdNotifySock()\n\tif err != nil {\n\t\tif !errors.Is(err, ErrNoNotifySock) {\n\t\t\tlog.Println(\"systemd: failed to acquire notify socket:\", err)\n\t\t}\n\t\treturn\n\t}\n\tdefer func() {\n\t\tif err := sock.Close(); err != nil {\n\t\t\tlog.Println(\"systemd: failed to close systemd socket:\", err)\n\t\t}\n\t}()\n\n\tif err := setScmPassCred(sock); err != nil {\n\t\tlog.Println(\"systemd: failed to set SCM_PASSCRED on the socket:\", err)\n\t}\n\n\tvar errno syscall.Errno\n\tif errors.As(reportedErr, &errno) {\n\t\tlog.Debugf(`systemd: ERRNO=%d STATUS=\"%v\"`, errno, reportedErr)\n\t\tif _, err := io.WriteString(sock, fmt.Sprintf(\"ERRNO=%d\\nSTATUS=%v\", errno, reportedErr)); err != nil {\n\t\t\tlog.Println(\"systemd: I/O error:\", err)\n\t\t}\n\t\treturn\n\t}\n\n\tif _, err := io.WriteString(sock, fmt.Sprintf(\"STATUS=%v\\n\", reportedErr)); err != nil {\n\t\tlog.Println(\"systemd: I/O error:\", err)\n\t}\n\tlog.Debugf(`systemd: STATUS=\"%v\"`, reportedErr)\n}\n"
  },
  {
    "path": "systemd_nonlinux.go",
    "content": "//go:build !linux\n// +build !linux\n\n/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage maddy\n\ntype SDStatus string\n\nconst (\n\tSDReady     = \"READY=1\"\n\tSDReloading = \"RELOADING=1\"\n\tSDStopping  = \"STOPPING=1\"\n)\n\nfunc systemdStatus(SDStatus, string) {}\n\nfunc systemdStatusErr(error) {}\n"
  },
  {
    "path": "tests/README.md",
    "content": "# maddy integration testing\n\n## Tests structure\n\nThe test library creates a temporary state and runtime directory, starts the\nserver with the specified configuration file and lets you interact with it\nusing a couple of convenient wrappers.\n\n## Running\n\nTo run tests, use `go test -tags integration` in this directory. Make sure to\nhave a maddy executable in the current working directory.\nUse `-integration.executable` if the executable is named different or is placed\nsomewhere else.\nUse `-integration.coverprofile` to pass `-test.coverprofile\nyour_value.RANDOM` to test executable. See `./build_cover.sh` to build a\nserver executable instrumented with coverage counters.\n"
  },
  {
    "path": "tests/basic_test.go",
    "content": "//go:build integration\n// +build integration\n\n/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage tests_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/foxcpp/maddy/tests\"\n)\n\nfunc TestBasic(tt *testing.T) {\n\ttt.Parallel()\n\n\t// This test is mostly intended to test whether the integration testing\n\t// library is working as expected.\n\n\tt := tests.NewT(tt)\n\tt.DNS(nil)\n\tt.Port(\"smtp\")\n\tt.Config(`\n\t\tsmtp tcp://127.0.0.1:{env:TEST_PORT_smtp} {\n\t\t\thostname mx.maddy.test\n\t\t\ttls off\n\n\t\t\tdeliver_to dummy\n\t\t}`)\n\tt.Run(1)\n\tdefer t.Close()\n\n\tconn := t.Conn(\"smtp\")\n\tdefer conn.Close()\n\tconn.ExpectPattern(\"220 mx.maddy.test *\")\n\tconn.Writeln(\"EHLO localhost\")\n\tconn.ExpectPattern(\"250-*\")\n\tconn.ExpectPattern(\"250-PIPELINING\")\n\tconn.ExpectPattern(\"250-8BITMIME\")\n\tconn.ExpectPattern(\"250-ENHANCEDSTATUSCODES\")\n\tconn.ExpectPattern(\"250-CHUNKING\")\n\tconn.ExpectPattern(\"250-SMTPUTF8\")\n\tconn.ExpectPattern(\"250-SIZE *\")\n\tconn.ExpectPattern(\"250 LIMITS RCPTMAX=20000\")\n\tconn.Writeln(\"QUIT\")\n\tconn.ExpectPattern(\"221 *\")\n}\n"
  },
  {
    "path": "tests/build_cover.sh",
    "content": "#!/bin/sh\nif [ -z \"$GO\" ]; then\n\tGO=go\nfi\nexec $GO test -race -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\n"
  },
  {
    "path": "tests/conn.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage tests\n\nimport (\n\t\"bufio\"\n\t\"crypto/tls\"\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"path\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n)\n\n// Conn is a helper that simplifies testing of text protocol interactions.\ntype Conn struct {\n\tT *T\n\n\tWriteTimeout time.Duration\n\tReadTimeout  time.Duration\n\n\tallowIOErr bool\n\n\tConn    net.Conn\n\tScanner *bufio.Scanner\n}\n\n// AllowIOErr toggles whether I/O errors should be returned to the caller of\n// Conn method or should immedately fail the test.\n//\n// By default (ok = false), the latter happens.\nfunc (c *Conn) AllowIOErr(ok bool) {\n\tc.allowIOErr = ok\n}\n\n// Write writes the string to the connection socket.\nfunc (c *Conn) Write(s string) {\n\tc.T.Helper()\n\n\t// Make sure the test will not accidentally hang waiting for I/O forever if\n\t// the server breaks.\n\tif err := c.Conn.SetWriteDeadline(time.Now().Add(c.WriteTimeout)); err != nil {\n\t\tc.fatal(\"Cannot set write deadline: %v\", err)\n\t}\n\tdefer func() {\n\t\tif err := c.Conn.SetWriteDeadline(time.Time{}); err != nil {\n\t\t\tc.log('-', \"Failed to reset connection deadline: %v\", err)\n\t\t}\n\t}()\n\n\tc.log('>', \"%s\", s)\n\tif _, err := io.WriteString(c.Conn, s); err != nil {\n\t\tc.fatal(\"Unexpected I/O error: %v\", err)\n\t}\n}\n\nfunc (c *Conn) Writeln(s string) {\n\tc.T.Helper()\n\n\tc.Write(s + \"\\r\\n\")\n}\n\nfunc (c *Conn) Readln() (string, error) {\n\tc.T.Helper()\n\n\t// Make sure the test will not accidentally hang waiting for I/O forever if\n\t// the server breaks.\n\tif err := c.Conn.SetReadDeadline(time.Now().Add(c.ReadTimeout)); err != nil {\n\t\tc.fatal(\"Cannot set write deadline: %v\", err)\n\t}\n\tdefer func() {\n\t\tif err := c.Conn.SetReadDeadline(time.Time{}); err != nil {\n\t\t\tc.log('-', \"Failed to reset connection deadline: %v\", err)\n\t\t}\n\t}()\n\n\tif !c.Scanner.Scan() {\n\t\tif err := c.Scanner.Err(); err != nil {\n\t\t\tif c.allowIOErr {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\t\t\tc.fatal(\"Unexpected I/O error: %v\", err)\n\t\t}\n\t\tif c.allowIOErr {\n\t\t\treturn \"\", io.EOF\n\t\t}\n\t\tc.fatal(\"Unexpected EOF\")\n\t}\n\n\tc.log('<', \"%v\", c.Scanner.Text())\n\n\treturn c.Scanner.Text(), nil\n}\n\nfunc (c *Conn) Expect(line string) {\n\tc.T.Helper()\n\n\tactual, err := c.Readln()\n\tif err != nil {\n\t\tc.T.Fatal(\"Unexpected I/O error:\", err)\n\t}\n\n\tif line != actual {\n\t\tc.T.Fatalf(\"Response line not matching the expected one, want %q\", line)\n\t}\n}\n\n// ExpectPattern reads a line from the connection socket and checks whether is\n// matches the supplied shell pattern (as defined by path.Match). The original\n// line is returned.\nfunc (c *Conn) ExpectPattern(pat string) string {\n\tc.T.Helper()\n\n\tline, err := c.Readln()\n\tif err != nil {\n\t\tc.T.Fatal(\"Unexpected I/O error:\", err)\n\t}\n\n\tmatch, err := path.Match(pat, line)\n\tif err != nil {\n\t\tc.T.Fatal(\"Malformed pattern:\", err)\n\t}\n\tif !match {\n\t\tc.T.Fatalf(\"Response line not matching the expected pattern, want %q\", pat)\n\t}\n\n\treturn line\n}\n\nfunc (c *Conn) fatal(f string, args ...interface{}) {\n\tc.T.Helper()\n\tc.log('-', f, args...)\n\tc.T.FailNow()\n}\n\nfunc (c *Conn) log(direction rune, f string, args ...interface{}) {\n\tc.T.Helper()\n\n\tlocal, remote := c.Conn.LocalAddr().(*net.TCPAddr), c.Conn.RemoteAddr().(*net.TCPAddr)\n\tmsg := strings.Builder{}\n\tif local.IP.IsLoopback() {\n\t\tmsg.WriteString(strconv.Itoa(local.Port))\n\t} else {\n\t\tmsg.WriteString(local.String())\n\t}\n\n\tmsg.WriteRune(' ')\n\tmsg.WriteRune(direction)\n\tmsg.WriteRune(' ')\n\n\tif remote.IP.IsLoopback() {\n\t\ttextPort := c.T.portsRev[uint16(remote.Port)]\n\t\tif textPort != \"\" {\n\t\t\tmsg.WriteString(textPort)\n\t\t} else {\n\t\t\tmsg.WriteString(strconv.Itoa(remote.Port))\n\t\t}\n\t} else {\n\t\tmsg.WriteString(local.String())\n\t}\n\n\tif _, ok := c.Conn.(*tls.Conn); ok {\n\t\tmsg.WriteString(\" [tls]\")\n\t}\n\tmsg.WriteString(\": \")\n\tfmt.Fprintf(&msg, f, args...)\n\tc.T.Log(strings.TrimRight(msg.String(), \"\\r\\n \"))\n}\n\nfunc (c *Conn) TLS() {\n\tc.T.Helper()\n\n\ttlsC := tls.Client(c.Conn, &tls.Config{\n\t\tServerName:         \"maddy.test\",\n\t\tInsecureSkipVerify: true,\n\t})\n\tif err := tlsC.Handshake(); err != nil {\n\t\tc.fatal(\"TLS handshake fail: %v\", err)\n\t}\n\n\tc.Conn = tlsC\n\tc.Scanner = bufio.NewScanner(c.Conn)\n}\n\nfunc (c *Conn) SMTPPlainAuth(username, password string, expectOk bool) {\n\tc.T.Helper()\n\n\tresp := append([]byte{0x00}, username...)\n\tresp = append(resp, 0x00)\n\tresp = append(resp, password...)\n\tc.Writeln(\"AUTH PLAIN \" + base64.StdEncoding.EncodeToString(resp))\n\tif expectOk {\n\t\tc.ExpectPattern(\"235 *\")\n\t} else {\n\t\tc.ExpectPattern(\"5*\")\n\t}\n}\n\nfunc (c *Conn) SMTPNegotation(ourName string, requireExts, blacklistExts []string) {\n\tc.T.Helper()\n\n\tneedCapsMap := make(map[string]bool)\n\tblacklistCapsMap := make(map[string]bool)\n\tfor _, ext := range requireExts {\n\t\tneedCapsMap[ext] = false\n\t}\n\tfor _, ext := range blacklistExts {\n\t\tblacklistCapsMap[ext] = false\n\t}\n\n\tc.Writeln(\"EHLO \" + ourName)\n\n\t// Consume the first line from socket, it is either initial greeting (sent\n\t// before we sent EHLO) or the EHLO reply in case of re-negotiation after\n\t// STARTTLS.\n\tl, err := c.Readln()\n\tif err != nil {\n\t\tc.T.Fatal(\"I/O error during SMTP negotiation:\", err)\n\t}\n\tif strings.HasPrefix(l, \"220\") {\n\t\t// That was initial greeting, consume one more line.\n\t\tc.ExpectPattern(\"250-*\")\n\t}\n\n\tvar caps []string\ncapsloop:\n\tfor {\n\t\tline, err := c.Readln()\n\t\tif err != nil {\n\t\t\tc.T.Fatal(\"I/O error during SMTP negotiation:\", err)\n\t\t}\n\n\t\tswitch {\n\t\tcase strings.HasPrefix(line, \"250-\"):\n\t\t\tcaps = append(caps, strings.TrimPrefix(line, \"250-\"))\n\t\tcase strings.HasPrefix(line, \"250 \"):\n\t\t\tcaps = append(caps, strings.TrimPrefix(line, \"250 \"))\n\t\t\tbreak capsloop\n\t\tdefault:\n\t\t\tc.T.Fatal(\"Unexpected reply during SMTP negotiation:\", line)\n\t\t}\n\t}\n\n\tfor _, ext := range caps {\n\t\tneedCapsMap[ext] = true\n\t\tif _, ok := blacklistCapsMap[ext]; ok {\n\t\t\tblacklistCapsMap[ext] = true\n\t\t}\n\t}\n\tfor ext, status := range needCapsMap {\n\t\tif !status {\n\t\t\tc.T.Fatalf(\"Capability %v is missing but required\", ext)\n\t\t}\n\t}\n\tfor ext, status := range blacklistCapsMap {\n\t\tif status {\n\t\t\tc.T.Fatalf(\"Capability %v is present but not allowed\", ext)\n\t\t}\n\t}\n}\n\nfunc (c *Conn) Close() error {\n\treturn c.Conn.Close()\n}\n\nfunc (c *Conn) MustClose() {\n\tc.T.Helper()\n\tif err := c.Close(); err != nil {\n\t\tc.fatal(\"Close: %v\", err)\n\t}\n}\n\nfunc (c *Conn) Rebind(subtest *T) *Conn {\n\tcpy := *c\n\tcpy.T = subtest\n\treturn &cpy\n}\n"
  },
  {
    "path": "tests/cover_test.go",
    "content": "//go:build cover_main\n// +build cover_main\n\n/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage tests\n\n/*\nGo toolchain lacks the ability to instrument arbitrary executables with\ncoverage counters.\n\nThis file wraps the maddy executable into a minimal layer of \"test\" logic to\nmake 'go test' work for it and produce the coverage report.\n\nUse ./build_cover.sh to compile it into ./maddy.cover.\n\nReferences:\nhttps://stackoverflow.com/questions/43381335/how-to-capture-code-coverage-from-a-go-binary\nhttps://blog.cloudflare.com/go-coverage-with-external-tests/\nhttps://github.com/albertito/chasquid/blob/master/coverage_test.go\n*/\n\nimport (\n\t\"flag\"\n\t\"io\"\n\t\"os\"\n\t\"testing\"\n\n\t_ \"github.com/foxcpp/maddy\"                  // To register run command\n\t_ \"github.com/foxcpp/maddy/internal/cli/ctl\" // To register other CLI commands.\n\n\tmaddycli \"github.com/foxcpp/maddy/internal/cli\"\n)\n\nfunc TestMain(m *testing.M) {\n\t// -test.* flags are registered somewhere in init() in \"testing\" (?).\n\n\t// maddy.Run changes the working directory, we need to change it back so\n\t// -test.coverprofile writes out profile in the right location.\n\twd, err := os.Getwd()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// Skip flag parsing and make flag.Parse no-op so when\n\t// m.Run calls it it will not error out on maddy flags.\n\targs := os.Args\n\tos.Args = []string{\"command\"}\n\tflag.Parse()\n\tos.Args = args\n\n\tcode := maddycli.RunWithoutExit()\n\n\tif err := os.Chdir(wd); err != nil {\n\t\tpanic(err)\n\t}\n\n\t// Silence output produced by \"testing\" runtime.\n\tr, w, err := os.Pipe()\n\tif err == nil {\n\t\tos.Stderr = w\n\t\tos.Stdout = w\n\t}\n\tgo func() {\n\t\t_, _ = io.ReadAll(r)\n\t}()\n\n\t// Even though we do not have any tests to run, we need to call out into\n\t// \"testing\" to make it process flags and produce the coverage report.\n\tm.Run()\n\n\t// TestMain doc says we have to exit with a sensible status code on our\n\t// own.\n\tos.Exit(code)\n}\n"
  },
  {
    "path": "tests/dovecot_sasl_test.go",
    "content": "//go:build integration && (darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris)\n\n/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2026 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\n// only posix systems ^\n\npackage tests_test\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"errors\"\n\t\"flag\"\n\t\"os\"\n\t\"os/exec\"\n\t\"os/user\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"syscall\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/foxcpp/maddy/tests\"\n)\n\nvar DovecotExecutable string\n\nfunc init() {\n\tflag.StringVar(&DovecotExecutable, \"integration.dovecot\", \"dovecot\", \"path to dovecot executable for interop tests\")\n}\n\nconst dovecotConf = `\nbase_dir = $ROOT/run/\nstate_dir = $ROOT/lib/\nlog_path = /dev/stderr\nssl = no\n\ndefault_internal_user = $USER\ndefault_internal_group = $GROUP\ndefault_login_user = $USER\n\nauth_failure_delay = 0\n\npassdb {\n\tdriver = passwd-file\n\targs = $ROOT/passwd\n}\n\nuserdb file {\n\tdriver = passwd-file\n\targs = $ROOT/passwd\n}\n\nservice auth {\n\tunix_listener auth {\n\t\tmode = 0666\n\t}\n}\n\n# Dovecot refuses to start without protocols, so we need to give it one.\nprotocols = imap\n\nservice imap-login {\n\tchroot =\n\tinet_listener imap {\n\t\tlisten = 127.0.0.1\n\t\tport = 0\n\t}\n}\n\nservice anvil {\n\tchroot =\n}\n\n# Turn on debugging information, to help troubleshooting issues.\nauth_verbose = yes\nauth_debug = yes\nauth_debug_passwords = yes\nauth_verbose_passwords = yes\nmail_debug = yes\n`\n\nconst dovecotConf24 = `dovecot_config_version = 2.4.0\ndovecot_storage_version = 2.4.0\n\nbase_dir = $ROOT/run/\nstate_dir = $ROOT/lib/\nmail_plugin_dir = $ROOT/lib/\nlogin_plugin_dir = $ROOT/lib/\nlog_path = /dev/stderr\nssl = no\n\ndefault_internal_user = $USER\ndefault_internal_group = $GROUP\ndefault_login_user = $USER\n\nauth_failure_delay = 0\n\npassdb file {\n\tdriver = passwd-file\n\tpasswd_file_path = $ROOT/passwd\n}\n\nuserdb file {\n\tdriver = passwd-file\n\tpasswd_file_path = $ROOT/passwd\n}\n\nservice auth {\n\tunix_listener auth {\n\t\tmode = 0666\n\t}\n}\n\n# Turn on debugging information, to help troubleshooting issues.\nauth_verbose = yes\nauth_debug = yes\nauth_debug_passwords = yes\nauth_verbose_passwords = yes\nmail_debug = yes\n`\n\nconst dovecotPasswd = `tester:{plain}123456:1000:1000::/home/user`\n\nfunc isDovecot24(t *testing.T, dovecotExec string) bool {\n\tcmd := exec.Command(dovecotExec, \"--version\")\n\tvar stdout bytes.Buffer\n\tcmd.Stdout = &stdout\n\tif err := cmd.Run(); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tversion, _, _ := strings.Cut(stdout.String(), \"-\")\n\tt.Log(\"Dovecot version:\", stdout.String())\n\n\tparts := strings.SplitN(version, \".\", 3)\n\n\treturn len(parts) >= 2 && parts[0] == \"2\" && parts[1] >= \"4\"\n}\n\nfunc runDovecot(t *testing.T) (string, *exec.Cmd) {\n\tdovecotExec, err := exec.LookPath(DovecotExecutable)\n\tif err != nil {\n\t\tif errors.Is(err, exec.ErrNotFound) {\n\t\t\tt.Skip(\"No Dovecot executable found, skipping interop. tests\")\n\t\t}\n\t\tt.Fatal(err)\n\t}\n\n\ttempDir := t.TempDir()\n\n\tcurUser, err := user.Current()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tcurGroup, err := user.LookupGroupId(curUser.Gid)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tdovecotConfTemplate := dovecotConf\n\tif isDovecot24(t, dovecotExec) {\n\t\tdovecotConfTemplate = dovecotConf24\n\t}\n\n\tdovecotConf := strings.NewReplacer(\n\t\t\"$ROOT\", tempDir,\n\t\t\"$USER\", curUser.Username,\n\t\t\"$GROUP\", curGroup.Name).Replace(dovecotConfTemplate)\n\terr = os.WriteFile(filepath.Join(tempDir, \"dovecot.conf\"), []byte(dovecotConf), os.ModePerm)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\terr = os.WriteFile(filepath.Join(tempDir, \"passwd\"), []byte(dovecotPasswd), os.ModePerm)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tcmd := exec.Command(dovecotExec, \"-F\", \"-c\", filepath.Join(tempDir, \"dovecot.conf\"))\n\tstderr, err := cmd.StderrPipe()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tif err := cmd.Start(); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tready := make(chan struct{}, 1)\n\n\tgo func() {\n\t\tscnr := bufio.NewScanner(stderr)\n\t\tfor scnr.Scan() {\n\t\t\tline := scnr.Text()\n\n\t\t\t// One of messages printed near completing initialization (Dovecot 2.3 or older)\n\t\t\tif strings.Contains(line, \"starting up for imap\") {\n\t\t\t\ttime.Sleep(500 * time.Millisecond)\n\t\t\t\tready <- struct{}{}\n\t\t\t}\n\t\t\t// Dovecot 2.4+\n\t\t\tif strings.Contains(line, \"starting up without any protocols\") {\n\t\t\t\ttime.Sleep(500 * time.Millisecond)\n\t\t\t\tready <- struct{}{}\n\t\t\t}\n\n\t\t\tt.Log(\"dovecot:\", line)\n\t\t}\n\t\tif err := scnr.Err(); err != nil {\n\t\t\tt.Log(\"stderr I/O error:\", err)\n\t\t}\n\t}()\n\n\t<-ready\n\n\treturn tempDir, cmd\n}\n\nfunc cleanDovecot(t *testing.T, tempDir string, cmd *exec.Cmd) {\n\tcmd.Process.Signal(syscall.SIGTERM)\n\tif !t.Failed() {\n\t\tos.RemoveAll(tempDir)\n\t} else {\n\t\tt.Log(\"Dovecot directory is not deleted:\", tempDir)\n\t}\n}\n\nfunc TestDovecotSASLClient(tt *testing.T) {\n\ttt.Parallel()\n\n\tdovecotDir, cmd := runDovecot(tt)\n\tdefer cleanDovecot(tt, dovecotDir, cmd)\n\n\tt := tests.NewT(tt)\n\tt.DNS(nil)\n\tt.Port(\"smtp\")\n\tt.Env(\"DOVECOT_SASL_SOCK=\" + filepath.Join(dovecotDir, \"run\", \"auth-client\"))\n\tt.Config(`\n\t\tsmtp tcp://127.0.0.1:{env:TEST_PORT_smtp} {\n\t\t\thostname mx.maddy.test\n\t\t\ttls off\n\t\t\tauth dovecot_sasl unix://{env:DOVECOT_SASL_SOCK}\n\t\t\tdeliver_to dummy\n\t\t}`)\n\tt.Run(1)\n\tdefer t.Close()\n\n\tc := t.Conn(\"smtp\")\n\tdefer c.Close()\n\tc.SMTPNegotation(\"localhost\", nil, nil)\n\tc.Writeln(\"AUTH PLAIN AHRlc3QAMTIzNDU2\") // 0x00 test 0x00 123456 (invalid user)\n\tc.ExpectPattern(\"535 *\")\n\tc.Writeln(\"AUTH PLAIN AHRlc3RlcgAxMjM0NQ==\") // 0x00 tester 0x00 12345 (invalid password)\n\tc.ExpectPattern(\"535 *\")\n\tc.Writeln(\"AUTH PLAIN AHRlc3RlcgAxMjM0NTY=\") // 0x00 tester 0x00 123456\n\tc.ExpectPattern(\"235 *\")\n}\n"
  },
  {
    "path": "tests/dovecot_sasld_test.go",
    "content": "//go:build integration\n// +build integration\n\n/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage tests_test\n\nimport (\n\t\"bufio\"\n\t\"errors\"\n\t\"flag\"\n\t\"io/ioutil\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"syscall\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/foxcpp/maddy/tests\"\n)\n\nvar ChasquidExecutable string\n\nfunc init() {\n\tflag.StringVar(&ChasquidExecutable, \"integration.chasquid\", \"chasquid\", \"path to chasquid executable for interop tests\")\n}\n\nconst chasquidConf = `smtp_address: \"127.0.0.2:44444\"\nsubmission_address: \"127.0.0.1:44443\"\n\ndata_dir: \"$ROOT\"\nmail_log_path: \"/dev/null\"\n\ndovecot_auth: true\ndovecot_userdb_path: \"$AUTH_CLIENT\" # needs any Unix socket, not actually used\ndovecot_client_path: \"$AUTH_CLIENT\"\n`\n\n// RSA 1024, valid for *.example.invalid, 127.0.0.1, 127.0.0.2,, 127.0.0.3\n// until Nov 18 17:13:45 2029 GMT.\nconst testServerCert = `-----BEGIN CERTIFICATE-----\nMIICDzCCAXigAwIBAgIRAJ1x+qCW7L+Hs6sRU8BHmWkwDQYJKoZIhvcNAQELBQAw\nEjEQMA4GA1UEChMHQWNtZSBDbzAeFw0xOTExMTgxNzEzNDVaFw0yOTExMTUxNzEz\nNDVaMBIxEDAOBgNVBAoTB0FjbWUgQ28wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ\nAoGBAPINKMyuu3AvzndLDS2/BroA+DRUcAhWPBxMxG1b1BkkHisAZWteKajKmwdO\nO13N8HHBRPPOD56AAPLZGNxYLHn6nel7AiH8k40/xC5tDOthqA82+00fwJHDFCnW\noDLOLcO17HulPvfCSWfefc+uee4kajPa+47hutqZH2bGMTXhAgMBAAGjZTBjMA4G\nA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAMBgNVHRMBAf8EAjAA\nMC4GA1UdEQQnMCWCESouZXhhbXBsZS5pbnZhbGlkhwR/AAABhwR/AAAChwR/AAAD\nMA0GCSqGSIb3DQEBCwUAA4GBAGRn3C2NbwR4cyQmTRm5jcaqi1kAYyEu6U8Q9PJW\nQ15BXMKUTx2lw//QScK9MH2JpKxDuzWDSvaxZMnTxgri2uiplqpe8ydsWj6Wl0q9\n2XMGJ9LIxTZk5+cyZP2uOolvmSP/q8VFTyk9Udl6KUZPQyoiiDq4rBFUIxUyb+bX\npHkR\n-----END CERTIFICATE-----`\n\nconst testServerKey = `-----BEGIN PRIVATE KEY-----\nMIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBAPINKMyuu3AvzndL\nDS2/BroA+DRUcAhWPBxMxG1b1BkkHisAZWteKajKmwdOO13N8HHBRPPOD56AAPLZ\nGNxYLHn6nel7AiH8k40/xC5tDOthqA82+00fwJHDFCnWoDLOLcO17HulPvfCSWfe\nfc+uee4kajPa+47hutqZH2bGMTXhAgMBAAECgYEAgPjSDH3uEdDnSlkLJJzskJ+D\noR58s3R/gvTElSCg2uSLzo3ffF4oBHAwOqxMpabdvz8j5mSdne7Gkp9qx72TtEG2\nwt6uX1tZhm2UTAkInH8IQDthj98P8vAWQsS6HHEIMErsrW2CyUrAt/+o1BRg/hWW\nzixA3CLTthhZTJkaUCECQQD5EM16UcTAKfhr3IZppgq+ZsAOMkeCl3XVV9gHo32i\nDL6UFAb27BAYyjfcZB1fPou4RszX0Ryu9yU0P5qm6N47AkEA+MpdAPkaPziY0ok4\ne9Tcee6P0mIR+/AHk9GliVX2P74DDoOHyMXOSRBwdb+z2tYjrdjkNEL1Txe+sHny\nk/EukwJBAOBqlmqPwNNRPeiaRHZvSSD0XjqsbSirJl48D4gadPoNt66fOQNGAt8D\nXj/z6U9HgQdiq/IOFmVEhT5FzSh1jL8CQQD3Myth8iGQO84tM0c6U3CWfuHMqsEv\n0XnV+HNAmHdLMqOa4joi1dh4ZKs5dDdi828UJ/PnsbhI1FEWzLSpJvWdAkAkVWqf\nAC/TvWvEZLA6Z5CllyNzZJ7XvtIaNOosxHDolyZ1HMWMlfEb2K2ZXWLy5foKPeoY\nXi3olS9rB0J+Rvjz\n-----END PRIVATE KEY-----`\n\nfunc runChasquid(t *testing.T, authClientPath string) (string, *exec.Cmd) {\n\ttempDir := t.TempDir()\n\tt.Log(\"Using\", tempDir)\n\n\tchasquidConf := strings.NewReplacer(\n\t\t\"$ROOT\", tempDir,\n\t\t\"$AUTH_CLIENT\", authClientPath).Replace(chasquidConf)\n\terr := ioutil.WriteFile(filepath.Join(tempDir, \"chasquid.conf\"), []byte(chasquidConf), os.ModePerm)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := os.MkdirAll(filepath.Join(tempDir, \"certs\", \"example.org\"), os.ModePerm); err != nil {\n\t\tt.Fatal(err)\n\t}\n\terr = ioutil.WriteFile(filepath.Join(tempDir, \"certs\", \"example.org\", \"fullchain.pem\"), []byte(testServerCert), os.ModePerm)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\terr = ioutil.WriteFile(filepath.Join(tempDir, \"certs\", \"example.org\", \"privkey.pem\"), []byte(testServerKey), os.ModePerm)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := os.MkdirAll(filepath.Join(tempDir, \"domains\", \"example.org\"), os.ModePerm); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\terr = ioutil.WriteFile(filepath.Join(tempDir, \"chasquid.conf\"), []byte(chasquidConf), os.ModePerm)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tcmd := exec.Command(ChasquidExecutable, \"-v=2\", \"-config_dir\", tempDir)\n\tt.Log(\"Launching\", cmd.String())\n\tstderr, err := cmd.StderrPipe()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tif err := cmd.Start(); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tready := make(chan struct{}, 1)\n\n\tgo func() {\n\t\tscnr := bufio.NewScanner(stderr)\n\t\tfor scnr.Scan() {\n\t\t\tline := scnr.Text()\n\n\t\t\t// One of messages printed near completing initialization.\n\t\t\tif strings.Contains(line, \"Loading certificates\") {\n\t\t\t\ttime.Sleep(1 * time.Second)\n\t\t\t\tready <- struct{}{}\n\t\t\t}\n\n\t\t\tt.Log(\"chasquid:\", line)\n\t\t}\n\t\tif err := scnr.Err(); err != nil {\n\t\t\tt.Log(\"stderr I/O error:\", err)\n\t\t}\n\t}()\n\n\t<-ready\n\n\treturn tempDir, cmd\n}\n\nfunc cleanChasquid(t *testing.T, tempDir string, cmd *exec.Cmd) {\n\tcmd.Process.Signal(syscall.SIGTERM)\n\tos.RemoveAll(tempDir)\n}\n\nfunc TestSASLServerWithChasquid(tt *testing.T) {\n\ttt.Parallel()\n\n\t_, err := exec.LookPath(ChasquidExecutable)\n\tif err != nil {\n\t\tif errors.Is(err, exec.ErrNotFound) {\n\t\t\ttt.Skip(\"No chasquid executable found, skipping interop. tests\")\n\t\t}\n\t\ttt.Fatal(err)\n\t}\n\n\tt := tests.NewT(tt)\n\tt.DNS(nil)\n\tt.Port(\"smtp\")\n\tt.Config(`\n\t\tdovecot_sasld unix://{env:TEST_STATE_DIR}/auth.sock {\n\t\t\tauth pass_table static {\n\t\t\t\t# tester@example.org:123456\n\t\t\t\tentry tester@example.org \"bcrypt:$2a$04$0SaXE/WOMBOfk5jyaKjo.OHkioRljdhMznLnYCg1nrksu9iLd51Ri\"\n\t\t\t}\n\t\t}`)\n\tt.Run(1)\n\tdefer t.Close()\n\n\tchasquidDir, cmd := runChasquid(tt, filepath.Join(t.StateDir(), \"auth.sock\"))\n\tdefer cleanChasquid(tt, chasquidDir, cmd)\n\n\tc := t.ConnUnnamed(44443)\n\tdefer c.Close()\n\tc.SMTPNegotation(\"localhost\", nil, nil)\n\tc.Writeln(\"STARTTLS\")\n\tc.ExpectPattern(\"220 *\")\n\tc.TLS()\n\tc.Writeln(\"AUTH PLAIN AHRlc3RAZXhhbXBsZS5vcmcAMTIzNDU2\") // 0x00 test@example.org 0x00 123456 (invalid user)\n\tc.ExpectPattern(\"535 *\")\n\tc.Writeln(\"AUTH PLAIN AHRlc3RlckBleGFtcGxlLm9yZwAxMjM0NQ==\") // 0x00 tester 0x00 12345 (invalid password)\n\tc.ExpectPattern(\"535 *\")\n\tc.Writeln(\"AUTH PLAIN AHRlc3RlckBleGFtcGxlLm9yZwAxMjM0NTY=\") // 0x00 tester 0x00 123456\n\tc.ExpectPattern(\"235 *\")\n}\n"
  },
  {
    "path": "tests/ghsa_5835_4gvc_32pc_test.go",
    "content": "//go:build integration\n\npackage tests_test\n\nimport (\n\t\"strconv\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/foxcpp/maddy/tests\"\n\t\"github.com/jimlambrt/gldap\"\n\t\"github.com/stretchr/testify/require\"\n)\n\ntype searchEntry struct {\n\tdn      string\n\toptions []gldap.Option\n}\n\ntype MockLDAP struct {\n\tT             *testing.T\n\tSearchEntries map[string][]searchEntry\n\tAllowedBinds  map[string]string\n}\n\nfunc (ml *MockLDAP) HandleBind(w *gldap.ResponseWriter, r *gldap.Request) {\n\tresp := r.NewBindResponse(\n\t\tgldap.WithResponseCode(gldap.ResultInvalidCredentials),\n\t)\n\n\tm, err := r.GetSimpleBindMessage()\n\tif err != nil {\n\t\trequire.NoError(ml.T, w.Write(resp))\n\t\treturn\n\t}\n\n\tpass, ok := ml.AllowedBinds[m.UserName]\n\tif ok && pass == string(m.Password) {\n\t\tresp.SetResultCode(gldap.ResultSuccess)\n\t\trequire.NoError(ml.T, w.Write(resp))\n\t}\n\n\trequire.NoError(ml.T, w.Write(resp))\n}\n\nfunc (ml *MockLDAP) HandleSearch(w *gldap.ResponseWriter, r *gldap.Request) {\n\tresp := r.NewSearchDoneResponse()\n\tm, err := r.GetSearchMessage()\n\tif err != nil {\n\t\tml.T.Logf(\"not a search message: %s\", err)\n\t\trequire.NoError(ml.T, w.Write(resp))\n\t\treturn\n\t}\n\tml.T.Logf(\"search base dn: %s\", m.BaseDN)\n\tml.T.Logf(\"search scope: %d\", m.Scope)\n\tml.T.Logf(\"search filter: %s\", m.Filter)\n\n\tentries := ml.SearchEntries[m.Filter]\n\tfor _, entry := range entries {\n\t\tldapEntry := r.NewSearchResponseEntry(entry.dn, entry.options...)\n\t\trequire.NoError(ml.T, w.Write(ldapEntry))\n\t}\n\n\tresp.SetResultCode(gldap.ResultSuccess)\n\trequire.NoError(ml.T, w.Write(resp))\n}\n\nfunc (ml *MockLDAP) Run(address string) {\n\ts, err := gldap.NewServer()\n\tif err != nil {\n\t\tml.T.Fatalf(\"unable to create server: %s\", err.Error())\n\t}\n\n\t// create a router and add a bind handler\n\tr, err := gldap.NewMux()\n\tif err != nil {\n\t\tml.T.Fatalf(\"unable to create router: %s\", err.Error())\n\t}\n\trequire.NoError(ml.T, r.Bind(ml.HandleBind))\n\trequire.NoError(ml.T, r.Search(ml.HandleSearch))\n\trequire.NoError(ml.T, s.Router(r))\n\tgo func() {\n\t\trequire.NoError(ml.T, s.Run(address))\n\t}()\n\tml.T.Cleanup(func() {\n\t\trequire.NoError(ml.T, s.Stop())\n\t})\n\n\tfor !s.Ready() {\n\t\tml.T.Log(\"Waiting for server to start\")\n\t\ttime.Sleep(100 * time.Millisecond)\n\t}\n}\n\nfunc TestLDAPInjectionFilter(tt *testing.T) {\n\ttt.Parallel()\n\tt := tests.NewT(tt)\n\n\tldapPort := t.Port(\"ldap\")\n\n\tldapSrv := &MockLDAP{\n\t\tT: tt,\n\t\tAllowedBinds: map[string]string{\n\t\t\t\"DC=com,CN=bob\":   \"bob_pass\",\n\t\t\t\"DC=com,CN=alice\": \"alice_pass\",\n\t\t},\n\t\tSearchEntries: map[string][]searchEntry{\n\t\t\t\"(&(objectClass=inetOrgPerson)(uid=alice))\": {\n\t\t\t\t{\n\t\t\t\t\tdn: \"DC=com,CN=alice\",\n\t\t\t\t\toptions: []gldap.Option{\n\t\t\t\t\t\tgldap.WithAttributes(map[string][]string{\n\t\t\t\t\t\t\t\"objectClass\": {\"inetOrgPerson\"},\n\t\t\t\t\t\t\t\"uid\":         {\"alice\"},\n\t\t\t\t\t\t\t\"description\": {\"prefix_test\"},\n\t\t\t\t\t\t}),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\t\"(&(objectClass=inetOrgPerson)(uid=bob))\": {\n\t\t\t\t{\n\t\t\t\t\tdn: \"DC=com,CN=bob\",\n\t\t\t\t\toptions: []gldap.Option{\n\t\t\t\t\t\tgldap.WithAttributes(map[string][]string{\n\t\t\t\t\t\t\t\"objectClass\": {\"inetOrgPerson\"},\n\t\t\t\t\t\t\t\"uid\":         {\"bob\"},\n\t\t\t\t\t\t\t\"description\": {\"prefix_test\"},\n\t\t\t\t\t\t}),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\t\"(&(objectClass=inetOrgPerson)(uid=bob)(description=prefix*))\": {\n\t\t\t\t{\n\t\t\t\t\tdn: \"DC=com,CN=bob\",\n\t\t\t\t\toptions: []gldap.Option{\n\t\t\t\t\t\tgldap.WithAttributes(map[string][]string{\n\t\t\t\t\t\t\t\"objectClass\": {\"inetOrgPerson\"},\n\t\t\t\t\t\t\t\"uid\":         {\"bob\"},\n\t\t\t\t\t\t\t\"description\": {\"prefix_test\"},\n\t\t\t\t\t\t}),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tldapSrv.Run(\":\" + strconv.Itoa(int(ldapPort)))\n\n\tt.Port(\"smtp\")\n\tt.DNS(nil)\n\tt.Config(`\n\t\thostname mx.maddy.test\n\t\ttls off\n\n\t\tauth.ldap ldap_auth {\n\t\t\turls ldap://127.0.0.1:{env:TEST_PORT_ldap}\n\t\t\tbind plain \"DC=com,CN=bob\" \"bob_pass\"\n\t\t\tbase_dn \"DC=com\"\n\t\t\tfilter \"(&(objectClass=inetOrgPerson)(uid={username}))\"\n\t\t}\n\n\t\tsubmission tcp://0.0.0.0:{env:TEST_PORT_smtp} {\n\t\t\tauth &ldap_auth\n\t\t\tdeliver_to dummy\n\t\t}\n\t`)\n\tt.Run(1)\n\tdefer t.Close()\n\n\tsmtpConn := t.Conn(\"smtp\")\n\tdefer smtpConn.MustClose()\n\tsmtpConn.SMTPNegotation(\"clieht.maddy.test\", nil, nil)\n\tsmtpConn.SMTPPlainAuth(\"alice\", \"alice_pass\", true)\n\n\tsmtpConn2 := t.Conn(\"smtp\")\n\tdefer smtpConn2.MustClose()\n\tsmtpConn2.SMTPNegotation(\"clieht.maddy.test\", nil, nil)\n\tsmtpConn2.SMTPPlainAuth(\"bob)(description=prefix*\", \"bob_pass\", false)\n}\n"
  },
  {
    "path": "tests/gocovcat.go",
    "content": "//usr/bin/env go run \"$0\" \"$@\"; exit $?\n//\n// From: https://git.lukeshu.com/go/cmd/gocovcat/\n//\n//go:build ignore\n// +build ignore\n\n// Copyright 2017 Luke Shumaker <lukeshu@parabola.nu>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n// Command gocovcat combines multiple go cover runs, and prints the\n// result on stdout.\npackage main\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"os\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n)\n\nfunc handleErr(err error) {\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"%v\\n\", err)\n\t\tos.Exit(1)\n\t}\n}\n\nfunc main() {\n\tmodeBool := false\n\tblocks := map[string]int{}\n\tfor _, filename := range os.Args[1:] {\n\t\tfile, err := os.Open(filename)\n\t\thandleErr(err)\n\t\tbuf := bufio.NewScanner(file)\n\t\tfor buf.Scan() {\n\t\t\tline := buf.Text()\n\n\t\t\tif strings.HasPrefix(line, \"mode: \") {\n\t\t\t\tm := strings.TrimPrefix(line, \"mode: \")\n\t\t\t\tswitch m {\n\t\t\t\tcase \"set\":\n\t\t\t\t\tmodeBool = true\n\t\t\t\tcase \"count\", \"atomic\":\n\t\t\t\t\t// do nothing\n\t\t\t\tdefault:\n\t\t\t\t\tfmt.Fprintf(os.Stderr, \"Unrecognized mode: %s\\n\", m)\n\t\t\t\t\tos.Exit(1)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tsp := strings.LastIndexByte(line, ' ')\n\t\t\t\tblock := line[:sp]\n\t\t\t\tcntStr := line[sp+1:]\n\t\t\t\tcnt, err := strconv.Atoi(cntStr)\n\t\t\t\thandleErr(err)\n\t\t\t\tblocks[block] += cnt\n\t\t\t}\n\t\t}\n\t\thandleErr(buf.Err())\n\t}\n\tkeys := make([]string, 0, len(blocks))\n\tfor key := range blocks {\n\t\tkeys = append(keys, key)\n\t}\n\tsort.Strings(keys)\n\tmodeStr := \"count\"\n\tif modeBool {\n\t\tmodeStr = \"set\"\n\t}\n\tfmt.Printf(\"mode: %s\\n\", modeStr)\n\tfor _, block := range keys {\n\t\tcnt := blocks[block]\n\t\tif modeBool && cnt > 1 {\n\t\t\tcnt = 1\n\t\t}\n\t\tfmt.Printf(\"%s %d\\n\", block, cnt)\n\t}\n}\n"
  },
  {
    "path": "tests/golangci-noisy.yml",
    "content": "linters:\n  enable:\n  - gosimple\n  - structcheck\n  - varcheck\n  - errcheck\n  - staticcheck\n  - ineffassign\n  - deadcode\n  - typecheck\n  - govet\n  - unused\n  - goimports\n  - prealloc\n  - unconvert\n  - misspell\n  - whitespace\n  - nakedret\n  - dogsled\n  - godox\n  - gocyclo\n  - dupl\n  - unparam\n"
  },
  {
    "path": "tests/imap_test.go",
    "content": "//go:build integration && cgo && !nosqlite3\n// +build integration,cgo,!nosqlite3\n\npackage tests_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/foxcpp/maddy/tests\"\n)\n\nfunc TestIMAPEndpointAuthMap(tt *testing.T) {\n\ttt.Parallel()\n\tt := tests.NewT(tt)\n\n\tt.DNS(nil)\n\tt.Port(\"imap\")\n\tt.Config(`\n\t\tstorage.imapsql test_store {\n\t\t\tdriver sqlite3\n\t\t\tdsn imapsql.db\n\t\t}\n\n\t\timap tcp://127.0.0.1:{env:TEST_PORT_imap} {\n\t\t\ttls off\n\n\t\t\tauth_map email_localpart\n\t\t\tauth pass_table static {\n\t\t\t\tentry \"user\" \"bcrypt:$2a$10$E.AuCH3oYbaRrETXfXwc0.4jRAQBbanpZiCfudsJz9bHzLr/qj6ti\" # password: 123\n\t\t\t}\n\t\t\tstorage &test_store\n\t\t}\n\t`)\n\tt.Run(1)\n\tdefer t.Close()\n\n\timapConn := t.Conn(\"imap\")\n\tdefer imapConn.Close()\n\timapConn.ExpectPattern(`\\* OK *`)\n\timapConn.Writeln(\". LOGIN user@example.org 123\")\n\timapConn.ExpectPattern(\". OK *\")\n\timapConn.Writeln(\". SELECT INBOX\")\n\timapConn.ExpectPattern(`\\* *`)\n\timapConn.ExpectPattern(`\\* *`)\n\timapConn.ExpectPattern(`\\* *`)\n\timapConn.ExpectPattern(`\\* *`)\n\timapConn.ExpectPattern(`\\* *`)\n\timapConn.ExpectPattern(`\\* *`)\n\timapConn.ExpectPattern(`. OK *`)\n}\n\nfunc TestIMAPEndpointStorageMap(tt *testing.T) {\n\ttt.Parallel()\n\tt := tests.NewT(tt)\n\n\tt.DNS(nil)\n\tt.Port(\"imap\")\n\tt.Config(`\n\t\tstorage.imapsql test_store {\n\t\t\tdriver sqlite3\n\t\t\tdsn imapsql.db\n\t\t}\n\n\t\timap tcp://127.0.0.1:{env:TEST_PORT_imap} {\n\t\t\ttls off\n\n\t\t\tstorage_map email_localpart\n\n\t\t\tauth_map email_localpart\n\t\t\tauth pass_table static {\n\t\t\t\tentry \"user\" \"bcrypt:$2a$10$z9SvUwUjkY8wKOWd9IbISeEmbJua2cXRPqw7s2BnLXJuc6pIMPncK\" # password: 123\n\t\t\t}\n\t\t\tstorage &test_store\n\t\t}\n\t`)\n\tt.Run(1)\n\tdefer t.Close()\n\n\timapConn := t.Conn(\"imap\")\n\tdefer imapConn.Close()\n\timapConn.ExpectPattern(`\\* OK *`)\n\timapConn.Writeln(\". LOGIN user@example.org 123\")\n\timapConn.ExpectPattern(\". OK *\")\n\timapConn.Writeln(\". CREATE testbox\")\n\timapConn.ExpectPattern(\". OK *\")\n\n\timapConn2 := t.Conn(\"imap\")\n\tdefer imapConn2.Close()\n\timapConn2.ExpectPattern(`\\* OK *`)\n\timapConn2.Writeln(\". LOGIN user@example.com 123\")\n\timapConn2.ExpectPattern(\". OK *\")\n\timapConn2.Writeln(`. LIST \"\" \"*\"`)\n\timapConn2.Expect(`* LIST (\\HasNoChildren) \".\" INBOX`)\n\timapConn2.Expect(`* LIST (\\HasNoChildren) \".\" \"testbox\"`)\n\timapConn2.ExpectPattern(\". OK *\")\n}\n"
  },
  {
    "path": "tests/imapsql_test.go",
    "content": "//go:build integration\n\n/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage tests_test\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/foxcpp/maddy/tests\"\n)\n\n// Smoke test to ensure message delivery is handled correctly.\n\nfunc TestImapsqlDelivery(tt *testing.T) {\n\ttt.Parallel()\n\tt := tests.NewT(tt)\n\n\tt.DNS(nil)\n\tt.Port(\"imap\")\n\tt.Port(\"smtp\")\n\tt.Config(`\n\t\tstorage.imapsql test_store {\n\t\t\tdriver sqlite3\n\t\t\tdsn imapsql.db\n\t\t}\n\n\t\timap tcp://127.0.0.1:{env:TEST_PORT_imap} {\n\t\t\ttls off\n\n\t\t\tauth dummy\n\t\t\tstorage &test_store\n\t\t}\n\n\t\tsmtp tcp://127.0.0.1:{env:TEST_PORT_smtp} {\n\t\t\thostname maddy.test\n\t\t\ttls off\n\n\t\t\tdeliver_to &test_store\n\t\t}\n\t`)\n\tt.Run(2)\n\tdefer t.Close()\n\n\timapConn := t.Conn(\"imap\")\n\tdefer imapConn.Close()\n\timapConn.ExpectPattern(`\\* OK *`)\n\timapConn.Writeln(\". LOGIN testusr@maddy.test 1234\")\n\timapConn.ExpectPattern(\". OK *\")\n\timapConn.Writeln(\". SELECT INBOX\")\n\timapConn.ExpectPattern(`\\* *`)\n\timapConn.ExpectPattern(`\\* *`)\n\timapConn.ExpectPattern(`\\* *`)\n\timapConn.ExpectPattern(`\\* *`)\n\timapConn.ExpectPattern(`\\* *`)\n\timapConn.ExpectPattern(`\\* *`)\n\timapConn.ExpectPattern(`. OK *`)\n\n\tsmtpConn := t.Conn(\"smtp\")\n\tdefer smtpConn.Close()\n\tsmtpConn.SMTPNegotation(\"localhost\", nil, nil)\n\tsmtpConn.Writeln(\"MAIL FROM:<sender@maddy.test>\")\n\tsmtpConn.ExpectPattern(\"2*\")\n\tsmtpConn.Writeln(\"RCPT TO:<testusr@maddy.test>\")\n\tsmtpConn.ExpectPattern(\"2*\")\n\tsmtpConn.Writeln(\"DATA\")\n\tsmtpConn.ExpectPattern(\"354 *\")\n\tsmtpConn.Writeln(\"From: <sender@maddy.test>\")\n\tsmtpConn.Writeln(\"To: <testusr@maddy.test>\")\n\tsmtpConn.Writeln(\"Subject: Hi!\")\n\tsmtpConn.Writeln(\"\")\n\tsmtpConn.Writeln(\"Hi!\")\n\tsmtpConn.Writeln(\".\")\n\tsmtpConn.ExpectPattern(\"2*\")\n\n\ttime.Sleep(500 * time.Millisecond)\n\n\timapConn.Writeln(\". NOOP\")\n\timapConn.ExpectPattern(`\\* 1 EXISTS`)\n\timapConn.ExpectPattern(`\\* 1 RECENT`)\n\timapConn.ExpectPattern(\". OK *\")\n\n\timapConn.Writeln(\". FETCH 1 (BODY.PEEK[])\")\n\timapConn.ExpectPattern(`\\* 1 FETCH (BODY\\[\\] {*}*`)\n\timapConn.Expect(`Delivered-To: testusr@maddy.test`)\n\timapConn.Expect(`Return-Path: <sender@maddy.test>`)\n\timapConn.ExpectPattern(`Received: from localhost (client.maddy.test \\[` + tests.DefaultSourceIP.String() + `\\]) by maddy.test`)\n\timapConn.ExpectPattern(` (envelope-sender <sender@maddy.test>) with ESMTP id *; *`)\n\timapConn.ExpectPattern(` *`)\n\timapConn.Expect(\"From: <sender@maddy.test>\")\n\timapConn.Expect(\"To: <testusr@maddy.test>\")\n\timapConn.Expect(\"Subject: Hi!\")\n\timapConn.Expect(\"\")\n\timapConn.Expect(\"Hi!\")\n\timapConn.Expect(\")\")\n\timapConn.ExpectPattern(`. OK *`)\n}\n\nfunc TestImapsqlDeliveryMap(tt *testing.T) {\n\ttt.Parallel()\n\tt := tests.NewT(tt)\n\n\tt.DNS(nil)\n\tt.Port(\"imap\")\n\tt.Port(\"smtp\")\n\tt.Config(`\n\t\tstorage.imapsql test_store {\n\t\t\tdelivery_map email_localpart\n\t\t\tauth_normalize precis\n\n\t\t\tdriver sqlite3\n\t\t\tdsn imapsql.db\n\t\t}\n\n\t\timap tcp://127.0.0.1:{env:TEST_PORT_imap} {\n\t\t\ttls off\n\n\t\t\tauth dummy\n\t\t\tstorage &test_store\n\t\t}\n\n\t\tsmtp tcp://127.0.0.1:{env:TEST_PORT_smtp} {\n\t\t\thostname maddy.test\n\t\t\ttls off\n\n\t\t\tdeliver_to &test_store\n\t\t}\n\t`)\n\tt.Run(2)\n\tdefer t.Close()\n\n\timapConn := t.Conn(\"imap\")\n\tdefer imapConn.Close()\n\timapConn.ExpectPattern(`\\* OK *`)\n\timapConn.Writeln(\". LOGIN testusr 1234\")\n\timapConn.ExpectPattern(\". OK *\")\n\timapConn.Writeln(\". SELECT INBOX\")\n\timapConn.ExpectPattern(`\\* *`)\n\timapConn.ExpectPattern(`\\* *`)\n\timapConn.ExpectPattern(`\\* *`)\n\timapConn.ExpectPattern(`\\* *`)\n\timapConn.ExpectPattern(`\\* *`)\n\timapConn.ExpectPattern(`\\* *`)\n\timapConn.ExpectPattern(`. OK *`)\n\n\tsmtpConn := t.Conn(\"smtp\")\n\tdefer smtpConn.Close()\n\tsmtpConn.SMTPNegotation(\"localhost\", nil, nil)\n\tsmtpConn.Writeln(\"MAIL FROM:<sender@maddy.test>\")\n\tsmtpConn.ExpectPattern(\"2*\")\n\tsmtpConn.Writeln(\"RCPT TO:<testusr@maddy.test>\")\n\tsmtpConn.ExpectPattern(\"2*\")\n\tsmtpConn.Writeln(\"DATA\")\n\tsmtpConn.ExpectPattern(\"354 *\")\n\tsmtpConn.Writeln(\"From: <sender@maddy.test>\")\n\tsmtpConn.Writeln(\"To: <testusr@maddy.test>\")\n\tsmtpConn.Writeln(\"Subject: Hi!\")\n\tsmtpConn.Writeln(\"\")\n\tsmtpConn.Writeln(\"Hi!\")\n\tsmtpConn.Writeln(\".\")\n\tsmtpConn.ExpectPattern(\"2*\")\n\n\ttime.Sleep(500 * time.Millisecond)\n\n\timapConn.Writeln(\". NOOP\")\n\timapConn.ExpectPattern(`\\* 1 EXISTS`)\n\timapConn.ExpectPattern(`\\* 1 RECENT`)\n\timapConn.ExpectPattern(\". OK *\")\n}\n\nfunc TestImapsqlAuthMap(tt *testing.T) {\n\ttt.Parallel()\n\tt := tests.NewT(tt)\n\n\tt.DNS(nil)\n\tt.Port(\"imap\")\n\tt.Port(\"smtp\")\n\tt.Config(`\n\t\tstorage.imapsql test_store {\n\t\t\tauth_map regexp \"(.*)\" \"$1@maddy.test\"\n\t\t\tauth_normalize precis\n\n\t\t\tdriver sqlite3\n\t\t\tdsn imapsql.db\n\t\t}\n\n\t\timap tcp://127.0.0.1:{env:TEST_PORT_imap} {\n\t\t\ttls off\n\n\t\t\tauth dummy\n\t\t\tstorage &test_store\n\t\t}\n\n\t\tsmtp tcp://127.0.0.1:{env:TEST_PORT_smtp} {\n\t\t\thostname maddy.test\n\t\t\ttls off\n\n\t\t\tdeliver_to &test_store\n\t\t}\n\t`)\n\tt.Run(2)\n\tdefer t.Close()\n\n\timapConn := t.Conn(\"imap\")\n\tdefer imapConn.Close()\n\timapConn.ExpectPattern(`\\* OK *`)\n\timapConn.Writeln(\". LOGIN testusr 1234\")\n\timapConn.ExpectPattern(\". OK *\")\n\timapConn.Writeln(\". SELECT INBOX\")\n\timapConn.ExpectPattern(`\\* *`)\n\timapConn.ExpectPattern(`\\* *`)\n\timapConn.ExpectPattern(`\\* *`)\n\timapConn.ExpectPattern(`\\* *`)\n\timapConn.ExpectPattern(`\\* *`)\n\timapConn.ExpectPattern(`\\* *`)\n\timapConn.ExpectPattern(`. OK *`)\n\n\tsmtpConn := t.Conn(\"smtp\")\n\tdefer smtpConn.Close()\n\tsmtpConn.SMTPNegotation(\"localhost\", nil, nil)\n\tsmtpConn.Writeln(\"MAIL FROM:<sender@maddy.test>\")\n\tsmtpConn.ExpectPattern(\"2*\")\n\tsmtpConn.Writeln(\"RCPT TO:<testusr@maddy.test>\")\n\tsmtpConn.ExpectPattern(\"2*\")\n\tsmtpConn.Writeln(\"DATA\")\n\tsmtpConn.ExpectPattern(\"354 *\")\n\tsmtpConn.Writeln(\"From: <sender@maddy.test>\")\n\tsmtpConn.Writeln(\"To: <testusr@maddy.test>\")\n\tsmtpConn.Writeln(\"Subject: Hi!\")\n\tsmtpConn.Writeln(\"\")\n\tsmtpConn.Writeln(\"Hi!\")\n\tsmtpConn.Writeln(\".\")\n\tsmtpConn.ExpectPattern(\"2*\")\n\n\ttime.Sleep(500 * time.Millisecond)\n\n\timapConn.Writeln(\". NOOP\")\n\timapConn.ExpectPattern(`\\* 1 EXISTS`)\n\timapConn.ExpectPattern(`\\* 1 RECENT`)\n\timapConn.ExpectPattern(\". OK *\")\n}\n"
  },
  {
    "path": "tests/issue327_test.go",
    "content": "//go:build integration\n// +build integration\n\n/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage tests_test\n\nimport (\n\t\"strconv\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/foxcpp/maddy/internal/testutils\"\n\t\"github.com/foxcpp/maddy/tests\"\n)\n\nfunc TestIssue327(tt *testing.T) {\n\ttt.Parallel()\n\tt := tests.NewT(tt)\n\tt.DNS(nil)\n\tt.Port(\"smtp\")\n\ttgtPort := t.Port(\"target\")\n\tt.Config(`\n\t\thostname mx.maddy.test\n\t\ttls off\n\t\tsmtp tcp://127.0.0.1:{env:TEST_PORT_smtp} {\n\t\t\tdeliver_to queue outbound_queue {\n\t\t\t\ttarget remote { } # it will fail\n\t\t\t\tautogenerated_msg_domain maddy.test\n\t\t\t\tbounce {\n\t\t\t\t\tcheck {\n\t\t\t\t\t\tspf\n\t\t\t\t\t}\n\t\t\t\t\tdeliver_to lmtp tcp://127.0.0.1:{env:TEST_PORT_target}\n\t\t\t\t}\n\t\t\t}\n\t\t}`)\n\tt.Run(1)\n\tdefer t.Close()\n\n\tbe, s := testutils.SMTPServer(tt, \"127.0.0.1:\"+strconv.Itoa(int(tgtPort)))\n\ts.LMTP = true\n\tbe.LMTPDataErr = []error{nil, nil}\n\tdefer s.Close()\n\n\tc := t.Conn(\"smtp\")\n\tdefer c.Close()\n\tc.SMTPNegotation(\"client.maddy.test\", nil, nil)\n\tc.Writeln(\"MAIL FROM:<from@maddy.test>\")\n\tc.ExpectPattern(\"250 *\")\n\tc.Writeln(\"RCPT TO:<to1@maddy.test>\")\n\tc.ExpectPattern(\"250 *\")\n\tc.Writeln(\"DATA\")\n\tc.ExpectPattern(\"354 *\")\n\tc.Writeln(\"From: <from@maddy.test>\")\n\tc.Writeln(\"To: <to@maddy.test>\")\n\tc.Writeln(\"Subject: Hello!\")\n\tc.Writeln(\"\")\n\tc.Writeln(\"Hello!\")\n\tc.Writeln(\".\")\n\tc.ExpectPattern(\"250 2.0.0 OK: queued\")\n\tc.Writeln(\"QUIT\")\n\tc.ExpectPattern(\"221 *\")\n\n\tfor i := 0; i < 5; i++ {\n\t\ttime.Sleep(1 * time.Second)\n\t\tif len(be.Messages) != 0 {\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif len(be.Messages) != 1 {\n\t\tt.Fatal(\"No DSN sent?\", len(be.Messages))\n\t}\n}\n"
  },
  {
    "path": "tests/limits_test.go",
    "content": "//go:build integration\n// +build integration\n\n/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage tests_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/foxcpp/maddy/tests\"\n)\n\nfunc TestConcurrencyLimit(tt *testing.T) {\n\ttt.Parallel()\n\tt := tests.NewT(tt)\n\tt.DNS(nil)\n\tt.Port(\"smtp\")\n\tt.Config(`\n\t\tsmtp tcp://127.0.0.1:{env:TEST_PORT_smtp} {\n\t\t\thostname mx.maddy.test\n\t\t\ttls off\n\n\t\t\tdefer_sender_reject no\n\t\t\tlimits {\n\t\t\t\tall concurrency 1\n\t\t\t}\n\n\t\t\tdeliver_to dummy\n\t\t}\n\t`)\n\tt.Run(1)\n\tdefer t.Close()\n\n\tc1 := t.Conn(\"smtp\")\n\tdefer c1.Close()\n\tc1.SMTPNegotation(\"localhost\", nil, nil)\n\tc1.Writeln(\"MAIL FROM:<testing@maddy.test>\")\n\tc1.ExpectPattern(\"250 *\")\n\t// Down on semaphore.\n\n\tc2 := t.Conn(\"smtp\")\n\tdefer c2.Close()\n\tc2.SMTPNegotation(\"localhost\", nil, nil)\n\tc1.Writeln(\"MAIL FROM:<testing@maddy.test>\")\n\t// Temporary error due to lock timeout.\n\tc1.ExpectPattern(\"451 *\")\n}\n\nfunc TestPerIPConcurrency(tt *testing.T) {\n\ttt.Parallel()\n\tt := tests.NewT(tt)\n\tt.DNS(nil)\n\tt.Port(\"smtp\")\n\tt.Config(`\n\t\tsmtp tcp://127.0.0.1:{env:TEST_PORT_smtp} {\n\t\t\thostname mx.maddy.test\n\t\t\ttls off\n\n\t\t\tdefer_sender_reject no\n\t\t\tlimits {\n\t\t\t\tip concurrency 1\n\t\t\t}\n\n\t\t\tdeliver_to dummy\n\t\t}\n\t`)\n\tt.Run(1)\n\tdefer t.Close()\n\n\tc1 := t.Conn(\"smtp\")\n\tdefer c1.Close()\n\tc1.SMTPNegotation(\"localhost\", nil, nil)\n\tc1.Writeln(\"MAIL FROM:<testing@maddy.test>\")\n\tc1.ExpectPattern(\"250 *\")\n\t// Down on semaphore.\n\n\tc3 := t.Conn4(\"127.0.0.2\", \"smtp\")\n\tdefer c3.Close()\n\tc3.SMTPNegotation(\"localhost\", nil, nil)\n\tc3.Writeln(\"MAIL FROM:<testing@maddy.test>\")\n\tc3.ExpectPattern(\"250 *\")\n\t// Down on semaphore (different IP).\n\n\tc2 := t.Conn(\"smtp\")\n\tdefer c2.Close()\n\tc2.SMTPNegotation(\"localhost\", nil, nil)\n\tc1.Writeln(\"MAIL FROM:<testing@maddy.test>\")\n\t// Temporary error due to lock timeout.\n\tc1.ExpectPattern(\"451 *\")\n}\n"
  },
  {
    "path": "tests/lmtp_test.go",
    "content": "//go:build integration\n// +build integration\n\n/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage tests_test\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/foxcpp/maddy/internal/testutils\"\n\t\"github.com/foxcpp/maddy/tests\"\n)\n\nfunc TestLMTPServer_Is_Actually_LMTP(tt *testing.T) {\n\ttt.Parallel()\n\n\tt := tests.NewT(tt)\n\tt.DNS(nil)\n\tt.Port(\"lmtp\")\n\tt.Config(`\n\t\tlmtp tcp://127.0.0.1:{env:TEST_PORT_lmtp} {\n\t\t\thostname mx.maddy.test\n\t\t\ttls off\n\t\t\tdeliver_to dummy\n\t\t}`)\n\tt.Run(1)\n\tdefer t.Close()\n\n\tc := t.Conn(\"lmtp\")\n\tdefer c.Close()\n\n\tc.Writeln(\"LHLO client.maddy.test\")\n\tc.ExpectPattern(\"220 *\")\ncapsloop:\n\tfor {\n\t\tline, err := c.Readln()\n\t\tif err != nil {\n\t\t\tt.Fatal(\"I/O error:\", err)\n\t\t}\n\t\tswitch {\n\t\tcase strings.HasPrefix(line, \"250-\"):\n\t\tcase strings.HasPrefix(line, \"250 \"):\n\t\t\tbreak capsloop\n\t\tdefault:\n\t\t\tt.Fatal(\"Unexpected deply:\", line)\n\t\t}\n\t}\n\n\tc.Writeln(\"MAIL FROM:<from@maddy.test>\")\n\tc.ExpectPattern(\"250 *\")\n\tc.Writeln(\"RCPT TO:<to1@maddy.test>\")\n\tc.ExpectPattern(\"250 *\")\n\tc.Writeln(\"RCPT TO:<to2@maddy.test>\")\n\tc.ExpectPattern(\"250 *\")\n\tc.Writeln(\"DATA\")\n\tc.ExpectPattern(\"354 *\")\n\tc.Writeln(\"From: <from@maddy.test>\")\n\tc.Writeln(\"To: <to@maddy.test>\")\n\tc.Writeln(\"Subject: Hello!\")\n\tc.Writeln(\"\")\n\tc.Writeln(\"Hello!\")\n\tc.Writeln(\".\")\n\tc.ExpectPattern(\"250 2.0.0 <to1@maddy.test> OK: queued\")\n\tc.ExpectPattern(\"250 2.0.0 <to2@maddy.test> OK: queued\")\n\tc.Writeln(\"QUIT\")\n\tc.ExpectPattern(\"221 *\")\n}\n\nfunc TestLMTPClient_Is_Actually_LMTP(tt *testing.T) {\n\tt := tests.NewT(tt)\n\tt.DNS(nil)\n\tt.Port(\"smtp\")\n\ttgtPort := t.Port(\"target\")\n\tt.Config(`\n\t\thostname mx.maddy.test\n\t\ttls off\n\t\tsmtp tcp://127.0.0.1:{env:TEST_PORT_smtp} {\n\t\t\tdeliver_to lmtp tcp://127.0.0.1:{env:TEST_PORT_target}\n\t\t}`)\n\tt.Run(1)\n\tdefer t.Close()\n\n\tbe, s := testutils.SMTPServer(tt, \"127.0.0.1:\"+strconv.Itoa(int(tgtPort)))\n\ts.LMTP = true\n\tbe.LMTPDataErr = []error{nil, nil}\n\tdefer s.Close()\n\n\tc := t.Conn(\"smtp\")\n\tdefer c.Close()\n\tc.SMTPNegotation(\"client.maddy.test\", nil, nil)\n\tc.Writeln(\"MAIL FROM:<from@maddy.test>\")\n\tc.ExpectPattern(\"250 *\")\n\tc.Writeln(\"RCPT TO:<to1@maddy.test>\")\n\tc.ExpectPattern(\"250 *\")\n\tc.Writeln(\"RCPT TO:<to2@maddy.test>\")\n\tc.ExpectPattern(\"250 *\")\n\tc.Writeln(\"DATA\")\n\tc.ExpectPattern(\"354 *\")\n\tc.Writeln(\"From: <from@maddy.test>\")\n\tc.Writeln(\"To: <to@maddy.test>\")\n\tc.Writeln(\"Subject: Hello!\")\n\tc.Writeln(\"\")\n\tc.Writeln(\"Hello!\")\n\tc.Writeln(\".\")\n\tc.ExpectPattern(\"250 2.0.0 OK: queued\")\n\tc.Writeln(\"QUIT\")\n\tc.ExpectPattern(\"221 *\")\n\n\tif be.SessionCounter != 1 {\n\t\tt.Fatal(\"No actual connection made?\", be.SessionCounter)\n\t}\n}\n\nfunc TestLMTPClient_Issue308(tt *testing.T) {\n\tt := tests.NewT(tt)\n\tt.DNS(nil)\n\tt.Port(\"smtp\")\n\ttgtPort := t.Port(\"target\")\n\tt.Config(`\n\t\thostname mx.maddy.test\n\t\ttls off\n\n\t\ttarget.lmtp local_mailboxes {\n\t\t\ttargets tcp://127.0.0.1:{env:TEST_PORT_target}\n\t\t}\n\n\t\tsmtp tcp://127.0.0.1:{env:TEST_PORT_smtp} {\n\t\t\tdeliver_to &local_mailboxes\n\t\t}`)\n\tt.Run(1)\n\tdefer t.Close()\n\n\tbe, s := testutils.SMTPServer(tt, \"127.0.0.1:\"+strconv.Itoa(int(tgtPort)))\n\ts.LMTP = true\n\tbe.LMTPDataErr = []error{nil, nil}\n\tdefer s.Close()\n\n\tc := t.Conn(\"smtp\")\n\tdefer c.Close()\n\tc.SMTPNegotation(\"client.maddy.test\", nil, nil)\n\tc.Writeln(\"MAIL FROM:<from@maddy.test>\")\n\tc.ExpectPattern(\"250 *\")\n\tc.Writeln(\"RCPT TO:<to1@maddy.test>\")\n\tc.ExpectPattern(\"250 *\")\n\tc.Writeln(\"RCPT TO:<to2@maddy.test>\")\n\tc.ExpectPattern(\"250 *\")\n\tc.Writeln(\"DATA\")\n\tc.ExpectPattern(\"354 *\")\n\tc.Writeln(\"From: <from@maddy.test>\")\n\tc.Writeln(\"To: <to@maddy.test>\")\n\tc.Writeln(\"Subject: Hello!\")\n\tc.Writeln(\"\")\n\tc.Writeln(\"Hello!\")\n\tc.Writeln(\".\")\n\tc.ExpectPattern(\"250 2.0.0 OK: queued\")\n\tc.Writeln(\"QUIT\")\n\tc.ExpectPattern(\"221 *\")\n\n\tif be.SessionCounter != 1 {\n\t\tt.Fatal(\"No actual connection made?\", be.SessionCounter)\n\t}\n}\n"
  },
  {
    "path": "tests/modules_test.go",
    "content": "//go:build integration\n\n/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage tests_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/foxcpp/maddy/tests\"\n)\n\nfunc TestConfigCycle(tt *testing.T) {\n\ttt.Parallel()\n\n\tt := tests.NewT(tt)\n\tt.DNS(nil)\n\tt.Config(`\n\t\thostname mx.maddy.test\n\n\t\tmsgpipeline local_routing {\n\t\t\tdestination maddy.test {\n\t\t\t\tdeliver_to dummy\n\t\t\t}\n\t\t\tdefault_destination {\n\t\t\t\tdeliver_to &outbound_queue\n\t\t\t}\n\t\t}\n\n\t\ttarget.queue outbound_queue {\n\t\t\ttarget dummy\n\t\t\tautogenerated_msg_domain maddy.test\n\t\t\tbounce {\n\t\t\t\tdeliver_to &local_routing\n\t\t\t}\n\t\t}\n\n\t\tsmtp tcp://127.0.0.1:1443 {\n\t\t\ttls off\n\n\t\t\tdeliver_to &local_routing\n\t\t}\n    `)\n\tt.Run(1)\n\n\tt.Close()\n}\n"
  },
  {
    "path": "tests/mta_test.go",
    "content": "//go:build integration\n// +build integration\n\n/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage tests_test\n\nimport (\n\t\"net\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"github.com/foxcpp/go-mockdns\"\n\t\"github.com/foxcpp/maddy/internal/testutils\"\n\t\"github.com/foxcpp/maddy/tests\"\n)\n\nfunc TestMTA_Outbound(tt *testing.T) {\n\tt := tests.NewT(tt)\n\tt.DNS(map[string]mockdns.Zone{\n\t\t\"example.invalid.\": {\n\t\t\tMX: []net.MX{{Host: \"mx.example.invalid.\", Pref: 10}},\n\t\t},\n\t\t\"mx.example.invalid.\": {\n\t\t\tA: []string{\"127.0.0.1\"},\n\t\t},\n\t})\n\tt.Port(\"smtp\")\n\ttgtPort := t.Port(\"remote_smtp\")\n\tt.Config(`\n\t\thostname mx.maddy.test\n\t\ttls off\n\t\tsmtp tcp://127.0.0.1:{env:TEST_PORT_smtp} {\n\t\t\tdeliver_to remote\n\t\t}`)\n\tt.Run(1)\n\tdefer t.Close()\n\n\tbe, s := testutils.SMTPServer(tt, \"127.0.0.1:\"+strconv.Itoa(int(tgtPort)))\n\tdefer s.Close()\n\n\tc := t.Conn(\"smtp\")\n\tdefer c.Close()\n\tc.SMTPNegotation(\"client.maddy.test\", nil, nil)\n\tc.Writeln(\"MAIL FROM:<from@maddy.test>\")\n\tc.ExpectPattern(\"250 *\")\n\tc.Writeln(\"RCPT TO:<to1@example.invalid>\")\n\tc.ExpectPattern(\"250 *\")\n\tc.Writeln(\"RCPT TO:<to2@example.invalid>\")\n\tc.ExpectPattern(\"250 *\")\n\tc.Writeln(\"DATA\")\n\tc.ExpectPattern(\"354 *\")\n\tc.Writeln(\"From: <from@maddy.test>\")\n\tc.Writeln(\"To: <to@maddy.test>\")\n\tc.Writeln(\"Subject: Hello!\")\n\tc.Writeln(\"\")\n\tc.Writeln(\"Hello!\")\n\tc.Writeln(\".\")\n\tc.ExpectPattern(\"250 2.0.0 OK: queued\")\n\tc.Writeln(\"QUIT\")\n\tc.ExpectPattern(\"221 *\")\n\n\tif be.SessionCounter != 1 {\n\t\tt.Fatal(\"No actual connection made?\", be.SessionCounter)\n\t}\n}\n\nfunc TestIssue321(tt *testing.T) {\n\tt := tests.NewT(tt)\n\tt.DNS(map[string]mockdns.Zone{\n\t\t\"example.invalid.\": {\n\t\t\tAD: true,\n\t\t\tA:  []string{\"127.0.0.1\"},\n\t\t},\n\t})\n\tt.Port(\"smtp\")\n\ttgtPort := t.Port(\"remote_smtp\")\n\tt.Config(`\n\t\thostname mx.maddy.test\n\t\ttls off\n\t\tsmtp tcp://127.0.0.1:{env:TEST_PORT_smtp} {\n\t\t\tdeliver_to remote {\n\t\t\t\tmx_auth {\n\t\t\t\t\tdnssec\n\t\t\t\t\tdane\n\t\t\t\t}\n\t\t\t}\n\t\t}`)\n\tt.Run(1)\n\tdefer t.Close()\n\n\tbe, s := testutils.SMTPServer(tt, \"127.0.0.1:\"+strconv.Itoa(int(tgtPort)))\n\tdefer s.Close()\n\n\tc := t.Conn(\"smtp\")\n\tdefer c.Close()\n\tc.SMTPNegotation(\"client.maddy.test\", nil, nil)\n\tc.Writeln(\"MAIL FROM:<from@maddy.test>\")\n\tc.ExpectPattern(\"250 *\")\n\tc.Writeln(\"RCPT TO:<to1@example.invalid>\")\n\tc.ExpectPattern(\"250 *\")\n\tc.Writeln(\"RCPT TO:<to2@example.invalid>\")\n\tc.ExpectPattern(\"250 *\")\n\tc.Writeln(\"DATA\")\n\tc.ExpectPattern(\"354 *\")\n\tc.Writeln(\"From: <from@maddy.test>\")\n\tc.Writeln(\"To: <to@maddy.test>\")\n\tc.Writeln(\"Subject: Hello!\")\n\tc.Writeln(\"\")\n\tc.Writeln(\"Hello!\")\n\tc.Writeln(\".\")\n\tc.ExpectPattern(\"250 2.0.0 OK: queued\")\n\tc.Writeln(\"QUIT\")\n\tc.ExpectPattern(\"221 *\")\n\n\tif be.SessionCounter != 1 {\n\t\tt.Fatal(\"No actual connection made?\", be.SessionCounter)\n\t}\n}\n"
  },
  {
    "path": "tests/multiple_domains_test.go",
    "content": "//go:build integration\n\n/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2025 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage tests_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/foxcpp/maddy/tests\"\n)\n\n// Test cases based on https://maddy.email/multiple-domains/\n\nfunc TestMultipleDomains_SeparateNamespace(tt *testing.T) {\n\ttt.Parallel()\n\tt := tests.NewT(tt)\n\tt.DNS(nil)\n\tt.Port(\"submission\")\n\tt.Port(\"imap\")\n\tt.Config(`\n\t\ttls off\n\t\thostname test.maddy.email\n\n\t\tauth.pass_table local_authdb {\n\t\t\ttable sql_table {\n\t\t\t\tdriver sqlite3\n\t\t\t\tdsn credentials.db\n\t\t\t\ttable_name passwords\n\t\t\t}\n\t\t}\n\t\tstorage.imapsql local_mailboxes {\n\t\t\tdriver sqlite3\n\t\t\tdsn imapsql.db\n\t\t}\n\n\t\tsubmission tcp://0.0.0.0:{env:TEST_PORT_submission} {\n\t\t\tauth &local_authdb\n\t\t\treject\n\t\t}\n\t\timap tcp://127.0.0.1:{env:TEST_PORT_imap} {\n\t\t\tauth &local_authdb\n\t\t\tstorage &local_mailboxes\n\t\t}\n\t`)\n\n\tt.MustRunCLIGroup(\n\t\t[]string{\"creds\", \"create\", \"-p\", \"user1\", \"user1@test1.maddy.email\"},\n\t\t[]string{\"creds\", \"create\", \"-p\", \"user2\", \"user2@test1.maddy.email\"},\n\t\t[]string{\"creds\", \"create\", \"-p\", \"user3\", \"user1@test2.maddy.email\"},\n\t\t[]string{\"imap-acct\", \"create\", \"--no-specialuse\", \"user1@test1.maddy.email\"},\n\t\t[]string{\"imap-acct\", \"create\", \"--no-specialuse\", \"user2@test1.maddy.email\"},\n\t\t[]string{\"imap-acct\", \"create\", \"--no-specialuse\", \"user1@test2.maddy.email\"},\n\t)\n\tt.Run(2)\n\n\tuser1 := t.Conn(\"imap\")\n\tdefer user1.Close()\n\tuser1.ExpectPattern(`\\* OK *`)\n\tuser1.Writeln(`. LOGIN user1@test1.maddy.email user1`)\n\tuser1.ExpectPattern(`. OK *`)\n\tuser1.Writeln(`. CREATE user1`)\n\tuser1.ExpectPattern(`. OK *`)\n\n\tuser1SMTP := t.Conn(\"submission\")\n\tdefer user1SMTP.Close()\n\tuser1SMTP.SMTPNegotation(\"localhost\", []string{\"AUTH PLAIN\"}, nil)\n\tuser1SMTP.SMTPPlainAuth(\"user1@test1.maddy.email\", \"user1\", true)\n\n\tuser2 := t.Conn(\"imap\")\n\tdefer user2.Close()\n\tuser2.ExpectPattern(`\\* OK *`)\n\tuser2.Writeln(`. LOGIN user2@test1.maddy.email user2`)\n\tuser2.ExpectPattern(`. OK *`)\n\tuser2.Writeln(`. CREATE user2`)\n\tuser2.ExpectPattern(`. OK *`)\n\n\tuser2SMTP := t.Conn(\"submission\")\n\tdefer user2SMTP.Close()\n\tuser2SMTP.SMTPNegotation(\"localhost\", []string{\"AUTH PLAIN\"}, nil)\n\tuser2SMTP.SMTPPlainAuth(\"user2@test1.maddy.email\", \"user2\", true)\n\n\tuser3 := t.Conn(\"imap\")\n\tdefer user3.Close()\n\tuser3.ExpectPattern(`\\* OK *`)\n\tuser3.Writeln(`. LOGIN user1@test2.maddy.email user3`)\n\tuser3.ExpectPattern(`. OK *`)\n\tuser3.Writeln(`. CREATE user3`)\n\tuser3.ExpectPattern(`. OK *`)\n\n\tuser3SMTP := t.Conn(\"submission\")\n\tdefer user3SMTP.Close()\n\tuser3SMTP.SMTPNegotation(\"localhost\", []string{\"AUTH PLAIN\"}, nil)\n\tuser3SMTP.SMTPPlainAuth(\"user1@test2.maddy.email\", \"user3\", true)\n\n\tuser1.Writeln(`. LIST \"\" \"*\"`)\n\tuser1.Expect(`* LIST (\\HasNoChildren) \".\" INBOX`)\n\tuser1.Expect(`* LIST (\\HasNoChildren) \".\" \"user1\"`)\n\tuser1.ExpectPattern(\". OK *\")\n\n\tuser2.Writeln(`. LIST \"\" \"*\"`)\n\tuser2.Expect(`* LIST (\\HasNoChildren) \".\" INBOX`)\n\tuser2.Expect(`* LIST (\\HasNoChildren) \".\" \"user2\"`)\n\tuser2.ExpectPattern(\". OK *\")\n\n\tuser3.Writeln(`. LIST \"\" \"*\"`)\n\tuser3.Expect(`* LIST (\\HasNoChildren) \".\" INBOX`)\n\tuser3.Expect(`* LIST (\\HasNoChildren) \".\" \"user3\"`)\n\tuser3.ExpectPattern(\". OK *\")\n}\n\nfunc TestMultipleDomains_SharedCredentials_DistinctMailboxes(tt *testing.T) {\n\ttt.Parallel()\n\tt := tests.NewT(tt)\n\tt.DNS(nil)\n\tt.Port(\"submission\")\n\tt.Port(\"imap\")\n\tt.Config(`\n\t\ttls off\n\t\thostname test.maddy.email\n\t\tauth_map email_localpart\n\n\t\tauth.pass_table local_authdb {\n\t\t\ttable sql_table {\n\t\t\t\tdriver sqlite3\n\t\t\t\tdsn credentials.db\n\t\t\t\ttable_name passwords\n\t\t\t}\n\t\t}\n\t\tstorage.imapsql local_mailboxes {\n\t\t\tdriver sqlite3\n\t\t\tdsn imapsql.db\n\t\t}\n\n\t\tsubmission tcp://0.0.0.0:{env:TEST_PORT_submission} {\n\t\t\tauth &local_authdb\n\t\t\treject\n\t\t}\n\t\timap tcp://127.0.0.1:{env:TEST_PORT_imap} {\n\t\t\tauth &local_authdb\n\t\t\tstorage &local_mailboxes\n\t\t}\n\t`)\n\n\tt.MustRunCLIGroup(\n\t\t[]string{\"creds\", \"create\", \"-p\", \"user1\", \"user1\"},\n\t\t[]string{\"creds\", \"create\", \"-p\", \"user2\", \"user2\"},\n\t\t[]string{\"imap-acct\", \"create\", \"--no-specialuse\", \"user1@test1.maddy.email\"},\n\t\t[]string{\"imap-acct\", \"create\", \"--no-specialuse\", \"user2@test1.maddy.email\"},\n\t\t[]string{\"imap-acct\", \"create\", \"--no-specialuse\", \"user1@test2.maddy.email\"},\n\t)\n\tt.Run(2)\n\n\tuser1 := t.Conn(\"imap\")\n\tdefer user1.Close()\n\tuser1.ExpectPattern(`\\* OK *`)\n\tuser1.Writeln(`. LOGIN user1@test1.maddy.email user1`)\n\tuser1.ExpectPattern(`. OK *`)\n\tuser1.Writeln(`. CREATE user1`)\n\tuser1.ExpectPattern(`. OK *`)\n\n\tuser1SMTP := t.Conn(\"submission\")\n\tdefer user1SMTP.Close()\n\tuser1SMTP.SMTPNegotation(\"localhost\", []string{\"AUTH PLAIN\"}, nil)\n\tuser1SMTP.SMTPPlainAuth(\"user1@test1.maddy.email\", \"user1\", true)\n\n\tuser2 := t.Conn(\"imap\")\n\tdefer user2.Close()\n\tuser2.ExpectPattern(`\\* OK *`)\n\tuser2.Writeln(`. LOGIN user2@test1.maddy.email user2`)\n\tuser2.ExpectPattern(`. OK *`)\n\tuser2.Writeln(`. CREATE user2`)\n\tuser2.ExpectPattern(`. OK *`)\n\n\tuser2SMTP := t.Conn(\"submission\")\n\tdefer user2SMTP.Close()\n\tuser2SMTP.SMTPNegotation(\"localhost\", []string{\"AUTH PLAIN\"}, nil)\n\tuser2SMTP.SMTPPlainAuth(\"user2@test1.maddy.email\", \"user2\", true)\n\n\tuser3 := t.Conn(\"imap\")\n\tdefer user3.Close()\n\tuser3.ExpectPattern(`\\* OK *`)\n\tuser3.Writeln(`. LOGIN user1@test2.maddy.email user1`)\n\tuser3.ExpectPattern(`. OK *`)\n\tuser3.Writeln(`. CREATE user3`)\n\tuser3.ExpectPattern(`. OK *`)\n\n\tuser3SMTP := t.Conn(\"submission\")\n\tdefer user3SMTP.Close()\n\tuser3SMTP.SMTPNegotation(\"localhost\", []string{\"AUTH PLAIN\"}, nil)\n\tuser3SMTP.SMTPPlainAuth(\"user1@test2.maddy.email\", \"user1\", true)\n\n\tuser1.Writeln(`. LIST \"\" \"*\"`)\n\tuser1.Expect(`* LIST (\\HasNoChildren) \".\" INBOX`)\n\tuser1.Expect(`* LIST (\\HasNoChildren) \".\" \"user1\"`)\n\tuser1.ExpectPattern(\". OK *\")\n\n\tuser2.Writeln(`. LIST \"\" \"*\"`)\n\tuser2.Expect(`* LIST (\\HasNoChildren) \".\" INBOX`)\n\tuser2.Expect(`* LIST (\\HasNoChildren) \".\" \"user2\"`)\n\tuser2.ExpectPattern(\". OK *\")\n\n\tuser3.Writeln(`. LIST \"\" \"*\"`)\n\tuser3.Expect(`* LIST (\\HasNoChildren) \".\" INBOX`)\n\tuser3.Expect(`* LIST (\\HasNoChildren) \".\" \"user3\"`)\n\tuser3.ExpectPattern(\". OK *\")\n}\n\nfunc TestMultipleDomains_SharedCredentials_SharedMailboxes(tt *testing.T) {\n\ttt.Parallel()\n\tt := tests.NewT(tt)\n\tt.DNS(nil)\n\tt.Port(\"submission\")\n\tt.Port(\"imap\")\n\tt.Config(`\n\t\ttls off\n\t\thostname test.maddy.email\n\t\tauth_map email_localpart_optional\n\n\t\tauth.pass_table local_authdb {\n\t\t\ttable sql_table {\n\t\t\t\tdriver sqlite3\n\t\t\t\tdsn credentials.db\n\t\t\t\ttable_name passwords\n\t\t\t}\n\t\t}\n\t\tstorage.imapsql local_mailboxes {\n\t\t\tdriver sqlite3\n\t\t\tdsn imapsql.db\n\n\t\t\tdelivery_map email_localpart_optional\n\t\t}\n\n\t\tsubmission tcp://0.0.0.0:{env:TEST_PORT_submission} {\n\t\t\tauth &local_authdb\n\t\t\treject\n\t\t}\n\t\timap tcp://127.0.0.1:{env:TEST_PORT_imap} {\n\t\t\tauth &local_authdb\n\t\t\tstorage &local_mailboxes\n\n\t\t\tstorage_map email_localpart_optional\n\t\t}\n\t`)\n\n\tt.MustRunCLIGroup(\n\t\t[]string{\"creds\", \"create\", \"-p\", \"user1\", \"user1\"},\n\t\t[]string{\"creds\", \"create\", \"-p\", \"user2\", \"user2\"},\n\t\t[]string{\"imap-acct\", \"create\", \"--no-specialuse\", \"user1\"},\n\t\t[]string{\"imap-acct\", \"create\", \"--no-specialuse\", \"user2\"},\n\t)\n\tt.Run(2)\n\n\tuser1 := t.Conn(\"imap\")\n\tdefer user1.Close()\n\tuser1.ExpectPattern(`\\* OK *`)\n\tuser1.Writeln(`. LOGIN user1 user1`)\n\tuser1.ExpectPattern(`. OK *`)\n\tuser1.Writeln(`. CREATE user1`)\n\tuser1.ExpectPattern(`. OK *`)\n\n\tuser1SMTP := t.Conn(\"submission\")\n\tdefer user1SMTP.Close()\n\tuser1SMTP.SMTPNegotation(\"localhost\", []string{\"AUTH PLAIN\"}, nil)\n\tuser1SMTP.SMTPPlainAuth(\"user1\", \"user1\", true)\n\n\tuser2 := t.Conn(\"imap\")\n\tdefer user2.Close()\n\tuser2.ExpectPattern(`\\* OK *`)\n\tuser2.Writeln(`. LOGIN user2@test1.maddy.email user2`)\n\tuser2.ExpectPattern(`. OK *`)\n\tuser2.Writeln(`. CREATE user2`)\n\tuser2.ExpectPattern(`. OK *`)\n\n\tuser2SMTP := t.Conn(\"submission\")\n\tdefer user2SMTP.Close()\n\tuser2SMTP.SMTPNegotation(\"localhost\", []string{\"AUTH PLAIN\"}, nil)\n\tuser2SMTP.SMTPPlainAuth(\"user2\", \"user2\", true)\n\n\tuser12 := t.Conn(\"imap\")\n\tdefer user12.Close()\n\tuser12.ExpectPattern(`\\* OK *`)\n\tuser12.Writeln(`. LOGIN user1@test2.maddy.email user1`)\n\tuser12.ExpectPattern(`. OK *`)\n\tuser12.Writeln(`. CREATE user12`)\n\tuser12.ExpectPattern(`. OK *`)\n\n\tuser13 := t.Conn(\"imap\")\n\tdefer user13.Close()\n\tuser13.ExpectPattern(`\\* OK *`)\n\tuser13.Writeln(`. LOGIN user1@test.maddy.email user1`)\n\tuser13.ExpectPattern(`. OK *`)\n\tuser13.Writeln(`. CREATE user13`)\n\tuser13.ExpectPattern(`. OK *`)\n\n\tuser12SMTP := t.Conn(\"submission\")\n\tdefer user12SMTP.Close()\n\tuser12SMTP.SMTPNegotation(\"localhost\", []string{\"AUTH PLAIN\"}, nil)\n\tuser12SMTP.SMTPPlainAuth(\"user1\", \"user1\", true)\n\n\tuser13SMTP := t.Conn(\"submission\")\n\tdefer user13SMTP.Close()\n\tuser13SMTP.SMTPNegotation(\"localhost\", []string{\"AUTH PLAIN\"}, nil)\n\tuser13SMTP.SMTPPlainAuth(\"user1@test.maddy.email\", \"user1\", true)\n\n\tuser1.Writeln(`. LIST \"\" \"*\"`)\n\tuser1.Expect(`* LIST (\\HasNoChildren) \".\" INBOX`)\n\tuser1.Expect(`* LIST (\\HasNoChildren) \".\" \"user1\"`)\n\tuser1.Expect(`* LIST (\\HasNoChildren) \".\" \"user12\"`)\n\tuser1.Expect(`* LIST (\\HasNoChildren) \".\" \"user13\"`)\n\tuser1.ExpectPattern(\". OK *\")\n\n\tuser2.Writeln(`. LIST \"\" \"*\"`)\n\tuser2.Expect(`* LIST (\\HasNoChildren) \".\" INBOX`)\n\tuser2.Expect(`* LIST (\\HasNoChildren) \".\" \"user2\"`)\n\tuser2.ExpectPattern(\". OK *\")\n\n\tuser12.Writeln(`. LIST \"\" \"*\"`)\n\tuser12.Expect(`* LIST (\\HasNoChildren) \".\" INBOX`)\n\tuser12.Expect(`* LIST (\\HasNoChildren) \".\" \"user1\"`)\n\tuser12.Expect(`* LIST (\\HasNoChildren) \".\" \"user12\"`)\n\tuser12.Expect(`* LIST (\\HasNoChildren) \".\" \"user13\"`)\n\tuser12.ExpectPattern(\". OK *\")\n}\n"
  },
  {
    "path": "tests/reload_non_unix.go",
    "content": "//go:build !unix\n\n/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2026 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage tests\n\nfunc (t *T) reloadConfig() {\n\tt.Skip(\"Tests for config reload are not available\")\n}\n"
  },
  {
    "path": "tests/reload_test.go",
    "content": "//go:build unix && integration\n\n// Can't reload on Windows, yet\n\n/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2026 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage tests_test\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\tsqliteprovider \"github.com/foxcpp/maddy/internal/sqlite\"\n\t\"github.com/foxcpp/maddy/tests\"\n)\n\nfunc TestSmtpPipelineSwitch(tt *testing.T) {\n\tif !sqliteprovider.IsTranspiled {\n\t\ttt.Skip(\"Test is unstable with original SQLite\")\n\t}\n\n\ttt.Parallel()\n\tt := tests.NewT(tt)\n\n\tt.DNS(nil)\n\tt.Port(\"smtp\")\n\tt.Config(`\n\t\tsmtp tcp://127.0.0.1:{env:TEST_PORT_smtp} {\n\t\t\thostname maddy.test\n\t\t\ttls off\n\n\t\t\treject\n\t\t}\n\t`)\n\tt.Run(1)\n\tdefer t.Close()\n\n\tconn1 := t.Conn(\"smtp\")\n\tdefer conn1.Close()\n\tconn1.SMTPNegotation(\"localhost\", nil, nil)\n\tconn1.Writeln(\"MAIL FROM:<sender@maddy.test>\")\n\tconn1.ExpectPattern(\"2*\")\n\tconn1.Writeln(\"RCPT TO:<testusr@maddy.test>\")\n\tconn1.ExpectPattern(\"5*\") // REJECTED\n\tconn1.Writeln(\"RSET\")\n\tconn1.ExpectPattern(\"2*\")\n\n\tt.Config(`\n\t\tsmtp tcp://127.0.0.1:{env:TEST_PORT_smtp} {\n\t\t\thostname maddy.test\n\t\t\ttls off\n\n\t\t\tdeliver_to dummy\n\t\t}\n\t`)\n\n\tconn2 := t.Conn(\"smtp\")\n\tdefer conn2.Close()\n\tconn2.SMTPNegotation(\"localhost\", nil, nil)\n\tconn2.Writeln(\"MAIL FROM:<sender@maddy.test>\")\n\tconn2.ExpectPattern(\"2*\")\n\tconn2.Writeln(\"RCPT TO:<testusr@maddy.test>\")\n\tconn2.ExpectPattern(\"2*\")\n\tconn2.Writeln(\"DATA\")\n\tconn2.ExpectPattern(\"354 *\")\n\tconn2.Writeln(\"From: <sender@maddy.test>\")\n\tconn2.Writeln(\"To: <testusr@maddy.test>\")\n\tconn2.Writeln(\"Subject: Hi!\")\n\tconn2.Writeln(\"\")\n\tconn2.Writeln(\"Hi!\")\n\tconn2.Writeln(\".\")\n\tconn2.ExpectPattern(\"2*\") // DISCARDED\n\n\tconn1.Writeln(\"MAIL FROM:<sender@maddy.test>\")\n\tconn1.ExpectPattern(\"2*\")\n\tconn1.Writeln(\"RCPT TO:<testusr@maddy.test>\")\n\tconn1.ExpectPattern(\"5*\") // Still REJECTED (running on old server).\n\tconn1.Writeln(\"RSET\")\n\tconn1.ExpectPattern(\"2*\")\n}\n\nfunc TestImapStorageSwitch(tt *testing.T) {\n\tif !sqliteprovider.IsTranspiled {\n\t\ttt.Skip(\"Test is unstable with original SQLite\")\n\t}\n\n\ttt.Parallel()\n\tt := tests.NewT(tt)\n\n\tt.DNS(nil)\n\tt.Port(\"smtp\")\n\tt.Port(\"imap\")\n\tt.Config(`\n\t\tstorage.imapsql test_store {\n\t\t\tdriver sqlite3\n\t\t\tdsn imapsql.db\n\t\t}\n\n\t\timap tcp://127.0.0.1:{env:TEST_PORT_imap} {\n\t\t\ttls off\n\n\t\t\tauth dummy\n\t\t\tstorage &test_store\n\t\t}\n\n\t\tsmtp tcp://127.0.0.1:{env:TEST_PORT_smtp} {\n\t\t\thostname maddy.test\n\t\t\ttls off\n\n\t\t\tdeliver_to &test_store\n\t\t}\n\t`)\n\tt.Run(1)\n\tdefer t.Close()\n\n\timapConn := t.Conn(\"imap\")\n\tdefer imapConn.Close()\n\timapConn.ExpectPattern(`\\* OK *`)\n\timapConn.Writeln(\". LOGIN testusr@maddy.test 1234\")\n\timapConn.ExpectPattern(\". OK *\")\n\timapConn.Writeln(\". SELECT INBOX\")\n\timapConn.ExpectPattern(`\\* *`)\n\timapConn.ExpectPattern(`\\* *`)\n\timapConn.ExpectPattern(`\\* *`)\n\timapConn.ExpectPattern(`\\* *`)\n\timapConn.ExpectPattern(`\\* *`)\n\timapConn.ExpectPattern(`\\* *`)\n\timapConn.ExpectPattern(`. OK *`)\n\n\tconn1 := t.Conn(\"smtp\")\n\tdefer conn1.Close()\n\tconn1.SMTPNegotation(\"localhost\", nil, nil)\n\tconn1.Writeln(\"MAIL FROM:<sender@maddy.test>\")\n\tconn1.ExpectPattern(\"2*\")\n\tconn1.Writeln(\"RCPT TO:<testusr@maddy.test>\")\n\tconn1.ExpectPattern(\"2*\")\n\tconn1.Writeln(\"DATA\")\n\tconn1.ExpectPattern(\"354 *\")\n\tconn1.Writeln(\"From: <sender@maddy.test>\")\n\tconn1.Writeln(\"To: <testusr@maddy.test>\")\n\tconn1.Writeln(\"Subject: Store 1\")\n\tconn1.Writeln(\"\")\n\tconn1.Writeln(\"Hi!\")\n\tconn1.Writeln(\".\")\n\tconn1.ExpectPattern(\"2*\") // Goes to storage 1\n\n\tt.Config(`\n\t\tstorage.imapsql test_store {\n\t\t\tdriver sqlite3\n\t\t\tdsn imapsql2.db\n\t\t}\n\n\t\timap tcp://127.0.0.1:{env:TEST_PORT_imap} {\n\t\t\ttls off\n\n\t\t\tauth dummy\n\t\t\tstorage &test_store\n\t\t}\n\n\t\tsmtp tcp://127.0.0.1:{env:TEST_PORT_smtp} {\n\t\t\thostname maddy.test\n\t\t\ttls off\n\n\t\t\tdeliver_to &test_store\n\t\t}\n\t`)\n\n\timapConn2 := t.Conn(\"imap\")\n\tdefer imapConn2.Close()\n\timapConn2.ExpectPattern(`\\* OK *`)\n\timapConn2.Writeln(\". LOGIN testusr2@maddy.test 1234\")\n\timapConn2.ExpectPattern(\". OK *\")\n\n\ttime.Sleep(500 * time.Millisecond)\n\n\tconn2 := t.Conn(\"smtp\")\n\tdefer conn2.Close()\n\tconn2.SMTPNegotation(\"localhost\", nil, nil)\n\tconn2.Writeln(\"MAIL FROM:<sender@maddy.test>\")\n\tconn2.ExpectPattern(\"2*\")\n\tconn2.Writeln(\"RCPT TO:<testusr2@maddy.test>\")\n\tconn2.ExpectPattern(\"2*\")\n\tconn2.Writeln(\"DATA\")\n\tconn2.ExpectPattern(\"354 *\")\n\tconn2.Writeln(\"From: <sender@maddy.test>\")\n\tconn2.Writeln(\"To: <testusr2@maddy.test>\")\n\tconn2.Writeln(\"Subject: Store 2\")\n\tconn2.Writeln(\"\")\n\tconn2.Writeln(\"Hi!\")\n\tconn2.Writeln(\".\")\n\tconn2.ExpectPattern(\"2*\") // Goes to storage 2\n\n\timapConn.Writeln(\". NOOP\")\n\timapConn.ExpectPattern(`\\* 1 EXISTS`)\n\timapConn.ExpectPattern(`\\* 1 RECENT`)\n\timapConn.ExpectPattern(\". OK *\")\n\n\t// Old connection sees message in store 1.\n\timapConn.Writeln(\". FETCH 1 (BODY.PEEK[])\")\n\timapConn.ExpectPattern(`\\* 1 FETCH (BODY\\[\\] {*}*`)\n\timapConn.Expect(`Delivered-To: testusr@maddy.test`)\n\timapConn.Expect(`Return-Path: <sender@maddy.test>`)\n\timapConn.ExpectPattern(`Received: from localhost (client.maddy.test \\[` + tests.DefaultSourceIP.String() + `\\]) by maddy.test`)\n\timapConn.ExpectPattern(` (envelope-sender <sender@maddy.test>) with ESMTP id *; *`)\n\timapConn.ExpectPattern(` *`)\n\timapConn.Expect(\"From: <sender@maddy.test>\")\n\timapConn.Expect(\"To: <testusr@maddy.test>\")\n\timapConn.Expect(\"Subject: Store 1\")\n\timapConn.Expect(\"\")\n\timapConn.Expect(\"Hi!\")\n\timapConn.Expect(\")\")\n\timapConn.ExpectPattern(`. OK *`)\n\n\t// New connection sees message in store 2.\n\timapConn2.Writeln(\". SELECT INBOX\")\n\timapConn2.ExpectPattern(`\\* *`)\n\timapConn2.ExpectPattern(`\\* *`)\n\timapConn2.ExpectPattern(`\\* *`)\n\timapConn2.ExpectPattern(`\\* *`)\n\timapConn2.ExpectPattern(`\\* *`)\n\timapConn2.ExpectPattern(`\\* *`)\n\timapConn2.ExpectPattern(`\\* *`)\n\timapConn2.ExpectPattern(`. OK *`)\n\timapConn2.Writeln(\". FETCH 1 (BODY.PEEK[])\")\n\timapConn2.ExpectPattern(`\\* 1 FETCH (BODY\\[\\] {*}*`)\n\timapConn2.Expect(`Delivered-To: testusr2@maddy.test`)\n\timapConn2.Expect(`Return-Path: <sender@maddy.test>`)\n\timapConn2.ExpectPattern(`Received: from localhost (client.maddy.test \\[` + tests.DefaultSourceIP.String() + `\\]) by maddy.test`)\n\timapConn2.ExpectPattern(` (envelope-sender <sender@maddy.test>) with ESMTP id *; *`)\n\timapConn2.ExpectPattern(` *`)\n\timapConn2.Expect(\"From: <sender@maddy.test>\")\n\timapConn2.Expect(\"To: <testusr2@maddy.test>\")\n\timapConn2.Expect(\"Subject: Store 2\")\n\timapConn2.Expect(\"\")\n\timapConn2.Expect(\"Hi!\")\n\timapConn2.Expect(\")\")\n\timapConn2.ExpectPattern(`. OK *`)\n\n}\n"
  },
  {
    "path": "tests/reload_unix.go",
    "content": "//go:build unix\n\n/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2026 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage tests\n\nimport (\n\t\"syscall\"\n\t\"time\"\n)\n\nfunc (t *T) reloadConfig() {\n\terr := t.servProc.Process.Signal(syscall.SIGUSR2)\n\tif err != nil {\n\t\tt.Fatal(\"Failed to send SIGUSR2:\", err)\n\t}\n\n\tt.Log(\"waiting for server to reload...\")\n\n\tselect {\n\tcase <-t.reloadedChan:\n\tcase <-time.After(5 * time.Second):\n\t\tt.killServer()\n\t\tt.Fatal(\"Server reload is taking too long, killed\")\n\t}\n}\n"
  },
  {
    "path": "tests/replace_addr_test.go",
    "content": "//go:build integration\n// +build integration\n\n/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage tests_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/foxcpp/maddy/tests\"\n)\n\nfunc TestReplaceAddr_Rcpt(tt *testing.T) {\n\ttest := func(name, cfg string) {\n\t\ttt.Run(name, func(tt *testing.T) {\n\t\t\tt := tests.NewT(tt)\n\t\t\tt.DNS(nil)\n\t\t\tt.Port(\"smtp\")\n\t\t\tt.Config(cfg)\n\t\t\tt.Run(1)\n\t\t\tdefer t.Close()\n\n\t\t\tc := t.Conn(\"smtp\")\n\t\t\tdefer c.Close()\n\t\t\tc.SMTPNegotation(\"client.maddy.test\", nil, nil)\n\t\t\tc.Writeln(\"MAIL FROM:<a@maddy.test>\")\n\t\t\tc.ExpectPattern(\"250 *\")\n\t\t\tc.Writeln(\"RCPT TO:<a@maddy.test>\")\n\t\t\tc.ExpectPattern(\"250 *\")\n\t\t\tc.Writeln(\"DATA\")\n\t\t\tc.ExpectPattern(\"354 *\")\n\t\t\tc.Writeln(\"From: <from@maddy.test>\")\n\t\t\tc.Writeln(\"To: <to@maddy.test>\")\n\t\t\tc.Writeln(\"Subject: Hello!\")\n\t\t\tc.Writeln(\"\")\n\t\t\tc.Writeln(\"Hello!\")\n\t\t\tc.Writeln(\".\")\n\t\t\tc.ExpectPattern(\"250 2.0.0 OK: queued\")\n\t\t\tc.Writeln(\"QUIT\")\n\t\t\tc.ExpectPattern(\"221 *\")\n\t\t})\n\t}\n\n\ttest(\"inline\", `\n\t\t\thostname mx.maddy.test\n\t\t\ttls off\n\n\t\t\tsmtp tcp://127.0.0.1:{env:TEST_PORT_smtp} {\n\t\t\t\tmodify {\n\t\t\t\t\treplace_rcpt static {\n\t\t\t\t\t\tentry a@maddy.test b@maddy.test\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tdestination a@maddy.test {\n\t\t\t\t\treject\n\t\t\t\t}\n\t\t\t\tdestination b@maddy.test {\n\t\t\t\t\tdeliver_to dummy\n\t\t\t\t}\n\t\t\t\tdefault_destination {\n\t\t\t\t\treject\n\t\t\t\t}\n\t\t\t}`)\n\ttest(\"inline qualified\", `\n\t\t\thostname mx.maddy.test\n\t\t\ttls off\n\n\t\t\tsmtp tcp://127.0.0.1:{env:TEST_PORT_smtp} {\n\t\t\t\tmodify {\n\t\t\t\t\tmodify.replace_rcpt static {\n\t\t\t\t\t\tentry a@maddy.test b@maddy.test\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tdestination a@maddy.test {\n\t\t\t\t\treject\n\t\t\t\t}\n\t\t\t\tdestination b@maddy.test {\n\t\t\t\t\tdeliver_to dummy\n\t\t\t\t}\n\t\t\t\tdefault_destination {\n\t\t\t\t\treject\n\t\t\t\t}\n\t\t\t}`)\n\n\t// FIXME: Not implemented\n\t// test(\"external\", `\n\t//\t\thostname mx.maddy.test\n\t//\t\ttls off\n\n\t//\t\tmodify.replace_rcpt local_aliases {\n\t//\t\t\ttable static {\n\t//\t\t\t\tentry a@maddy.test b@maddy.test\n\t//\t\t\t}\n\t//\t\t}\n\n\t//\t\tsmtp tcp://127.0.0.1:{env:TEST_PORT_smtp} {\n\t//\t\t\tmodify {\n\t//\t\t\t\t&local_aliases\n\t//\t\t\t}\n\t//\t\t\tsource a@maddy.test {\n\t//\t\t\t\tdestination a@maddy.test {\n\t//\t\t\t\t\treject\n\t//\t\t\t\t}\n\t//\t\t\t\tdestination b@maddy.test {\n\t//\t\t\t\t\tdeliver_to dummy\n\t//\t\t\t\t}\n\t//\t\t\t\tdefault_destination {\n\t//\t\t\t\t\treject\n\t//\t\t\t\t}\n\t//\t\t\t}\n\t//\t\t\tdefault_source {\n\t//\t\t\t\treject\n\t//\t\t\t}\n\t//\t\t}`)\n}\n\nfunc TestReplaceAddr_Sender(tt *testing.T) {\n\ttest := func(name, cfg string) {\n\t\ttt.Run(name, func(tt *testing.T) {\n\t\t\tt := tests.NewT(tt)\n\t\t\tt.DNS(nil)\n\t\t\tt.Port(\"smtp\")\n\t\t\tt.Config(cfg)\n\t\t\tt.Run(1)\n\t\t\tdefer t.Close()\n\n\t\t\tc := t.Conn(\"smtp\")\n\t\t\tdefer c.Close()\n\t\t\tc.SMTPNegotation(\"client.maddy.test\", nil, nil)\n\t\t\tc.Writeln(\"MAIL FROM:<a@maddy.test>\")\n\t\t\tc.ExpectPattern(\"250 *\")\n\t\t\tc.Writeln(\"RCPT TO:<a@maddy.test>\")\n\t\t\tc.ExpectPattern(\"250 *\")\n\t\t\tc.Writeln(\"DATA\")\n\t\t\tc.ExpectPattern(\"354 *\")\n\t\t\tc.Writeln(\"From: <from@maddy.test>\")\n\t\t\tc.Writeln(\"To: <to@maddy.test>\")\n\t\t\tc.Writeln(\"Subject: Hello!\")\n\t\t\tc.Writeln(\"\")\n\t\t\tc.Writeln(\"Hello!\")\n\t\t\tc.Writeln(\".\")\n\t\t\tc.ExpectPattern(\"250 2.0.0 OK: queued\")\n\t\t\tc.Writeln(\"QUIT\")\n\t\t\tc.ExpectPattern(\"221 *\")\n\t\t})\n\t}\n\n\ttest(\"inline\", `\n\t\t\thostname mx.maddy.test\n\t\t\ttls off\n\n\t\t\tsmtp tcp://127.0.0.1:{env:TEST_PORT_smtp} {\n\t\t\t\tmodify {\n\t\t\t\t\treplace_sender static {\n\t\t\t\t\t\tentry a@maddy.test b@maddy.test\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tsource a@maddy.test {\n\t\t\t\t\treject\n\t\t\t\t}\n\t\t\t\tsource b@maddy.test {\n\t\t\t\t\tdestination a@maddy.test {\n\t\t\t\t\t\tdeliver_to dummy\n\t\t\t\t\t}\n\t\t\t\t\tdefault_destination {\n\t\t\t\t\t\treject\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tdefault_source {\n\t\t\t\t\treject\n\t\t\t\t}\n\t\t\t}`)\n\ttest(\"inline qualified\", `\n\t\t\thostname mx.maddy.test\n\t\t\ttls off\n\n\t\t\tsmtp tcp://127.0.0.1:{env:TEST_PORT_smtp} {\n\t\t\t\tmodify {\n\t\t\t\t\tmodify.replace_sender static {\n\t\t\t\t\t\tentry a@maddy.test b@maddy.test\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tsource a@maddy.test {\n\t\t\t\t\treject\n\t\t\t\t}\n\t\t\t\tsource b@maddy.test {\n\t\t\t\t\tdestination a@maddy.test {\n\t\t\t\t\t\tdeliver_to dummy\n\t\t\t\t\t}\n\t\t\t\t\tdefault_destination {\n\t\t\t\t\t\treject\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tdefault_source {\n\t\t\t\t\treject\n\t\t\t\t}\n\t\t\t}`)\n\t// FIXME: Not implemented\n\t// test(\"external\", `\n\t//\t\thostname mx.maddy.test\n\t//\t\ttls off\n\n\t//\t\tmodify.replace_sender local_aliases {\n\t//\t\t\ttable static {\n\t//\t\t\t\tentry a@maddy.test b@maddy.test\n\t//\t\t\t}\n\t//\t\t}\n\n\t//\t\tsmtp tcp://127.0.0.1:{env:TEST_PORT_smtp} {\n\t//\t\t\tmodify {\n\t//\t\t\t\t&local_aliases\n\t//\t\t\t}\n\t//\t\t\tsource a@maddy.test {\n\t//\t\t\t\treject\n\t//\t\t\t}\n\t//\t\t\tsource b@maddy.test {\n\t//\t\t\t\tdestination a@maddy.test {\n\t//\t\t\t\t\tdeliver_to dummy\n\t//\t\t\t\t}\n\t//\t\t\t\tdefault_destination {\n\t//\t\t\t\t\treject\n\t//\t\t\t\t}\n\t//\t\t\t}\n\t//\t\t\tdefault_source {\n\t//\t\t\t\treject\n\t//\t\t\t}\n\t//\t\t}`)\n}\n"
  },
  {
    "path": "tests/run.sh",
    "content": "#!/bin/sh\n\nset -e\n\nif [ -z \"$GO\" ]; then\n\texport GO=go\nfi\n\n./build_cover.sh\n\nclean() {\n    rm -f /tmp/maddy-coverage-report*\n}\ntrap clean EXIT\n\n$GO test -tags integration -integration.executable ./maddy.cover -integration.coverprofile /tmp/maddy-coverage-report \"$@\"\n$GO run gocovcat.go /tmp/maddy-coverage-report* > coverage.out\n"
  },
  {
    "path": "tests/smtp_autobuffer_test.go",
    "content": "//go:build integration && cgo && !nosqlite3\n// +build integration,cgo,!nosqlite3\n\n/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage tests_test\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/foxcpp/maddy/tests\"\n)\n\nfunc TestSMTPEndpoint_LargeMessage(tt *testing.T) {\n\t// Send 1.44 MiB message to verify it being handled correctly\n\t// everywhere.\n\t// (Issue 389)\n\n\ttt.Parallel()\n\tt := tests.NewT(tt)\n\tt.DNS(nil)\n\tt.Port(\"imap\")\n\tt.Port(\"smtp\")\n\tt.Config(`\n\t\tstorage.imapsql test_store {\n\t\t\tdriver sqlite3\n\t\t\tdsn imapsql.db\n\t\t}\n\n\t\timap tcp://127.0.0.1:{env:TEST_PORT_imap} {\n\t\t\ttls off\n\n\t\t\tauth dummy\n\t\t\tstorage &test_store\n\t\t}\n\n\t\tsmtp tcp://127.0.0.1:{env:TEST_PORT_smtp} {\n\t\t\thostname maddy.test\n\t\t\ttls off\n\n\t\t\tdeliver_to &test_store\n\t\t}\n\t`)\n\tt.Run(2)\n\tdefer t.Close()\n\n\timapConn := t.Conn(\"imap\")\n\tdefer imapConn.Close()\n\timapConn.ExpectPattern(`\\* OK *`)\n\timapConn.Writeln(\". LOGIN testusr@maddy.test 1234\")\n\timapConn.ExpectPattern(\". OK *\")\n\timapConn.Writeln(\". SELECT INBOX\")\n\timapConn.ExpectPattern(`\\* *`)\n\timapConn.ExpectPattern(`\\* *`)\n\timapConn.ExpectPattern(`\\* *`)\n\timapConn.ExpectPattern(`\\* *`)\n\timapConn.ExpectPattern(`\\* *`)\n\timapConn.ExpectPattern(`\\* *`)\n\timapConn.ExpectPattern(`. OK *`)\n\n\tsmtpConn := t.Conn(\"smtp\")\n\tdefer smtpConn.Close()\n\tsmtpConn.SMTPNegotation(\"localhost\", nil, nil)\n\tsmtpConn.Writeln(\"MAIL FROM:<sender@maddy.test>\")\n\tsmtpConn.ExpectPattern(\"2*\")\n\tsmtpConn.Writeln(\"RCPT TO:<testusr@maddy.test>\")\n\tsmtpConn.ExpectPattern(\"2*\")\n\tsmtpConn.Writeln(\"DATA\")\n\tsmtpConn.ExpectPattern(\"354 *\")\n\tsmtpConn.Writeln(\"From: <sender@maddy.test>\")\n\tsmtpConn.Writeln(\"To: <testusr@maddy.test>\")\n\tsmtpConn.Writeln(\"Subject: Hi!\")\n\tsmtpConn.Writeln(\"\")\n\tfor i := 0; i < 3000; i++ {\n\t\tsmtpConn.Writeln(strings.Repeat(\"A\", 500))\n\t}\n\t// 3000*502 ~ 1.44 MiB not including header side\n\tsmtpConn.Writeln(\".\")\n\n\ttime.Sleep(500 * time.Millisecond)\n\n\timapConn.Writeln(\". NOOP\")\n\timapConn.ExpectPattern(`\\* 1 EXISTS`)\n\timapConn.ExpectPattern(`\\* 1 RECENT`)\n\timapConn.ExpectPattern(\". OK *\")\n\n\timapConn.Writeln(\". FETCH 1 (BODY.PEEK[])\")\n\timapConn.ExpectPattern(`\\* 1 FETCH (BODY\\[\\] {1506312}*`)\n\timapConn.Expect(`Delivered-To: testusr@maddy.test`)\n\timapConn.Expect(`Return-Path: <sender@maddy.test>`)\n\timapConn.ExpectPattern(`Received: from localhost (client.maddy.test \\[` + tests.DefaultSourceIP.String() + `\\]) by maddy.test`)\n\timapConn.ExpectPattern(` (envelope-sender <sender@maddy.test>) with ESMTP id *; *`)\n\timapConn.ExpectPattern(` *`)\n\timapConn.Expect(\"From: <sender@maddy.test>\")\n\timapConn.Expect(\"To: <testusr@maddy.test>\")\n\timapConn.Expect(\"Subject: Hi!\")\n\timapConn.Expect(\"\")\n\tfor i := 0; i < 3000; i++ {\n\t\timapConn.Expect(strings.Repeat(\"A\", 500))\n\t}\n\timapConn.Expect(\")\")\n\timapConn.ExpectPattern(`. OK *`)\n}\n\nfunc TestSMTPEndpoint_FileBuffer(tt *testing.T) {\n\trun := func(tt *testing.T, bufferOpt string) {\n\t\ttt.Parallel()\n\t\tt := tests.NewT(tt)\n\n\t\tt.DNS(nil)\n\t\tt.Port(\"imap\")\n\t\tt.Port(\"smtp\")\n\t\tt.Config(`\n\t\tstorage.imapsql test_store {\n\t\t\tdriver sqlite3\n\t\t\tdsn imapsql.db\n\t\t}\n\n\t\timap tcp://127.0.0.1:{env:TEST_PORT_imap} {\n\t\t\ttls off\n\n\t\t\tauth dummy\n\t\t\tstorage &test_store\n\t\t}\n\n\t\tsmtp tcp://127.0.0.1:{env:TEST_PORT_smtp} {\n\t\t\thostname maddy.test\n\t\t\ttls off\n\t\t\tbuffer ` + bufferOpt + `\n\n\t\t\tdeliver_to &test_store\n\t\t}\n\t`)\n\t\tt.Run(2)\n\t\tdefer t.Close()\n\n\t\timapConn := t.Conn(\"imap\")\n\t\tdefer imapConn.Close()\n\t\timapConn.ExpectPattern(`\\* OK *`)\n\t\timapConn.Writeln(\". LOGIN testusr@maddy.test 1234\")\n\t\timapConn.ExpectPattern(\". OK *\")\n\t\timapConn.Writeln(\". SELECT INBOX\")\n\t\timapConn.ExpectPattern(`\\* *`)\n\t\timapConn.ExpectPattern(`\\* *`)\n\t\timapConn.ExpectPattern(`\\* *`)\n\t\timapConn.ExpectPattern(`\\* *`)\n\t\timapConn.ExpectPattern(`\\* *`)\n\t\timapConn.ExpectPattern(`\\* *`)\n\t\timapConn.ExpectPattern(`. OK *`)\n\n\t\tsmtpConn := t.Conn(\"smtp\")\n\t\tdefer smtpConn.Close()\n\t\tsmtpConn.SMTPNegotation(\"localhost\", nil, nil)\n\t\tsmtpConn.Writeln(\"MAIL FROM:<sender@maddy.test>\")\n\t\tsmtpConn.ExpectPattern(\"2*\")\n\t\tsmtpConn.Writeln(\"RCPT TO:<testusr@maddy.test>\")\n\t\tsmtpConn.ExpectPattern(\"2*\")\n\t\tsmtpConn.Writeln(\"DATA\")\n\t\tsmtpConn.ExpectPattern(\"354 *\")\n\t\tsmtpConn.Writeln(\"From: <sender@maddy.test>\")\n\t\tsmtpConn.Writeln(\"To: <testusr@maddy.test>\")\n\t\tsmtpConn.Writeln(\"Subject: Hi!\")\n\t\tsmtpConn.Writeln(\"\")\n\t\tsmtpConn.Writeln(\"AAAAABBBBBB\")\n\t\tsmtpConn.Writeln(\".\")\n\t\tsmtpConn.ExpectPattern(\"2*\")\n\n\t\ttime.Sleep(500 * time.Millisecond)\n\n\t\timapConn.Writeln(\". NOOP\")\n\t\timapConn.ExpectPattern(`\\* 1 EXISTS`)\n\t\timapConn.ExpectPattern(`\\* 1 RECENT`)\n\t\timapConn.ExpectPattern(\". OK *\")\n\n\t\timapConn.Writeln(\". FETCH 1 (BODY.PEEK[])\")\n\t\timapConn.ExpectPattern(`\\* 1 FETCH (BODY\\[\\] {*}*`)\n\t\timapConn.Expect(`Delivered-To: testusr@maddy.test`)\n\t\timapConn.Expect(`Return-Path: <sender@maddy.test>`)\n\t\timapConn.ExpectPattern(`Received: from localhost (client.maddy.test \\[` + tests.DefaultSourceIP.String() + `\\]) by maddy.test`)\n\t\timapConn.ExpectPattern(` (envelope-sender <sender@maddy.test>) with ESMTP id *; *`)\n\t\timapConn.ExpectPattern(` *`)\n\t\timapConn.Expect(\"From: <sender@maddy.test>\")\n\t\timapConn.Expect(\"To: <testusr@maddy.test>\")\n\t\timapConn.Expect(\"Subject: Hi!\")\n\t\timapConn.Expect(\"\")\n\t\timapConn.Expect(\"AAAAABBBBBB\")\n\t\timapConn.Expect(\")\")\n\t\timapConn.ExpectPattern(`. OK *`)\n\t}\n\n\ttt.Run(\"ram\", func(tt *testing.T) { run(tt, \"ram\") })\n\ttt.Run(\"fs\", func(tt *testing.T) { run(tt, \"fs\") })\n}\n\nfunc TestSMTPEndpoint_Autobuffer(tt *testing.T) {\n\ttt.Parallel()\n\tt := tests.NewT(tt)\n\n\tt.DNS(nil)\n\tt.Port(\"imap\")\n\tt.Port(\"smtp\")\n\tt.Config(`\n\t\tstorage.imapsql test_store {\n\t\t\tdriver sqlite3\n\t\t\tdsn imapsql.db\n\t\t}\n\n\t\timap tcp://127.0.0.1:{env:TEST_PORT_imap} {\n\t\t\ttls off\n\n\t\t\tauth dummy\n\t\t\tstorage &test_store\n\t\t}\n\n\t\tsmtp tcp://127.0.0.1:{env:TEST_PORT_smtp} {\n\t\t\thostname maddy.test\n\t\t\ttls off\n\t\t\tbuffer auto 5b\n\n\t\t\tdeliver_to &test_store\n\t\t}\n\t`)\n\tt.Run(2)\n\tdefer t.Close()\n\n\timapConn := t.Conn(\"imap\")\n\tdefer imapConn.Close()\n\timapConn.ExpectPattern(`\\* OK *`)\n\timapConn.Writeln(\". LOGIN testusr@maddy.test 1234\")\n\timapConn.ExpectPattern(\". OK *\")\n\timapConn.Writeln(\". SELECT INBOX\")\n\timapConn.ExpectPattern(`\\* *`)\n\timapConn.ExpectPattern(`\\* *`)\n\timapConn.ExpectPattern(`\\* *`)\n\timapConn.ExpectPattern(`\\* *`)\n\timapConn.ExpectPattern(`\\* *`)\n\timapConn.ExpectPattern(`\\* *`)\n\timapConn.ExpectPattern(`. OK *`)\n\n\tsmtpConn := t.Conn(\"smtp\")\n\tdefer smtpConn.Close()\n\tsmtpConn.SMTPNegotation(\"localhost\", nil, nil)\n\tsmtpConn.Writeln(\"MAIL FROM:<sender@maddy.test>\")\n\tsmtpConn.ExpectPattern(\"2*\")\n\tsmtpConn.Writeln(\"RCPT TO:<testusr@maddy.test>\")\n\tsmtpConn.ExpectPattern(\"2*\")\n\tsmtpConn.Writeln(\"DATA\")\n\tsmtpConn.ExpectPattern(\"354 *\")\n\tsmtpConn.Writeln(\"From: <sender@maddy.test>\")\n\tsmtpConn.Writeln(\"To: <testusr@maddy.test>\")\n\tsmtpConn.Writeln(\"Subject: Hi!\")\n\tsmtpConn.Writeln(\"\")\n\tsmtpConn.Writeln(\"AAAAABBBBBB\")\n\tsmtpConn.Writeln(\".\")\n\tsmtpConn.ExpectPattern(\"2*\")\n\n\tsmtpConn.Writeln(\"MAIL FROM:<sender@maddy.test>\")\n\tsmtpConn.ExpectPattern(\"2*\")\n\tsmtpConn.Writeln(\"RCPT TO:<testusr@maddy.test>\")\n\tsmtpConn.ExpectPattern(\"2*\")\n\tsmtpConn.Writeln(\"DATA\")\n\tsmtpConn.ExpectPattern(\"354 *\")\n\tsmtpConn.Writeln(\"From: <sender@maddy.test>\")\n\tsmtpConn.Writeln(\"To: <testusr@maddy.test>\")\n\tsmtpConn.Writeln(\"Subject: Hi!\")\n\tsmtpConn.Writeln(\"\")\n\tsmtpConn.Writeln(\".\")\n\tsmtpConn.ExpectPattern(\"2*\")\n\n\tsmtpConn.Writeln(\"MAIL FROM:<sender@maddy.test>\")\n\tsmtpConn.ExpectPattern(\"2*\")\n\tsmtpConn.Writeln(\"RCPT TO:<testusr@maddy.test>\")\n\tsmtpConn.ExpectPattern(\"2*\")\n\tsmtpConn.Writeln(\"DATA\")\n\tsmtpConn.ExpectPattern(\"354 *\")\n\tsmtpConn.Writeln(\"From: <sender@maddy.test>\")\n\tsmtpConn.Writeln(\"To: <testusr@maddy.test>\")\n\tsmtpConn.Writeln(\"Subject: Hi!\")\n\tsmtpConn.Writeln(\"\")\n\tsmtpConn.Writeln(\"AAA\")\n\tsmtpConn.Writeln(\".\")\n\tsmtpConn.ExpectPattern(\"2*\")\n\n\ttime.Sleep(500 * time.Millisecond)\n\n\timapConn.Writeln(\". NOOP\")\n\t// This will break with go-imap v2 upgrade merging updates.\n\timapConn.ExpectPattern(`\\* 3 EXISTS`)\n\timapConn.ExpectPattern(`\\* 3 RECENT`)\n\timapConn.ExpectPattern(\". OK *\")\n\n\timapConn.Writeln(\". FETCH 1:3 (BODY.PEEK[])\")\n\timapConn.ExpectPattern(`\\* 1 FETCH (BODY\\[\\] {*}*`)\n\timapConn.Expect(`Delivered-To: testusr@maddy.test`)\n\timapConn.Expect(`Return-Path: <sender@maddy.test>`)\n\timapConn.ExpectPattern(`Received: from localhost (client.maddy.test \\[` + tests.DefaultSourceIP.String() + `\\]) by maddy.test`)\n\timapConn.ExpectPattern(` (envelope-sender <sender@maddy.test>) with ESMTP id *; *`)\n\timapConn.ExpectPattern(` *`)\n\timapConn.Expect(\"From: <sender@maddy.test>\")\n\timapConn.Expect(\"To: <testusr@maddy.test>\")\n\timapConn.Expect(\"Subject: Hi!\")\n\timapConn.Expect(\"\")\n\timapConn.Expect(\"AAAAABBBBBB\")\n\timapConn.Expect(\")\")\n\timapConn.ExpectPattern(`\\* 2 FETCH (BODY\\[\\] {*}*`)\n\timapConn.Expect(`Delivered-To: testusr@maddy.test`)\n\timapConn.Expect(`Return-Path: <sender@maddy.test>`)\n\timapConn.ExpectPattern(`Received: from localhost (client.maddy.test \\[` + tests.DefaultSourceIP.String() + `\\]) by maddy.test`)\n\timapConn.ExpectPattern(` (envelope-sender <sender@maddy.test>) with ESMTP id *; *`)\n\timapConn.ExpectPattern(` *`)\n\timapConn.Expect(\"From: <sender@maddy.test>\")\n\timapConn.Expect(\"To: <testusr@maddy.test>\")\n\timapConn.Expect(\"Subject: Hi!\")\n\timapConn.Expect(\"\")\n\timapConn.Expect(\")\")\n\timapConn.ExpectPattern(`\\* 3 FETCH (BODY\\[\\] {*}*`)\n\timapConn.Expect(`Delivered-To: testusr@maddy.test`)\n\timapConn.Expect(`Return-Path: <sender@maddy.test>`)\n\timapConn.ExpectPattern(`Received: from localhost (client.maddy.test \\[` + tests.DefaultSourceIP.String() + `\\]) by maddy.test`)\n\timapConn.ExpectPattern(` (envelope-sender <sender@maddy.test>) with ESMTP id *; *`)\n\timapConn.ExpectPattern(` *`)\n\timapConn.Expect(\"From: <sender@maddy.test>\")\n\timapConn.Expect(\"To: <testusr@maddy.test>\")\n\timapConn.Expect(\"Subject: Hi!\")\n\timapConn.Expect(\"\")\n\timapConn.Expect(\"AAA\")\n\timapConn.Expect(\")\")\n\timapConn.ExpectPattern(`. OK *`)\n}\n"
  },
  {
    "path": "tests/smtp_test.go",
    "content": "//go:build integration\n// +build integration\n\n/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage tests_test\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/foxcpp/go-mockdns\"\n\t\"github.com/foxcpp/maddy/tests\"\n)\n\nfunc TestCheckRequireTLS(tt *testing.T) {\n\ttt.Parallel()\n\tt := tests.NewT(tt)\n\tt.DNS(nil)\n\tt.Port(\"smtp\")\n\tt.Config(`\n\t\tsmtp tcp://127.0.0.1:{env:TEST_PORT_smtp} {\n\t\t\thostname mx.maddy.test\n\t\t\ttls self_signed\n\n\t\t\tdefer_sender_reject no\n\n\t\t\tcheck {\n\t\t\t\trequire_tls\n\t\t\t}\n\t\t\tdeliver_to dummy\n\t\t}\n\t`)\n\tt.Run(1)\n\tdefer t.Close()\n\n\tconn := t.Conn(\"smtp\")\n\tdefer conn.Close()\n\tconn.SMTPNegotation(\"localhost\", nil, nil)\n\tconn.Writeln(\"MAIL FROM:<testing@two.maddy.test>\")\n\tconn.ExpectPattern(\"550 5.7.1 *\")\n\tconn.Writeln(\"STARTTLS\")\n\tconn.ExpectPattern(\"220 *\")\n\tconn.TLS()\n\tconn.SMTPNegotation(\"localhost\", nil, nil)\n\tconn.Writeln(\"MAIL FROM:<testing@two.maddy.test>\")\n\tconn.ExpectPattern(\"250 *\")\n\tconn.Writeln(\"QUIT\")\n\tconn.ExpectPattern(\"221 *\")\n}\n\nfunc TestProxyProtocolTrustedSource(tt *testing.T) {\n\ttt.Parallel()\n\tt := tests.NewT(tt)\n\tt.DNS(map[string]mockdns.Zone{\n\t\t\"one.maddy.test.\": {\n\t\t\tTXT: []string{\"v=spf1 ip4:127.0.0.17 -all\"},\n\t\t},\n\t})\n\tt.Port(\"smtp\")\n\tt.Config(`\n\t\tsmtp tcp://127.0.0.1:{env:TEST_PORT_smtp} {\n\t\t\thostname mx.maddy.test\n\t\t\ttls off\n\n\t\t\tproxy_protocol {\n\t\t\t\ttrust ` + tests.DefaultSourceIP.String() + ` ::1/128\n\t\t\t\ttls off\n\t\t\t}\n\n\t\t\tdefer_sender_reject no\n\n\t\t\tcheck {\n\t\t\t\tspf {\n\t\t\t\t\tenforce_early yes\n\t\t\t\t\tfail_action reject\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tdeliver_to dummy\n\t\t}\n\t`)\n\tt.Run(1)\n\tdefer t.Close()\n\n\tconn := t.Conn(\"smtp\")\n\tdefer conn.Close()\n\tconn.Writeln(fmt.Sprintf(\"PROXY TCP4 127.0.0.17 %s 12345 %d\", tests.DefaultSourceIP.String(), t.Port(\"smtp\")))\n\tconn.SMTPNegotation(\"localhost\", nil, nil)\n\tconn.Writeln(\"MAIL FROM:<testing@one.maddy.test>\")\n\tconn.ExpectPattern(\"250 *\")\n\tconn.Writeln(\"QUIT\")\n\tconn.ExpectPattern(\"221 *\")\n}\n\nfunc TestProxyProtocolUntrustedSource(tt *testing.T) {\n\ttt.Parallel()\n\tt := tests.NewT(tt)\n\tt.DNS(map[string]mockdns.Zone{\n\t\t\"one.maddy.test.\": {\n\t\t\tTXT: []string{\"v=spf1 ip4:127.0.0.17 -all\"},\n\t\t},\n\t})\n\tt.Port(\"smtp\")\n\tt.Config(`\n\t\tsmtp tcp://127.0.0.1:{env:TEST_PORT_smtp} {\n\t\t\thostname mx.maddy.test\n\t\t\ttls off\n\n\t\t\tproxy_protocol {\n\t\t\t\ttrust fe80::bad/128\n\t\t\t\ttls off\n\t\t\t}\n\n\t\t\tdefer_sender_reject no\n\n\t\t\tcheck {\n\t\t\t\tspf {\n\t\t\t\t\tenforce_early yes\n\t\t\t\t\tfail_action reject\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tdeliver_to dummy\n\t\t}\n\t`)\n\tt.Run(1)\n\tdefer t.Close()\n\n\tconn := t.Conn(\"smtp\")\n\tdefer conn.Close()\n\tconn.Writeln(fmt.Sprintf(\"PROXY TCP4 127.0.0.17 %s 12345 %d\", tests.DefaultSourceIP.String(), t.Port(\"smtp\")))\n\tconn.SMTPNegotation(\"localhost\", nil, nil)\n\tconn.Writeln(\"MAIL FROM:<testing@one.maddy.test>\")\n\tconn.ExpectPattern(\"550 *\")\n\tconn.Writeln(\"QUIT\")\n\tconn.ExpectPattern(\"221 *\")\n}\n\nfunc TestCheckSPF(tt *testing.T) {\n\ttt.Parallel()\n\tt := tests.NewT(tt)\n\tt.DNS(map[string]mockdns.Zone{\n\t\t\"none.maddy.test.\": {\n\t\t\tTXT: []string{},\n\t\t},\n\t\t\"pass.maddy.test.\": {\n\t\t\tTXT: []string{\"v=spf1 +all\"},\n\t\t},\n\t\t\"neutral.maddy.test.\": {\n\t\t\tTXT: []string{\"v=spf1 ?all\"},\n\t\t},\n\t\t\"fail.maddy.test.\": {\n\t\t\tTXT: []string{\"v=spf1 -all\"},\n\t\t},\n\t\t\"softfail.maddy.test.\": {\n\t\t\tTXT: []string{\"v=spf1 ~all\"},\n\t\t},\n\t\t\"permerr.maddy.test.\": {\n\t\t\tTXT: []string{\"v=spf1 something_clever\"},\n\t\t},\n\t\t\"temperr.maddy.test.\": {\n\t\t\tErr: errors.New(\"IANA forgot to resign the root zone\"),\n\t\t},\n\t})\n\tt.Port(\"smtp\")\n\tt.Config(`\n\t\tsmtp tcp://127.0.0.1:{env:TEST_PORT_smtp} {\n\t\t\thostname mx.maddy.test\n\t\t\ttls off\n\n\t\t\tdefer_sender_reject no\n\n\t\t\tcheck {\n\t\t\t\tspf {\n\t\t\t\t\tenforce_early yes\n\n\t\t\t\t\tnone_action reject 551\n\t\t\t\t\tneutral_action reject\n\t\t\t\t\tfail_action reject 552\n\t\t\t\t\tsoftfail_action reject 553\n\t\t\t\t\tpermerr_action reject 554\n\t\t\t\t\ttemperr_action reject 455\n\t\t\t\t}\n\t\t\t}\n\t\t\tdeliver_to dummy\n\t\t}\n\t`)\n\tt.Run(1)\n\tdefer t.Close()\n\n\tconn := t.Conn(\"smtp\")\n\tdefer conn.Close()\n\tconn.SMTPNegotation(\"fail.maddy.test\", nil, nil)\n\n\tconn.Writeln(\"MAIL FROM:<testing@pass.maddy.test>\")\n\tconn.ExpectPattern(\"250 *\")\n\tconn.Writeln(\"RSET\")\n\tconn.ExpectPattern(\"250 *\")\n\n\t// Actually checks fail.maddy.test.\n\tconn.Writeln(\"MAIL FROM:<>\")\n\tconn.ExpectPattern(\"552 5.7.0 *\")\n\n\tconn.SMTPNegotation(\"pass.maddy.test\", nil, nil)\n\n\tconn.Writeln(\"MAIL FROM:<>\")\n\tconn.ExpectPattern(\"250 *\")\n\n\tconn.Writeln(\"MAIL FROM:<testing@none.maddy.test>\")\n\tconn.ExpectPattern(\"551 5.7.0 *\")\n\n\t// Also check the default enhanced code is meaningful.\n\tconn.Writeln(\"MAIL FROM:<testing@neutral.maddy.test>\")\n\tconn.ExpectPattern(\"550 5.7.23 *\")\n\n\tconn.Writeln(\"MAIL FROM:<testing@fail.maddy.test>\")\n\tconn.ExpectPattern(\"552 5.7.0 *\")\n\n\tconn.Writeln(\"MAIL FROM:<testing@softfail.maddy.test>\")\n\tconn.ExpectPattern(\"553 5.7.0 *\")\n\n\tconn.Writeln(\"MAIL FROM:<testing@permerr.maddy.test>\")\n\tconn.ExpectPattern(\"554 5.7.0 *\")\n\n\tconn.Writeln(\"MAIL FROM:<testing@temperr.maddy.test>\")\n\tconn.ExpectPattern(\"455 4.7.0 *\")\n\n\tconn.Writeln(\"QUIT\")\n\tconn.ExpectPattern(\"221 *\")\n}\n\nfunc TestSPF_DMARCDefer(tt *testing.T) {\n\ttt.Parallel()\n\tt := tests.NewT(tt)\n\tt.DNS(map[string]mockdns.Zone{\n\t\t\"subdomain.maddy-dmarc.test.\": {\n\t\t\tTXT: []string{\"v=spf1 -all\"},\n\t\t},\n\t\t\"maddy-dmarc.test.\": {\n\t\t\tTXT: []string{\"v=spf1 -all\"},\n\t\t},\n\t\t\"_dmarc.maddy-dmarc.test.\": {\n\t\t\tTXT: []string{\"v=DMARC1; p=reject; sp=none\"},\n\t\t},\n\t\t\"subdomain.maddy-dmarc2.test.\": {\n\t\t\tTXT: []string{\"v=spf1 -all\"},\n\t\t},\n\t\t\"maddy-dmarc2.test.\": {\n\t\t\tTXT: []string{\"v=spf1 -all\"},\n\t\t},\n\t\t\"_dmarc.maddy-dmarc2.test.\": {\n\t\t\tTXT: []string{\"v=DMARC1; p=reject\"},\n\t\t},\n\t\t\"maddy-no-dmarc.test.\": {\n\t\t\tTXT: []string{\"v=spf1 -all\"},\n\t\t},\n\t\t\"maddy-dmarc-lookup-fail.test.\": {\n\t\t\tTXT: []string{\"v=spf1 -all\"},\n\t\t},\n\t\t\"_dmarc.maddy-dmarc-lookup-fail.test.\": {\n\t\t\tErr: errors.New(\"nop\"),\n\t\t},\n\t})\n\tt.Port(\"smtp\")\n\tt.Config(`\n\t\tsmtp tcp://127.0.0.1:{env:TEST_PORT_smtp} {\n\t\t\thostname mx.maddy.test\n\t\t\ttls off\n\n\t\t\tdefer_sender_reject no\n\n\t\t\tcheck {\n\t\t\t\tspf {\n\t\t\t\t\tenforce_early no\n\n\t\t\t\t\tnone_action ignore\n\t\t\t\t\tneutral_action reject\n\t\t\t\t\tfail_action reject\n\t\t\t\t\tsoftfail_action reject\n\t\t\t\t\tpermerr_action reject\n\t\t\t\t\ttemperr_action reject\n\t\t\t\t}\n\t\t\t}\n\t\t\tdeliver_to dummy\n\t\t}\n\t`)\n\tt.Run(1)\n\tdefer t.Close()\n\n\tconn := t.Conn(\"smtp\")\n\tdefer conn.Close()\n\tconn.SMTPNegotation(\"localhost\", nil, nil)\n\n\tmsg := func(fromEnv, fromHdr, bodyRespPattern string) {\n\t\ttt.Helper()\n\n\t\tconn.Writeln(\"MAIL FROM:<\" + fromEnv + \">\")\n\t\tconn.ExpectPattern(\"250 *\")\n\t\tconn.Writeln(\"RCPT TO:<testing@maddy.test>\")\n\t\tconn.ExpectPattern(\"250 *\")\n\t\tconn.Writeln(\"DATA\")\n\t\tconn.ExpectPattern(\"354 *\")\n\t\tconn.Writeln(\"From: <\" + fromHdr + \">\")\n\t\tconn.Writeln(\"\")\n\t\tconn.Writeln(\"Heya!\")\n\t\tconn.Writeln(\".\")\n\t\tconn.ExpectPattern(bodyRespPattern)\n\t}\n\n\tmsg(\"test@subdomain.maddy-dmarc.test\", \"test@subdomain.maddy-dmarc.test\", \"550 *\")\n\n\t// Malformed From domain, DMARC cannot work so use only SPF.\n\tmsg(\"test@subdomain.maddy-dmarc.test\", \"\", \"550 *\")\n\n\tmsg(\"test@subdomain.maddy-dmarc.test\", \"maddy-dmarc-lookup-fail.test\", \"550 *\")\n\n\t// No actual DMARC check is done but SPF check results are not applied.\n\tmsg(\"test@maddy-dmarc.test\", \"test@maddy-dmarc.test\", \"250 *\")\n\tmsg(\"test@maddy-dmarc2.test\", \"test@maddy-dmarc2.test\", \"250 *\")\n\n\tmsg(\"test@maddy-no-dmarc.test\", \"test@maddy-no-dmarc.test\", \"550 *\")\n\n\tconn.Writeln(\"QUIT\")\n\tconn.ExpectPattern(\"221 *\")\n}\n\nfunc TestDNSBLConfig(tt *testing.T) {\n\ttt.Parallel()\n\tt := tests.NewT(tt)\n\tt.DNS(map[string]mockdns.Zone{\n\t\ttests.DefaultSourceIPRev + \".dnsbl.test.\": {\n\t\t\tA: []string{\"127.0.0.127\"},\n\t\t},\n\t\t\"sender.test.dnsbl.test.\": {\n\t\t\tA: []string{\"127.0.0.127\"},\n\t\t},\n\t})\n\tt.Port(\"smtp\")\n\tt.Config(`\n\t\tsmtp tcp://127.0.0.1:{env:TEST_PORT_smtp} {\n\t\t\thostname mx.maddy.test\n\t\t\ttls off\n\n\t\t\tdefer_sender_reject no\n\n\t\t\tcheck {\n\t\t\t\tdnsbl {\n\t\t\t\t\treject_threshold 1\n\n\t\t\t\t\tdnsbl.test {\n\t\t\t\t\t\tclient_ipv4\n\t\t\t\t\t\tmailfrom\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tdeliver_to dummy\n\t\t}\n\t`)\n\tt.Run(1)\n\tdefer t.Close()\n\n\tconn := t.Conn(\"smtp\")\n\tdefer conn.Close()\n\tconn.SMTPNegotation(\"localhost\", nil, nil)\n\n\tconn.Writeln(\"MAIL FROM:<testing@sender.test>\")\n\tconn.ExpectPattern(\"554 5.7.0 Client identity is listed in the used DNSBL *\")\n\n\tconn.Writeln(\"MAIL FROM:<testing@misc.test>\")\n\tconn.ExpectPattern(\"554 5.7.0 Client identity is listed in the used DNSBL *\")\n\n\tconn.Writeln(\"QUIT\")\n\tconn.ExpectPattern(\"221 *\")\n}\n\nfunc TestDNSBLConfig2(tt *testing.T) {\n\ttt.Parallel()\n\tt := tests.NewT(tt)\n\tt.DNS(map[string]mockdns.Zone{\n\t\ttests.DefaultSourceIPRev + \".dnsbl2.test.\": {\n\t\t\tA: []string{\"127.0.0.127\"},\n\t\t},\n\t\t\"sender.test.dnsbl.test.\": {\n\t\t\tA: []string{\"127.0.0.127\"},\n\t\t},\n\t})\n\tt.Port(\"smtp\")\n\tt.Config(`\n\t\tsmtp tcp://127.0.0.1:{env:TEST_PORT_smtp} {\n\t\t\thostname mx.maddy.test\n\t\t\ttls off\n\n\t\t\tdefer_sender_reject no\n\n\t\t\tcheck {\n\t\t\t\tdnsbl {\n\t\t\t\t\treject_threshold 1\n\n\t\t\t\t\tdnsbl.test {\n\t\t\t\t\t\tmailfrom\n\t\t\t\t\t}\n\t\t\t\t\tdnsbl2.test {\n\t\t\t\t\t\tclient_ipv4\n\t\t\t\t\t\tscore -1\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tdeliver_to dummy\n\t\t}\n\t`)\n\tt.Run(1)\n\tdefer t.Close()\n\n\tconn := t.Conn(\"smtp\")\n\tdefer conn.Close()\n\tconn.SMTPNegotation(\"localhost\", nil, nil)\n\n\tconn.Writeln(\"MAIL FROM:<testing@sender.test>\")\n\tconn.ExpectPattern(\"250 *\")\n\n\tconn.Writeln(\"QUIT\")\n\tconn.ExpectPattern(\"221 *\")\n}\n\nfunc TestCheckAuthorizeSender(tt *testing.T) {\n\ttt.Parallel()\n\tt := tests.NewT(tt)\n\tt.DNS(nil)\n\tt.Port(\"smtp\")\n\tt.Config(`\n\t\tsmtp tcp://127.0.0.1:{env:TEST_PORT_smtp} {\n\t\t\thostname mx.maddy.test\n\t\t\ttls off\n\n\t\t\tauth dummy\n\t\t\tdefer_sender_reject off\n\n\t\t\tsource example1.org {\n\t\t\t\tcheck {\n\t\t\t\t\tauthorize_sender {\n\t\t\t\t\t\tauth_normalize precis_casefold\n\t\t\t\t\t\tuser_to_email static {\n\t\t\t\t\t\t\tentry \"test-user1\" \"test@example1.org\"\n\t\t\t\t\t\t\tentry \"test-user2\" \"é@example1.org\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tdeliver_to dummy\n\t\t\t}\n\t\t\tsource example2.org {\n\t\t\t\tcheck {\n\t\t\t\t\tauthorize_sender {\n\t\t\t\t\t\tauth_normalize precis_casefold\n\t\t\t\t\t\tprepare_email static {\n\t\t\t\t\t\t\tentry \"alias-to-test@example2.org\" \"test@example2.org\"\n\t\t\t\t\t\t}\n\t\t\t\t\t\tuser_to_email static {\n\t\t\t\t\t\t\tentry \"test-user1\" \"test@example2.org\"\n\t\t\t\t\t\t\tentry \"test-user2\" \"test2@example2.org\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tdeliver_to dummy\n\t\t\t}\n\n\t\t\tdefault_source {\n\t\t\t\treject\n\t\t\t}\n\t\t}`)\n\tt.Run(1)\n\tdefer t.Close()\n\n\tc := t.Conn(\"smtp\")\n\tc.SMTPNegotation(\"client.maddy.test\", nil, nil)\n\tc.SMTPPlainAuth(\"test-user2\", \"1\", true)\n\tc.Writeln(\"MAIL FROM:<test@example1.org>\")\n\tc.ExpectPattern(\"5*\") // rejected - user is not test-user1\n\tc.Writeln(\"MAIL FROM:<test3@example1.org>\")\n\tc.ExpectPattern(\"5*\") // rejected - unknown email\n\tc.Writeln(\"MAIL FROM:<E\\u0301@EXAMPLE1.org> SMTPUTF8\")\n\tc.ExpectPattern(\"2*\") // OK - é@example1.org belongs to test-user2\n\tc.Close()\n\n\tc = t.Conn(\"smtp\")\n\tc.SMTPNegotation(\"client.maddy.test\", nil, nil)\n\tc.SMTPPlainAuth(\"test-user1\", \"1\", true)\n\tc.Writeln(\"MAIL FROM:<test2@example2.org>\")\n\tc.ExpectPattern(\"5*\") // rejected - user is not test-user2\n\tc.Writeln(\"MAIL FROM:<test@example2.org>\")\n\tc.ExpectPattern(\"2*\") // OK - test@example2.org belongs to test-user\n\tc.Writeln(\"MAIL FROM:<alias-to-test@example2.org>\")\n\tc.ExpectPattern(\"2*\") // OK - test@example2.org belongs to test-user\n\tc.Close()\n}\n\nfunc TestCheckCommand(tt *testing.T) {\n\ttt.Parallel()\n\tt := tests.NewT(tt)\n\tt.DNS(nil)\n\tt.Port(\"smtp\")\n\tt.Config(`\n\t\tsmtp tcp://127.0.0.1:{env:TEST_PORT_smtp} {\n\t\t\thostname mx.maddy.test\n\t\t\ttls off\n\n\t\t\tcheck {\n\t\t\t\tcommand {env:TEST_PWD}/testdata/check_command.sh {sender} {\n\t\t\t\t\tcode 12 reject\n\t\t\t\t}\n\t\t\t}\n\t\t\tdeliver_to dummy\n\t\t}\n\t`)\n\tt.Run(1)\n\tdefer t.Close()\n\n\tconn := t.Conn(\"smtp\")\n\tdefer conn.Close()\n\tconn.SMTPNegotation(\"localhost\", nil, nil)\n\n\t// Note: Internally, messages are handled using LF line endings, being\n\t// converted CRLF only when transfered over Internet protocols.\n\texpectedMsg := \"From: <testing@sender.test>\\n\" +\n\t\t\"To: <testing@maddy.test>\\n\" +\n\t\t\"Subject: Hi there!\\n\" +\n\t\t\"\\n\" +\n\t\t\"Nice to meet you!\\n\"\n\tsubmitMsg := func(conn *tests.Conn, from string) {\n\t\t// Fairly trivial SMTP transaction.\n\t\tconn.Writeln(\"MAIL FROM:<\" + from + \">\")\n\t\tconn.ExpectPattern(\"250 *\")\n\t\tconn.Writeln(\"RCPT TO:<testing@maddy.test>\")\n\t\tconn.ExpectPattern(\"250 *\")\n\t\tconn.Writeln(\"DATA\")\n\t\tconn.ExpectPattern(\"354 *\")\n\t\tconn.Writeln(\"From: <testing@sender.test>\")\n\t\tconn.Writeln(\"To: <testing@maddy.test>\")\n\t\tconn.Writeln(\"Subject: Hi there!\")\n\t\tconn.Writeln(\"\")\n\t\tconn.Writeln(\"Nice to meet you!\")\n\t\tconn.Writeln(\".\")\n\t}\n\n\tt.Subtest(\"Message dump\", func(t *tests.T) {\n\t\tconn := conn.Rebind(t)\n\n\t\tsubmitMsg(conn, \"testing@maddy.test\")\n\t\tconn.ExpectPattern(\"250 *\")\n\n\t\tmsgPath := filepath.Join(t.StateDir(), \"msg\")\n\t\tmsgContents, err := ioutil.ReadFile(msgPath)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tif string(msgContents) != expectedMsg {\n\t\t\tt.Log(\"Wrong message contents received by check script!\")\n\t\t\tt.Log(\"Actual:\")\n\t\t\tt.Log(msgContents)\n\t\t\tt.Log(\"Expected:\")\n\t\t\tt.Log(expectedMsg)\n\t\t}\n\t})\n\tt.Subtest(\"Message dump + Add header\", func(t *tests.T) {\n\t\tconn := conn.Rebind(t)\n\n\t\tsubmitMsg(conn, \"testing+addHeader@maddy.test\")\n\t\tconn.ExpectPattern(\"250 *\")\n\n\t\tmsgPath := filepath.Join(t.StateDir(), \"msg\")\n\t\tmsgContents, err := ioutil.ReadFile(msgPath)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\texpectedMsg := \"X-Added-Header: 1\\n\" + expectedMsg\n\t\tif string(msgContents) != expectedMsg {\n\t\t\tt.Log(\"Wrong message contents received by check script!\")\n\t\t\tt.Log(\"Actual:\")\n\t\t\tt.Log(msgContents)\n\t\t\tt.Log(\"Expected:\")\n\t\t\tt.Log(expectedMsg)\n\t\t}\n\t})\n\tt.Subtest(\"Body reject\", func(t *tests.T) {\n\t\tconn := conn.Rebind(t)\n\n\t\tsubmitMsg(conn, \"testing+reject@maddy.test\")\n\t\tconn.ExpectPattern(\"550 *\")\n\n\t\tmsgPath := filepath.Join(t.StateDir(), \"msg\")\n\t\tmsgContents, err := ioutil.ReadFile(msgPath)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tif string(msgContents) != expectedMsg {\n\t\t\tt.Log(\"Wrong message contents received by check script!\")\n\t\t\tt.Log(\"Actual:\")\n\t\t\tt.Log(msgContents)\n\t\t\tt.Log(\"Expected:\")\n\t\t\tt.Log([]byte(expectedMsg))\n\t\t}\n\t})\n\n\tconn.Writeln(\"QUIT\")\n\tconn.ExpectPattern(\"221 *\")\n}\n\nfunc TestHeaderSizeConstraint(tt *testing.T) {\n\ttt.Parallel()\n\tt := tests.NewT(tt)\n\tt.DNS(nil)\n\tt.Port(\"smtp\")\n\tt.Config(`\n\t\tsmtp tcp://127.0.0.1:{env:TEST_PORT_smtp} {\n\t\t\thostname mx.maddy.test\n\t\t\ttls off\n\t\t\tdeliver_to dummy\n\t\t\tmax_header_size 1K\n\t\t}\n\t`)\n\tt.Run(1)\n\tdefer t.Close()\n\n\tconn := t.Conn(\"smtp\")\n\tdefer conn.Close()\n\tconn.SMTPNegotation(\"localhost\", nil, nil)\n\tconn.Writeln(\"MAIL FROM:<testsender@maddy.test>\")\n\tconn.ExpectPattern(\"250 *\")\n\tconn.Writeln(\"RCPT TO:<testing@maddy.test>\")\n\tconn.ExpectPattern(\"250 *\")\n\tconn.Writeln(\"DATA\")\n\tconn.ExpectPattern(\"354 *\")\n\tconn.Writeln(\"From: <testing@sender.test>\")\n\tconn.Writeln(\"To: <testing@maddy.test>\")\n\tconn.Writeln(\"Subject: \" + strings.Repeat(\"A\", 2*1024))\n\tconn.Writeln(\"\")\n\tconn.Writeln(\"Hi\")\n\tconn.Writeln(\".\")\n\n\tconn.ExpectPattern(\"552 5.3.4 Message header size exceeds limit *\")\n\n\tconn.Writeln(\"QUIT\")\n\tconn.ExpectPattern(\"221 *\")\n}\n"
  },
  {
    "path": "tests/stress_test.go",
    "content": "//go:build integration\n// +build integration\n\n/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\npackage tests_test\n\nimport (\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/foxcpp/maddy/tests\"\n)\n\nfunc floodSmtp(c *tests.Conn, commands, expectedPatterns []string, iterations int) {\n\tfor i := 0; i < iterations; i++ {\n\t\tfor i, cmd := range commands {\n\t\t\tc.Writeln(cmd)\n\t\t\tif expectedPatterns[i] != \"\" {\n\t\t\t\tc.ExpectPattern(expectedPatterns[i])\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc TestSMTPFlood_FullMsg_NoLimits_1Conn(tt *testing.T) {\n\ttt.Parallel()\n\n\tt := tests.NewT(tt)\n\tt.DNS(nil)\n\tt.Port(\"smtp\")\n\tt.Config(`\n\t\tsmtp tcp://127.0.0.1:{env:TEST_PORT_smtp} {\n\t\t\thostname mx.maddy.test\n\t\t\ttls off\n\n\t\t\tdeliver_to dummy\n\t\t}`)\n\tt.Run(1)\n\tdefer t.Close()\n\n\tc := t.Conn(\"smtp\")\n\tdefer c.Close()\n\tc.SMTPNegotation(\"helo.maddy.test\", nil, nil)\n\tfloodSmtp(&c, []string{\n\t\t\"MAIL FROM:<from@maddy.test>\",\n\t\t\"RCPT TO:<to@maddy.test>\",\n\t\t\"DATA\",\n\t\t\"From: <from@maddy.test>\",\n\t\t\"\",\n\t\t\"Heya!\",\n\t\t\".\",\n\t}, []string{\n\t\t\"250 *\",\n\t\t\"250 *\",\n\t\t\"354 *\",\n\t\t\"\",\n\t\t\"\",\n\t\t\"\",\n\t\t\"250 *\",\n\t}, 100)\n}\n\nfunc TestSMTPFlood_FullMsg_NoLimits_10Conns(tt *testing.T) {\n\ttt.Parallel()\n\n\tt := tests.NewT(tt)\n\tt.DNS(nil)\n\tt.Port(\"smtp\")\n\tt.Config(`\n\t\tsmtp tcp://127.0.0.1:{env:TEST_PORT_smtp} {\n\t\t\thostname mx.maddy.test\n\t\t\ttls off\n\n\t\t\tdeliver_to dummy\n\t\t}`)\n\tt.Run(1)\n\tdefer t.Close()\n\n\twg := sync.WaitGroup{}\n\tfor i := 0; i < 10; i++ {\n\t\twg.Add(1)\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\tc := t.Conn(\"smtp\")\n\t\t\tdefer c.Close()\n\t\t\tc.SMTPNegotation(\"helo.maddy.test\", nil, nil)\n\t\t\tfloodSmtp(&c, []string{\n\t\t\t\t\"MAIL FROM:<from@maddy.test>\",\n\t\t\t\t\"RCPT TO:<to@maddy.test>\",\n\t\t\t\t\"DATA\",\n\t\t\t\t\"From: <from@maddy.test>\",\n\t\t\t\t\"\",\n\t\t\t\t\"Heya!\",\n\t\t\t\t\".\",\n\t\t\t}, []string{\n\t\t\t\t\"250 *\",\n\t\t\t\t\"250 *\",\n\t\t\t\t\"354 *\",\n\t\t\t\t\"\",\n\t\t\t\t\"\",\n\t\t\t\t\"\",\n\t\t\t\t\"250 *\",\n\t\t\t}, 100)\n\t\t\tt.Log(\"Done\")\n\t\t}()\n\t}\n\n\twg.Wait()\n}\n\nfunc TestSMTPFlood_EnvelopeAbort_NoLimits_10Conns(tt *testing.T) {\n\ttt.Parallel()\n\n\tt := tests.NewT(tt)\n\tt.DNS(nil)\n\tt.Port(\"smtp\")\n\tt.Config(`\n\t\tsmtp tcp://127.0.0.1:{env:TEST_PORT_smtp} {\n\t\t\thostname mx.maddy.test\n\t\t\ttls off\n\n\t\t\tdeliver_to dummy\n\t\t}`)\n\tt.Run(1)\n\tdefer t.Close()\n\n\twg := sync.WaitGroup{}\n\tfor i := 0; i < 10; i++ {\n\t\twg.Add(1)\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\tc := t.Conn(\"smtp\")\n\t\t\tdefer c.Close()\n\t\t\tc.SMTPNegotation(\"helo.maddy.test\", nil, nil)\n\t\t\tfloodSmtp(&c, []string{\n\t\t\t\t\"MAIL FROM:<from@maddy.test>\",\n\t\t\t\t\"RCPT TO:<to@maddy.test>\",\n\t\t\t\t\"RSET\",\n\t\t\t}, []string{\n\t\t\t\t\"250 *\",\n\t\t\t\t\"250 *\",\n\t\t\t\t\"250 *\",\n\t\t\t}, 100)\n\t\t\tt.Log(\"Done\")\n\t\t}()\n\t}\n\n\twg.Wait()\n}\n\nfunc TestSMTPFlood_EnvelopeAbort_Ratelimited(tt *testing.T) {\n\ttt.Parallel()\n\n\tt := tests.NewT(tt)\n\tt.DNS(nil)\n\tt.Port(\"smtp\")\n\tt.Config(`\n\t\tsmtp tcp://127.0.0.1:{env:TEST_PORT_smtp} {\n\t\t\thostname mx.maddy.test\n\t\t\ttls off\n\n\t\t\tlimits {\n\t\t\t\tall rate 10 1s\n\t\t\t}\n\n\t\t\tdeliver_to dummy\n\t\t}`)\n\tt.Run(1)\n\tdefer t.Close()\n\n\tconns := 5\n\tmsgsPerConn := 10\n\texpectedRate := 10\n\tslip := 10\n\n\tstart := time.Now()\n\n\twg := sync.WaitGroup{}\n\tfor i := 0; i < conns; i++ {\n\t\twg.Add(1)\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\tc := t.Conn(\"smtp\")\n\t\t\tdefer c.Close()\n\t\t\tc.SMTPNegotation(\"helo.maddy.test\", nil, nil)\n\t\t\tfloodSmtp(&c, []string{\n\t\t\t\t\"MAIL FROM:<from@maddy.test>\",\n\t\t\t\t\"RCPT TO:<to@maddy.test>\",\n\t\t\t\t\"RSET\",\n\t\t\t}, []string{\n\t\t\t\t\"250 *\",\n\t\t\t\t\"250 *\",\n\t\t\t\t\"250 *\",\n\t\t\t}, msgsPerConn)\n\t\t\tt.Log(\"Done\")\n\t\t}()\n\t}\n\n\twg.Wait()\n\tend := time.Now()\n\n\tt.Log(\"Sent\", conns*msgsPerConn, \"messages using\", conns, \"connections\")\n\tt.Log(\"Took\", end.Sub(start))\n\n\teffectiveRate := float64(conns*msgsPerConn) / end.Sub(start).Seconds()\n\tif effectiveRate > float64(expectedRate+slip) {\n\t\tt.Fatal(\"Effective rate is significantly bigger than limit:\", effectiveRate)\n\t}\n\tt.Log(\"Effective rate:\", effectiveRate)\n}\n\nfunc TestSMTPFlood_FullMsg_Ratelimited_PerSource(tt *testing.T) {\n\ttt.Parallel()\n\n\tt := tests.NewT(tt)\n\tt.DNS(nil)\n\tt.Port(\"smtp\")\n\tt.Config(`\n\t\tsmtp tcp://127.0.0.1:{env:TEST_PORT_smtp} {\n\t\t\thostname mx.maddy.test\n\t\t\ttls off\n\n\t\t\tdefer_sender_reject false\n\n\t\t\tlimits {\n\t\t\t\tsource rate 10 1s\n\t\t\t}\n\n\t\t\tdeliver_to dummy\n\t\t}`)\n\tt.Run(1)\n\tdefer t.Close()\n\n\tconns := 5\n\tmsgsPerConn := 10\n\texpectedRate := 10\n\tslip := 10\n\n\tstart := time.Now()\n\n\twg := sync.WaitGroup{}\n\tfor i := 0; i < conns; i++ {\n\t\twg.Add(1)\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\tc := t.Conn(\"smtp\")\n\t\t\tdefer c.Close()\n\t\t\tc.SMTPNegotation(\"helo.maddy.test\", nil, nil)\n\t\t\tfloodSmtp(&c, []string{\n\t\t\t\t\"MAIL FROM:<from@1.maddy.test>\",\n\t\t\t\t\"RCPT TO:<to@maddy.test>\",\n\t\t\t\t\"DATA\",\n\t\t\t\t\"From: <from@1.maddy.test>\",\n\t\t\t\t\"\",\n\t\t\t\t\"Heya!\",\n\t\t\t\t\".\",\n\t\t\t}, []string{\n\t\t\t\t\"250 *\",\n\t\t\t\t\"250 *\",\n\t\t\t\t\"354 *\",\n\t\t\t\t\"\",\n\t\t\t\t\"\",\n\t\t\t\t\"\",\n\t\t\t\t\"250 *\",\n\t\t\t}, msgsPerConn)\n\t\t\tt.Log(\"Done\")\n\t\t}()\n\t}\n\tfor i := 0; i < conns; i++ {\n\t\twg.Add(1)\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\tc := t.Conn(\"smtp\")\n\t\t\tdefer c.Close()\n\t\t\tc.SMTPNegotation(\"helo.maddy.test\", nil, nil)\n\t\t\tfloodSmtp(&c, []string{\n\t\t\t\t\"MAIL FROM:<from@2.maddy.test>\",\n\t\t\t\t\"RCPT TO:<to@maddy.test>\",\n\t\t\t\t\"DATA\",\n\t\t\t\t\"From: <from@1.maddy.test>\",\n\t\t\t\t\"\",\n\t\t\t\t\"Heya!\",\n\t\t\t\t\".\",\n\t\t\t}, []string{\n\t\t\t\t\"250 *\",\n\t\t\t\t\"250 *\",\n\t\t\t\t\"354 *\",\n\t\t\t\t\"\",\n\t\t\t\t\"\",\n\t\t\t\t\"\",\n\t\t\t\t\"250 *\",\n\t\t\t}, msgsPerConn)\n\t\t\tt.Log(\"Done\")\n\t\t}()\n\t}\n\n\twg.Wait()\n\tend := time.Now()\n\n\tt.Log(\"Sent\", conns*msgsPerConn, \"messages using\", conns, \"connections\")\n\tt.Log(\"Took\", end.Sub(start))\n\n\teffectiveRate := float64(conns*msgsPerConn*2) / end.Sub(start).Seconds()\n\t// Expect the rate twice since we send from two sources.\n\tif effectiveRate > float64(expectedRate*2+slip) {\n\t\tt.Fatal(\"Effective rate is significantly bigger than limit:\", effectiveRate)\n\t}\n\tt.Log(\"Effective rate:\", effectiveRate)\n}\n\nfunc TestSMTPFlood_EnvelopeAbort_Ratelimited_PerIP(tt *testing.T) {\n\ttt.Parallel()\n\n\tt := tests.NewT(tt)\n\tt.DNS(nil)\n\tt.Port(\"smtp\")\n\tt.Config(`\n\t\tsmtp tcp://127.0.0.1:{env:TEST_PORT_smtp} {\n\t\t\thostname mx.maddy.test\n\t\t\ttls off\n\n\t\t\tdefer_sender_reject false\n\n\t\t\tlimits {\n\t\t\t\tip rate 10 1s\n\t\t\t}\n\n\t\t\tdeliver_to dummy\n\t\t}`)\n\tt.Run(1)\n\tdefer t.Close()\n\n\tconns := 2\n\tmsgsPerConn := 50\n\texpectedRate := 10\n\tslip := 10\n\n\tstart := time.Now()\n\n\twg := sync.WaitGroup{}\n\tfor i := 0; i < conns; i++ {\n\t\twg.Add(1)\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\tc := t.Conn4(\"127.0.0.1\", \"smtp\")\n\t\t\tdefer c.Close()\n\t\t\tc.SMTPNegotation(\"helo.maddy.test\", nil, nil)\n\t\t\tfloodSmtp(&c, []string{\n\t\t\t\t\"MAIL FROM:<from@maddy.test>\",\n\t\t\t\t\"RCPT TO:<to@maddy.test>\",\n\t\t\t\t\"RSET\",\n\t\t\t}, []string{\n\t\t\t\t\"250 *\",\n\t\t\t\t\"250 *\",\n\t\t\t\t\"250 *\",\n\t\t\t}, msgsPerConn)\n\t\t\tt.Log(\"Done\")\n\t\t}()\n\t}\n\tfor i := 0; i < conns; i++ {\n\t\twg.Add(1)\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\tc := t.Conn4(\"127.0.0.2\", \"smtp\")\n\t\t\tdefer c.Close()\n\t\t\tc.SMTPNegotation(\"helo.maddy.test\", nil, nil)\n\t\t\tfloodSmtp(&c, []string{\n\t\t\t\t\"MAIL FROM:<from@maddy.test>\",\n\t\t\t\t\"RCPT TO:<to@maddy.test>\",\n\t\t\t\t\"RSET\",\n\t\t\t}, []string{\n\t\t\t\t\"250 *\",\n\t\t\t\t\"250 *\",\n\t\t\t\t\"250 *\",\n\t\t\t}, msgsPerConn)\n\t\t\tt.Log(\"Done\")\n\t\t}()\n\t}\n\n\twg.Wait()\n\tend := time.Now()\n\n\tt.Log(\"Sent\", 2*conns*msgsPerConn, \"messages using\", conns*2, \"connections\")\n\tt.Log(\"Took\", end.Sub(start))\n\n\teffectiveRate := float64(conns*msgsPerConn*2) / end.Sub(start).Seconds()\n\t// Expect the rate twice since we send from two sources.\n\tif effectiveRate > float64(expectedRate*2+slip) {\n\t\tt.Fatal(\"Effective rate is significantly bigger than limit:\", effectiveRate)\n\t}\n\tt.Log(\"Expected rate:\", expectedRate*2)\n\tt.Log(\"Effective rate:\", effectiveRate)\n}\n"
  },
  {
    "path": "tests/t.go",
    "content": "/*\nMaddy Mail Server - Composable all-in-one email server.\nCopyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n*/\n\n// Package tests provides the framework for integration testing of maddy.\n//\n// The packages core object is tests.T object that encapsulates all test\n// state. It runs the server using test-provided configuration file and acts as\n// a proxy for all interactions with the server.\npackage tests\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"flag\"\n\t\"fmt\"\n\t\"math/rand\"\n\t\"net\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/foxcpp/go-mockdns\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nvar (\n\tTestBinary  = \"./maddy\"\n\tCoverageOut string\n\tDebugLog    bool\n)\n\ntype T struct {\n\t*testing.T\n\n\ttestDir string\n\tcfg     string\n\n\tdnsServ  *mockdns.Server\n\tenv      []string\n\tports    map[string]uint16\n\tportsRev map[uint16]string\n\n\tservProc *exec.Cmd\n\n\treloadedChan chan struct{}\n}\n\nfunc NewT(t *testing.T) *T {\n\treturn &T{\n\t\tT:            t,\n\t\tports:        map[string]uint16{},\n\t\tportsRev:     map[uint16]string{},\n\t\treloadedChan: make(chan struct{}, 1),\n\t}\n}\n\n// Config sets the configuration to use for the server. It must be called\n// before Run.\nfunc (t *T) Config(cfg string) {\n\tt.Helper()\n\n\tt.cfg = cfg\n\n\tif t.servProc != nil {\n\t\tt.Log(\"Reloading configuration for running server...\")\n\n\t\tconfigPreable := \"state_dir \" + filepath.Join(t.testDir, \"statedir\") + \"\\n\" +\n\t\t\t\"runtime_dir \" + filepath.Join(t.testDir, \"runtimedir\") + \"\\n\\n\"\n\n\t\terr := os.WriteFile(filepath.Join(t.testDir, \"maddy.conf\"), []byte(configPreable+t.cfg), os.ModePerm)\n\t\tif err != nil {\n\t\t\tt.Fatal(\"Test configuration failed:\", err)\n\t\t}\n\n\t\tt.reloadConfig()\n\t}\n}\n\n// DNS sets the DNS zones to emulate for the tested server instance.\n//\n// If it is not called before Run, DNS(nil) call is assumed which makes the\n// mockdns server respond with NXDOMAIN to all queries.\nfunc (t *T) DNS(zones map[string]mockdns.Zone) {\n\tt.Helper()\n\n\tif zones == nil {\n\t\tzones = map[string]mockdns.Zone{}\n\t}\n\tif _, ok := zones[\"100.97.109.127.in-addr.arpa.\"]; !ok {\n\t\tzones[\"100.97.109.127.in-addr.arpa.\"] = mockdns.Zone{PTR: []string{\"client.maddy.test.\"}}\n\t}\n\n\tif t.dnsServ != nil {\n\t\tt.Log(\"NOTE: Multiple DNS calls, replacing the server instance...\")\n\t\trequire.NoError(t, t.dnsServ.Close())\n\t}\n\n\tdnsServ, err := mockdns.NewServerWithLogger(zones, t, false)\n\tif err != nil {\n\t\tt.Fatal(\"Test configuration failed:\", err)\n\t}\n\tdnsServ.Log = t\n\tt.dnsServ = dnsServ\n\n\tt.Cleanup(func() {\n\t\tif t.dnsServ == nil {\n\t\t\treturn\n\t\t}\n\n\t\t// Shutdown the DNS server after maddy to make sure it will not spend time\n\t\t// timing out queries.\n\t\tif err := t.dnsServ.Close(); err != nil {\n\t\t\tt.Log(\"Unable to stop the DNS server:\", err)\n\t\t}\n\t\tt.dnsServ = nil\n\t})\n}\n\n// Port allocates the random TCP port for use by test. It will made accessible\n// in the configuration via environment variables with name in the form\n// TEST_PORT_name.\n//\n// If there is a port with name remote_smtp, it will be passed as the value for\n// the -debug.smtpport parameter.\nfunc (t *T) Port(name string) uint16 {\n\tif port := t.ports[name]; port != 0 {\n\t\treturn port\n\t}\n\n\t// TODO: Try to bind on port to test its usability.\n\tport := rand.Int31n(45536) + 20000\n\tt.ports[name] = uint16(port)\n\tt.portsRev[uint16(port)] = name\n\treturn uint16(port)\n}\n\nfunc (t *T) Env(kv string) {\n\tt.env = append(t.env, kv)\n}\n\nfunc (t *T) ensureCanRun() {\n\tif t.cfg == \"\" {\n\t\tpanic(\"tests: Run called without configuration set\")\n\t}\n\tif t.dnsServ == nil {\n\t\t// If there is no DNS zones set in test - start a server that will\n\t\t// respond with NXDOMAIN to all queries to avoid accidentally leaking\n\t\t// any DNS queries to the real world.\n\t\tt.Log(\"NOTE: Explicit DNS(nil) is recommended.\")\n\t\tt.DNS(nil)\n\t}\n\n\t// Setup file system, create statedir, runtimedir, write out config.\n\tif t.testDir == \"\" {\n\t\ttestDir, err := os.MkdirTemp(\"\", \"maddy-tests-\")\n\t\tif err != nil {\n\t\t\tt.Fatal(\"Test configuration failed:\", err)\n\t\t}\n\t\tt.testDir = testDir\n\t\tt.Log(\"using\", t.testDir)\n\n\t\tif err := os.MkdirAll(filepath.Join(t.testDir, \"statedir\"), os.ModePerm); err != nil {\n\t\t\tt.Fatal(\"Test configuration failed:\", err)\n\t\t}\n\t\tif err := os.MkdirAll(filepath.Join(t.testDir, \"runtimedir\"), os.ModePerm); err != nil {\n\t\t\tt.Fatal(\"Test configuration failed:\", err)\n\t\t}\n\n\t\tt.Cleanup(func() {\n\t\t\tif !t.Failed() {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tt.Log(\"removing\", t.testDir)\n\t\t\tassert.NoError(t, os.RemoveAll(t.testDir))\n\t\t\tt.testDir = \"\"\n\t\t})\n\t}\n\n\tconfigPreable := \"state_dir \" + filepath.Join(t.testDir, \"statedir\") + \"\\n\" +\n\t\t\"runtime_dir \" + filepath.Join(t.testDir, \"runtimedir\") + \"\\n\\n\"\n\n\terr := os.WriteFile(filepath.Join(t.testDir, \"maddy.conf\"), []byte(configPreable+t.cfg), os.ModePerm)\n\tif err != nil {\n\t\tt.Fatal(\"Test configuration failed:\", err)\n\t}\n}\n\nfunc (t *T) buildCmd(additionalArgs ...string) *exec.Cmd {\n\t// Assigning 0 by default will make outbound SMTP unusable.\n\tremoteSmtp := \"0\"\n\tif port := t.ports[\"remote_smtp\"]; port != 0 {\n\t\tremoteSmtp = strconv.Itoa(int(port))\n\t}\n\n\targs := []string{\"-config\", filepath.Join(t.testDir, \"maddy.conf\"),\n\t\t\"-debug.smtpport\", remoteSmtp,\n\t\t\"-debug.dnsoverride\", t.dnsServ.LocalAddr().String(),\n\t}\n\n\tif CoverageOut != \"\" {\n\t\targs = append(args, \"-test.coverprofile\", CoverageOut+\".\"+strconv.FormatInt(time.Now().UnixNano(), 16))\n\t}\n\tif DebugLog {\n\t\targs = append(args, \"-debug\")\n\t}\n\n\targs = append(args, additionalArgs...)\n\n\tcmd := exec.Command(TestBinary, args...)\n\n\tpwd, err := os.Getwd()\n\tif err != nil {\n\t\tt.Fatal(\"Test configuration failed:\", err)\n\t}\n\n\t// Set environment variables.\n\tcmd.Env = os.Environ()\n\tcmd.Env = append(cmd.Env,\n\t\t\"TEST_PWD=\"+pwd,\n\t\t\"TEST_STATE_DIR=\"+filepath.Join(t.testDir, \"statedir\"),\n\t\t\"TEST_RUNTIME_DIR=\"+filepath.Join(t.testDir, \"runtimedir\"),\n\t)\n\tfor name, port := range t.ports {\n\t\tcmd.Env = append(cmd.Env, fmt.Sprintf(\"TEST_PORT_%s=%d\", name, port))\n\t}\n\tcmd.Env = append(cmd.Env, t.env...)\n\n\treturn cmd\n}\n\nfunc (t *T) MustRunCLIGroup(args ...[]string) {\n\tt.ensureCanRun()\n\n\twg := sync.WaitGroup{}\n\tfor _, arg := range args {\n\t\twg.Add(1)\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\n\t\t\t_, err := t.RunCLI(arg...)\n\t\t\tif err != nil {\n\t\t\t\tt.Printf(\"maddy %v: %v\", arg, err)\n\t\t\t\tt.Fail()\n\t\t\t}\n\t\t}()\n\t}\n\twg.Wait()\n}\n\nfunc (t *T) MustRunCLI(args ...string) string {\n\ts, err := t.RunCLI(args...)\n\tif err != nil {\n\t\tt.Fatalf(\"maddy %v: %v\", args, err)\n\t}\n\treturn s\n}\n\nfunc (t *T) RunCLI(args ...string) (string, error) {\n\tt.ensureCanRun()\n\tcmd := t.buildCmd(args...)\n\n\tvar stderr, stdout bytes.Buffer\n\tcmd.Stderr = &stderr\n\tcmd.Stdout = &stdout\n\n\tt.Log(\"launching maddy\", cmd.Args)\n\tif err := cmd.Run(); err != nil {\n\t\tt.Log(\"Stderr:\", stderr.String())\n\t\tt.Fatal(\"Test configuration failed:\", err)\n\t}\n\n\tt.Log(\"Stderr:\", stderr.String())\n\n\treturn stdout.String(), nil\n}\n\n// Run completes the configuration of test environment and starts the test server.\n//\n// T.Close should be called by the end of test to release any resources and\n// shutdown the server.\n//\n// The parameter waitListeners specifies the amount of listeners the server is\n// supposed to configure. Run() will block before all of them are up.\nfunc (t *T) Run(waitListeners int) {\n\tt.ensureCanRun()\n\tcmd := t.buildCmd(\"run\")\n\n\t// Capture maddy log and redirect it.\n\tlogOut, err := cmd.StderrPipe()\n\tif err != nil {\n\t\tt.Fatal(\"Test configuration failed:\", err)\n\t}\n\n\tt.Log(\"launching maddy\", cmd.Args)\n\tif err := cmd.Start(); err != nil {\n\t\tt.Fatal(\"Test configuration failed:\", err)\n\t}\n\n\tserverStarted := make(chan bool)\n\n\tgo func() {\n\t\tdefer close(serverStarted)\n\t\tscnr := bufio.NewScanner(logOut)\n\t\tfor scnr.Scan() {\n\t\t\tline := scnr.Text()\n\n\t\t\tt.Log(\"maddy:\", line)\n\n\t\t\tif strings.HasPrefix(line, \"server started\") {\n\t\t\t\tserverStarted <- true\n\t\t\t}\n\n\t\t\tif strings.HasPrefix(line, \"new server started\") {\n\t\t\t\tselect {\n\t\t\t\tcase t.reloadedChan <- struct{}{}:\n\t\t\t\t\tt.Log(\"server reload confirmed, continuing test\")\n\t\t\t\tdefault:\n\t\t\t\t\tt.Log(\"unexpected reloads detected\")\n\t\t\t\t\tt.Fail()\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif err := scnr.Err(); err != nil {\n\t\t\tt.Log(\"stderr I/O error:\", err)\n\t\t}\n\t}()\n\n\tif !<-serverStarted {\n\t\tt.Fatal(\"Log ended before all expected listeners are up. Start-up error?\")\n\t}\n\n\tt.servProc = cmd\n\n\tt.Cleanup(t.killServer)\n}\n\nfunc (t *T) StateDir() string {\n\treturn filepath.Join(t.testDir, \"statedir\")\n}\n\nfunc (t *T) RuntimeDir() string {\n\treturn filepath.Join(t.testDir, \"statedir\")\n}\n\nfunc (t *T) killServer() {\n\tif err := t.servProc.Process.Signal(os.Interrupt); err != nil {\n\t\tt.Log(\"Unable to kill the server process:\", err)\n\t\tassert.NoError(t, os.RemoveAll(t.testDir))\n\t\treturn // Return, as now it is pointless to wait for it.\n\t}\n\n\tgo func() {\n\t\ttime.Sleep(5 * time.Second)\n\t\tif t.servProc != nil {\n\t\t\tt.Log(\"Killing possibly hung server process\")\n\t\t\tt.servProc.Process.Kill() //nolint:errcheck\n\t\t}\n\t}()\n\n\tif err := t.servProc.Wait(); err != nil {\n\t\tt.Error(\"The server did not stop cleanly, deadlock?\")\n\t}\n\n\tt.servProc = nil\n\n\tif err := os.RemoveAll(t.testDir); err != nil {\n\t\tt.Log(\"Failed to remove test directory:\", err)\n\t}\n\tt.testDir = \"\"\n}\n\nfunc (t *T) Close() {\n\tt.Log(\"close is no-op\")\n}\n\n// Printf implements Logger interfaces used by some libraries.\nfunc (t *T) Printf(f string, a ...interface{}) {\n\tt.Logf(f, a...)\n}\n\n// Conn6 connects to the server listener at the specified named port using IPv6 loopback.\nfunc (t *T) Conn6(portName string) Conn {\n\tport := t.ports[portName]\n\tif port == 0 {\n\t\tpanic(\"tests: connection for the unused port name is requested\")\n\t}\n\n\tconn, err := net.Dial(\"tcp6\", \"[::1]:\"+strconv.Itoa(int(port)))\n\tif err != nil {\n\t\tt.Fatal(\"Could not connect, is server listening?\", err)\n\t}\n\n\treturn Conn{\n\t\tT:            t,\n\t\tWriteTimeout: 1 * time.Second,\n\t\tReadTimeout:  15 * time.Second,\n\t\tConn:         conn,\n\t\tScanner:      bufio.NewScanner(conn),\n\t}\n}\n\n// Conn4 connects to the server listener at the specified named port using one\n// of 127.0.0.0/8 addresses as a source.\nfunc (t *T) Conn4(sourceIP, portName string) Conn {\n\tport := t.ports[portName]\n\tif port == 0 {\n\t\tpanic(\"tests: connection for the unused port name is requested\")\n\t}\n\n\tlocalIP := net.ParseIP(sourceIP)\n\tif localIP == nil {\n\t\tpanic(\"tests: invalid localIP argument\")\n\t}\n\tif localIP.To4() == nil {\n\t\tpanic(\"tests: only IPv4 addresses are allowed\")\n\t}\n\n\tconn, err := net.DialTCP(\"tcp4\", &net.TCPAddr{\n\t\tIP:   localIP,\n\t\tPort: 0,\n\t}, &net.TCPAddr{\n\t\tIP:   net.IPv4(127, 0, 0, 1),\n\t\tPort: int(port),\n\t})\n\tif err != nil {\n\t\tt.Fatal(\"Could not connect, is server listening?\", err)\n\t}\n\n\treturn Conn{\n\t\tT:            t,\n\t\tWriteTimeout: 1 * time.Second,\n\t\tReadTimeout:  15 * time.Second,\n\t\tConn:         conn,\n\t\tScanner:      bufio.NewScanner(conn),\n\t}\n}\n\nvar (\n\tDefaultSourceIP    = net.IPv4(127, 109, 97, 100)\n\tDefaultSourceIPRev = \"100.97.109.127\"\n)\n\nfunc (t *T) ConnUnnamed(port uint16) Conn {\n\tconn, err := net.DialTCP(\"tcp4\", &net.TCPAddr{\n\t\tIP:   DefaultSourceIP,\n\t\tPort: 0,\n\t}, &net.TCPAddr{\n\t\tIP:   net.IPv4(127, 0, 0, 1),\n\t\tPort: int(port),\n\t})\n\tif err != nil {\n\t\tt.Fatal(\"Could not connect, is server listening?\", err)\n\t}\n\n\treturn Conn{\n\t\tT:            t,\n\t\tWriteTimeout: 1 * time.Second,\n\t\tReadTimeout:  15 * time.Second,\n\t\tConn:         conn,\n\t\tScanner:      bufio.NewScanner(conn),\n\t}\n}\n\nfunc (t *T) Conn(portName string) Conn {\n\tport := t.ports[portName]\n\tif port == 0 {\n\t\tpanic(\"tests: connection for the unused port name is requested\")\n\t}\n\n\treturn t.ConnUnnamed(port)\n}\n\nfunc (t *T) Subtest(name string, f func(t *T)) {\n\tt.T.Run(name, func(subTT *testing.T) {\n\t\tsubT := *t\n\t\tsubT.T = subTT\n\t\tf(&subT)\n\t})\n}\n\nfunc init() {\n\tflag.StringVar(&TestBinary, \"integration.executable\", \"./maddy\", \"executable to test\")\n\tflag.StringVar(&CoverageOut, \"integration.coverprofile\", \"\", \"write coverage stats to file (requires special maddy executable)\")\n\tflag.BoolVar(&DebugLog, \"integration.debug\", false, \"pass -debug to maddy executable\")\n}\n"
  },
  {
    "path": "tests/testdata/check_command.sh",
    "content": "#!/bin/sh\n\nif [ -e \"${TEST_PWD}/testdata/${1}.hdr\" ]; then\n    cat \"${TEST_PWD}/testdata/${1}.hdr\"\nfi\n\ncat > ${TEST_STATE_DIR}/msg\n\nif [ -e \"${TEST_PWD}/testdata/${1}.exit\" ]; then\n    exit \"$(cat \"${TEST_PWD}/testdata/${1}.exit\")\"\nfi\n"
  },
  {
    "path": "tests/testdata/testing+addHeader@maddy.test.hdr",
    "content": "X-Added-Header: 1\n"
  },
  {
    "path": "tests/testdata/testing+reject@maddy.test.exit",
    "content": "12\n"
  }
]