diff --git a/.all-contributorsrc b/.all-contributorsrc deleted file mode 100644 index c3d39c655d1..00000000000 --- a/.all-contributorsrc +++ /dev/null @@ -1,26 +0,0 @@ -{ - "files": [ - "CONTRIBUTORS.md" - ], - "imageSize": 100, - "commit": false, - "contributors": [ - { - "login": "matrixes", - "name": "matrixes", - "avatar_url": "https://avatars.githubusercontent.com/u/46491408?v=4", - "profile": "https://github.com/matrixes", - "contributions": [ - "blog" - ] - } - ], - "contributorsPerLine": 7, - "badgeTemplate": "", - "projectName": "docker-mailserver", - "projectOwner": "docker-mailserver", - "repoType": "github", - "repoHost": "https://github.com", - "skipCi": false, - "commitConvention": "none" -} diff --git a/.dockerignore b/.dockerignore index b6eef669ae8..50365994307 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,2 @@ * !target -!VERSION diff --git a/.editorconfig b/.editorconfig index fd68e2f2b88..f5f1e31766a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,6 +8,7 @@ root = true [*] charset = utf-8 end_of_line = lf +indent_size = 2 indent_style = space insert_final_newline = true trim_trailing_whitespace = true @@ -16,21 +17,9 @@ trim_trailing_whitespace = true # --- Specific ---------------------------------- # ----------------------------------------------- -[*.{yaml,yml,sh,bats}] -indent_size = 2 - -[Makefile] +[{Makefile,.gitmodules}] indent_style = tab indent_size = 4 [*.md] trim_trailing_whitespace = false - -# ----------------------------------------------- -# --- Git Submodules ---------------------------- -# ----------------------------------------------- - -[{test/bats/**,test/test_helper/**}] -indent_style = none -indent_size = none -end_of_line = none diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000000..8933edd9ab9 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,158 @@ +# Normalize line endings of all non-binary files to LF upon check-in (`git add` / `git commit`): +* text=auto + +################################################# +### General ################################### +################################################# + +## GENERIC +### CI + docs/mkdocs.yml +*.yml text +### Documentation (Project, Tests, Docs site) +*.md text +### TLS certs (test/files/) + DHE params (target/shared/) +*.pem text +*.pem.sha512sum text + +################################################# +### Project ################################### +################################################# + +## BUILD: +.dockerignore text +Dockerfile text eol=lf +Makefile + +## EXAMPLE (RUNTIME): +*.env text +*.yaml text + +## PROJECT +.editorconfig text export-ignore +.gitattributes text export-ignore +.gitignore text export-ignore +.gitkeep text export-ignore +.gitmodules text export-ignore +LICENSE text + +## SOURCE CODE +*.sh text eol=lf +### acme.json extractor (target/bin/) +*.py text eol=lf +### Only contain scripts (glob for extensionless) +target/bin/** text eol=lf + +################################################# +### Config #################################### +################################################# + +## CONFIG +### Contains all text files (glob for extensionless) +target/amavis/** text +target/fetchmail/** text +target/getmail/** text +target/opendkim/** text +target/opendmarc/** text +target/postgrey/** text +target/postsrsd/** text +### Generic target/ + test/config/ +*.cf text +*.conf text +### Dovecot +*.ext text +*.sieve text +### Dovecot + Rspamd +*.inc text +### Fail2Ban + Postgrey (test/config/) +*.local text +### Postfix +*.pcre text + +################################################# +### Tests ##################################### +################################################# + +## BATS +*.bash text eol=lf +*.bats text eol=lf + +## CONFIG (test/config/) +### OpenLDAP image +*.ldif text +### OpenDKIM +*.private text +KeyTable text +SigningTable text +TrustedHosts text +### Postgrey +whitelist_recipients text + +## MISC +### test/config/ + test/files/ +*.txt text +### test/linting/ (.ecrc.json) + test/files/ (*.acme.json): +*.json text + +################################################# +### Documentation Website ##################### +################################################# + +## DOCUMENTATION +### docs/content/assets/ +*.css text +*.png binary +*.svg text -diff +*.woff binary +### docs/overrides/ +*.html text +*.ico binary +*.webp binary + +################################################# +### Info # ##################################### +################################################# + +### WHAT IS THIS FILE? +# `.gitattributes` - Pattern-based overrides (Project specific) +# Documentation: https://git-scm.com/docs/gitattributes +# +# Travels with the project and can override the defaults from `.gitconfig`. +# This helps to enforce consistent line endings (CRLF / LF) where needed via +# patterns (_when the git client supports `.gitattributes`_). + +# `.gitconfig` - Global Git defaults (Dev environment) +# Documentation: https://git-scm.com/docs/git-config +# +# Git settings `core.autocrlf` and `core.eol` can vary across dev environments. +# Those defaults can introduce subtle bugs due to incompatible line endings. + + +### WHY SHOULD I CARE? +# The desired result is to ensure the repo contains normalized LF line endings, +# notably avoiding unhelpful noise in diffs or issues incurred from mixed line +# endings. Storing as LF ensures no surprises for line endings during checkout. +# Additionally for checkout to the local working directory, line endings can be +# forced to CRLF or LF per file where appropriate, which ensures the files have +# compatible line endings where software expects a specific kind. +# +# Examples: +# Diffs with nothing visual changed. Line endings appear invisible. +# Tests that compare text from two sources where only line endings differ fail. +# /bin/sh with a shebang fails to run a binary at the given path due to a CRLF. + + +### ATTRIBUTES +# `text` normalizes the line endings of a file to LF upon commit (CRLF -> LF). +# `text=auto` sets `text` if Git doesn't consider the file as binary data. + +# `eol` sets an explicit line ending to write files to the working directory. +# `core.eol` is used for any files not explicitly set with an `eol` attr value. +# `core.eol` uses the native line endings for your platform by default. +# `core.autocrlf` (if set to `true` or `input`) overrides the `core.eol` value. + +# `binary` is an alias for `-text -diff`. The file won't be normalized (-text). +# `-diff` indicates to avoid creating a diff. Useful when diffs are unlikely +# to be meaningful, such as generated content (SVG, Source Maps, Lockfiles). + +# `export-ignore` excludes matched files and directories during `git archive`, +# which services like Github use to create releases with archived source files. diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 3774bf7b377..0061f3b7953 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,179 +1,70 @@ -name: Bug report -description: File a bug report -title: "[BUG] " +name: Bug Report +description: Submit a bug report to help us improve +title: 'bug report: ' labels: - - kind/bug + - kind/bug/report - meta/needs triage - - priority/medium body: - - type: markdown - attributes: - value: | - # Filing a report - - Thank you for participating in this project and reporting a bug. Docker Mail Server (DMS) is a community-driven project, and each contribution counts. - - Please **fill out all the fields and checkboxes of this form** to make it easier for maintainers to understand the problem and to solve it. The maintainers and moderators are volunteers that need you to fill this template with accurate informations in order to help you in the best and quickest way. We will have to label your request with `meta/no template - no support` if your request is sloppy and provides no way to help you correctly. - - **Make sure** you read through the whole [README](https://github.com/docker-mailserver/docker-mailserver/blob/master/README.md), the [documentation](https://docker-mailserver.github.io/docker-mailserver/edge/) and the [issue tracker](https://github.com/docker-mailserver/docker-mailserver/issues?q=is%3Aissue) before opening a new bug report. - - Markdown formatting can be used in almost all text fields. The description will tell you if this is not the case for a specific field. - - ## Levels of support - - We provide official support for - - 1. OS/ARCH: Linux on AMD64 (x86_64) and ARM64 (AArch64) - 2. Containerization platform: Docker and Docker Compose (i.e. based on Docker Engine) - - Other configurations are not officially supported, but there are maintainers that may be able to help you. This is the case for - - 1. OS/ARCH: macOS - 2. Containerization platform: Kubernetes (K8s) - - Support for these cases is dependent on specific maintainers, and these cases are marked with `not officially supported`. All other cases are not supported and there are very likely no maintainers that can help you. These cases are marked with `unsupported`. - type: checkboxes - id: miscellaneous-first-checks + id: preliminary-checks attributes: - label: Miscellaneous first checks - description: Please read these carefully. + label: 📝 Preliminary Checks + description: | + By submitting this issue, you agree to our [Code of Conduct](https://github.com/docker-mailserver/docker-mailserver/blob/master/CODE_OF_CONDUCT.md). options: - - label: I checked that all ports are open and not blocked by my ISP / hosting provider. - required: true - - label: I know that SSL errors are likely the result of a wrong setup on the user side and not caused by DMS itself. I'm confident my setup is correct. + - label: I tried searching for an existing issue and followed the [debugging docs](https://docker-mailserver.github.io/docker-mailserver/latest/config/debugging/) advice, but still need assistance. required: true - - type: input - id: affected-components - attributes: - label: Affected Component(s) - description: What is affected by this bug? Please describe it with only a few words here. Detailed description can be given later. - placeholder: No debug output is shown. - validations: - required: true - - type: textarea - id: when-does-it-occur - attributes: - label: What happened and when does this occur? - description: Tell us what happened. Use [fenced code blocks](https://docs.github.com/en/github/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks#fenced-code-blocks) when pasting lots of text! - placeholder: Although `LOG_LEVEL=debug` is set, I see no debug output. - validations: - required: true + - label: I will disclose any AI assistance I have used with the information I provide in my report, so that I do not waste the time of humans trying to help me. - type: textarea - id: what-did-you-expect-to-happen + id: what-happened attributes: - label: What did you expect to happen? - description: Tell us what you expected. - placeholder: I expected to see debug messages. + label: 👀 What Happened? + description: How did this differ from your expectations? + placeholder: Although `LOG_LEVEL=debug` is set, the logs are missing debug output. validations: required: true - type: textarea - id: how-do-we-replicate-this-issue + id: steps-to-reproduce attributes: - label: How do we replicate the issue? - description: What did you do and how can we replicate this issue? - value: | - 1. - 2. - 3. - ... - validations: - required: true + label: 👟 Reproduction Steps + description: | + How did you trigger this bug? Please walk us through it step by step. + Please use [fenced code blocks](https://docs.github.com/en/github/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks#fenced-code-blocks) when pasting lots of text! + placeholder: The easier it is for us to reproduce your issue, the sooner we can help resolve it 😉 - type: input id: mailserver-version attributes: - label: DMS version + label: 🐋 DMS Version description: On which version (image tag) did you encounter this bug? - placeholder: v10.1.2 - validations: - required: true - - type: dropdown - id: operating-system - attributes: - label: What operating system is DMS running on? - options: - - Linux - - macOS (not officially supported) - - Windows (unsupported) - - Other (unsupported) + placeholder: v12.1.0 (do not put "latest") validations: required: true - type: input - id: operating-system-version - attributes: - label: Which operating system version? - placeholder: e.g. Debian 11 - validations: - required: true - - type: dropdown - id: isa - attributes: - label: What instruction set architecture is DMS running on? - options: - - AMD64 / x86_64 - - ARM64 / AArch64 (ARM v8 and newer) - - Other (unsupported) - validations: - required: true - - type: dropdown - id: container-orchestrator + id: operating-system attributes: - label: What container orchestration tool are you using? - options: - - Docker - - Docker Compose - - Podman (not officially supported) - - Kubernetes (not officially supported) - - Other (unsupported) + label: 💻 Operating System and Architecture + description: | + Which OS is your docker host running on? + **NOTE:** Windows and macOS have limited support. + placeholder: Debian 11 (Bullseye) x86_64, Fedora 38 ARM64 validations: required: true - type: textarea - id: important-environment-variables + id: container-configuration-files attributes: - label: docker-compose.yml - description: Show us your `docker-compose.yml` file or your equivalent `docker run` command, if applicable. This filed is formatted as YAML. + label: ⚙️ Container configuration files + description: | + Show us the `compose.yaml` file or command that you used to run DMS (and possibly other related services). + - This field is formatted as YAML. + - If you are using Kubernetes, you can alternatively share your manifest files here. render: yml - type: textarea id: relevant-log-output attributes: - label: Relevant log output - description: Show us relevant log output here. You can enable debug output by setting the environment variable `LOG_LEVEL` to `debug` or `trace`. This field's contents are interpreted as pure text. + label: 📜 Relevant log output + description: | + Show us relevant log output here. + - This field expects only plain text (_rendered as a fenced code block_). + - You can enable debug output by setting the environment variable `LOG_LEVEL` to `debug` or `trace`. render: Text - - type: textarea - id: other-relevant-information - attributes: - label: Other relevant information - description: If there is more, you can tell us here. - - type: checkboxes - id: experience - attributes: - label: What level of experience do you have with Docker and mail servers? - description: > - **You are not obliged to answer this question**. - We do encourage answering though as it provides context to better assist you. - Less experienced users tend to make common mistakes, which is ok; by letting us know we can spot those more easily. If you are experienced, we can skip basic questions and save time. - - options: - - label: I am inexperienced with docker - - label: I am rather experienced with docker - - label: I am inexperienced with mail servers - - label: I am rather experienced with mail servers - - label: I am uncomfortable with the CLI - - label: I am rather comfortable with the CLI - - type: checkboxes - id: terms-code-of-conduct - attributes: - label: Code of conduct - description: By submitting this issue, you agree to follow [our code of conduct](https://github.com/docker-mailserver/docker-mailserver/blob/master/CODE_OF_CONDUCT.md). - options: - - label: I have read this project's [Code of Conduct](https://github.com/docker-mailserver/docker-mailserver/blob/master/CODE_OF_CONDUCT.md) and I agree - required: true - - label: I have read the [README](https://github.com/docker-mailserver/docker-mailserver/blob/master/README.md) and the [documentation](https://docker-mailserver.github.io/docker-mailserver/edge/) and I searched the [issue tracker](https://github.com/docker-mailserver/docker-mailserver/issues?q=is%3Aissue) but could not find a solution - required: true - - type: input - id: form-improvements - attributes: - label: Improvements to this form? - description: If you have criticism or general feedback about this issue form, feel free to tell us so we can enhance the experience for everyone. - validations: - required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index c1efca2e505..d5d80621320 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,9 +1,12 @@ blank_issues_enabled: false contact_links: - - name: Documentation - url: https://docker-mailserver.github.io/docker-mailserver/edge - about: Extended documentaton - visit this first before opening issues - - name: Environment Variables Section - url: https://docker-mailserver.github.io/docker-mailserver/edge/config/environment/ - about: Read this section for information about mail server variables + - name: Documentation | Landing Page + url: https://docker-mailserver.github.io/docker-mailserver/latest + about: Visit this first before opening issues! + - name: Documentation | Environment Variables Page + url: https://docker-mailserver.github.io/docker-mailserver/latest/config/environment/ + about: Read this page for information about mail server variables. + - name: Documentation | Debugging Page + url: https://docker-mailserver.github.io/docker-mailserver/latest/config/debugging/ + about: Read this page for information on how to debug DMS. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index ac685461362..00000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -name: "\U0001F389 Feature request" -about: Suggest an idea for this project -title: '[FR]' -labels: area/enhancement, kind/feature (request), meta/needs triage, priority/low -assignees: '' - ---- - -# Feature Request - -## Context - - - -### Is your Feature Request related to a Problem? - - - -### Describe the Solution you'd like - - - -### Are you going to implement it? - - - -Yes, because I know the probability of someone else doing it is low and I can learn from it. - -No, and I understand that it is highly likely no one will implement it. Furthermore, I understand that this issue will likely become stale and will be closed. - -### What are you going to contribute?? - - - -## Additional context - -### Alternatives you've considered - - - -### Who will that Feature be useful to? - - - -### What have you done already? - - diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 00000000000..ba5bd987e8a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,62 @@ +name: Feature Request +description: Suggest an idea for this project +title: 'feature request: ' +labels: + - kind/new feature + - meta/needs triage +projects: + - DMS Core Backlog + +body: + - type: markdown + attributes: + value: | + Markdown formatting can be used in almost all text fields. The description will tell you if this is not the case for a specific field. + + Be as precise as possible, and if in doubt, it's best to add more information that too few. + + --- + - type: textarea + id: context + attributes: + label: Context + description: Tell us how your request is related to DMS, one of its components or another issue / PR. Also **link all connected issues and PRs here**! + validations: + required: true + - type: textarea + id: description + attributes: + label: Description + description: Describe the solution you would like to have implemented. Be as precise as possible! + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: Alternatives + description: Which alternatives have you considered? + validations: + required: true + - type: textarea + id: applicable-users + attributes: + label: Applicable Users + description: Who will that feature be useful to? + validations: + required: true + - type: dropdown + id: implementer + attributes: + label: Are you going to implement it? + options: + - Yes, because I know the probability of someone else doing it is low and I can learn from it. + - No, and I understand that it is highly likely no one will implement it. Furthermore, I understand that this issue will likely become stale and will be closed. + validations: + required: true + - type: textarea + id: contribution + attributes: + label: What are you going to contribute? + description: You may also tell us what you have already done. + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/question.yml b/.github/ISSUE_TEMPLATE/question.yml new file mode 100644 index 00000000000..b58d0370c34 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.yml @@ -0,0 +1,30 @@ +name: Other +description: Miscellaneous questions and reports for the project (not support) +title: 'other: ' +labels: + - meta/help wanted + +body: + - type: dropdown + id: subject + attributes: + label: Subject + options: + - I would like to contribute to the project + - I would like to configure a not documented mail server use case + - I would like some feedback concerning a use case + - Something else that requires developers attention + validations: + required: true + - type: textarea + id: description + validations: + required: true + attributes: + label: Description + value: | + diff --git a/.github/ISSUE_TEMPLATE/questions.md b/.github/ISSUE_TEMPLATE/questions.md deleted file mode 100644 index 448ff47f61b..00000000000 --- a/.github/ISSUE_TEMPLATE/questions.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -name: "❓ Question / Other" -about: Ask a question about docker-mailserver -title: '' -labels: kind/question, priority/low, meta/help wanted, meta/needs triage -assignees: '' - ---- - -# Subject - - - -I would like to contribute to the project -I would like to configure a not documented mail server use case -I would like some feedback concerning a use case -I have questions about TLS/SSL/STARTTLS/OpenSSL -Other - -## Description - - - -``` BASH -# CODE GOES HERE - -``` diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 99a0fb33e4a..327014c57d5 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,8 +4,6 @@ updates: directory: "/" schedule: interval: "weekly" - reviewers: - - "docker-mailserver/maintainers" labels: - "area/ci" - "kind/update" @@ -15,8 +13,6 @@ updates: directory: / schedule: interval: "weekly" - reviewers: - - "docker-mailserver/maintainers" labels: - "area/ci" - "kind/update" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index bcdf248a1ef..104f9433b73 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,7 +1,9 @@ # Description - + Fixes # @@ -16,11 +18,12 @@ Fixes # - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] This change requires a documentation update -## Checklist: +## Checklist - [ ] My code follows the style guidelines of this project -- [ ] I have performed a self-review of my own code +- [ ] I have performed a self-review of my code - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation (README.md or the documentation under `docs/`) -- [ ] If necessary I have added tests that prove my fix is effective or that my feature works +- [ ] If necessary, I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes +- [ ] **I have added information about changes made in this PR to `CHANGELOG.md`** diff --git a/.github/workflows/contributors.yml b/.github/workflows/contributors.yml deleted file mode 100644 index b4d5204e0fa..00000000000 --- a/.github/workflows/contributors.yml +++ /dev/null @@ -1,65 +0,0 @@ -name: Update contributors -on: - workflow_dispatch: - schedule: - - cron: '0 0 1 * *' - -jobs: - delete-old-branch: - runs-on: ubuntu-22.04 - continue-on-error: true - steps: - - name: Delete old contributors-update branch - uses: dawidd6/action-delete-branch@v3 - with: - github_token: ${{secrets.GITHUB_TOKEN}} - branches: contributors-update - - add-contributors: - runs-on: ubuntu-22.04 - needs: delete-old-branch - steps: - - uses: actions/checkout@v3 - - - name: Create contributors-update branch - uses: peterjgrainger/action-create-branch@v2.4.0 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - branch: 'contributors-update' - - - name: Auto-add contributors - uses: BobAnkh/add-contributors@v0.2.2 - with: - BRANCH: 'contributors-update' - PULL_REQUEST: 'master' - CONTRIBUTOR: '## Contributors' - COLUMN_PER_ROW: '6' - ACCESS_TOKEN: ${{secrets.GITHUB_TOKEN}} - IMG_WIDTH: '100' - FONT_SIZE: '14' - PATH: '/CONTRIBUTORS.md' - COMMIT_MESSAGE: 'docs(CONTRIBUTORS): update contributors' - AVATAR_SHAPE: 'round' - - # This workflow will not trigger a `pull_request` event without a PAT. - # The lint workflow is not important for this type of PR, skip it and pretend it was successful: - - name: 'Get the latest commit hash from the contributors-update branch' - id: commit-data - run: | - git pull - git checkout contributors-update - echo "head_sha=$(git rev-parse contributors-update)" >>"${GITHUB_OUTPUT}" - - - name: 'Commit Status: Set Lint status to success (skipped)' - uses: myrotvorets/set-commit-status-action@1.1.6 - with: - token: ${{ secrets.GITHUB_TOKEN }} - # Skipped workflows are still assigned a "success" status: - status: success - # This should be the correct commit SHA on the contributors-update branch: - sha: ${{ steps.commit-data.outputs.head_sha }} - # Name of status check to add/update: - context: 'lint' - # Optional message/note we can inline to the right of the context name in the UI: - description: "Lint skipped. Not relevant." diff --git a/.github/workflows/docs-preview-deploy.yml b/.github/workflows/docs-preview-deploy.yml index bd57a819fd4..d48bca9e832 100644 --- a/.github/workflows/docs-preview-deploy.yml +++ b/.github/workflows/docs-preview-deploy.yml @@ -1,120 +1,166 @@ -name: 'Documentation (run)' +name: 'Documentation (Deploy)' on: + # This workflow runs off the primary branch which provides access to the `secrets` context: workflow_run: workflows: ['Documentation (PR)'] types: - completed -# Note: If limiting concurrency is required for this workflow: -# 1. Add an additional job prior to `preview` to get the PR number make it an output. -# 2. Assign that new job as a `needs` dependency for the `preview` job. -# It is still required for `preview` job to download the artifact so that it can access the preview build files. +permissions: + # Required by `actions/download-artifact`: + actions: read + # Required by `set-pr-context`: + contents: read + # Required by `marocchino/sticky-pull-request-comment` (write) + `set-pr-context` (read): + pull-requests: write + # Required by `myrotvorets/set-commit-status-action`: + statuses: write -# This workflow runs off the primary branch and has access to secrets as expected. jobs: - preview: + # NOTE: This is handled as pre-requisite job to minimize the noise from acquiring these two outputs needed for `deploy-preview` ENV: + pr-context: + name: 'Acquire PR Context' + runs-on: ubuntu-24.04 + outputs: + PR_HEADSHA: ${{ steps.set-pr-context.outputs.head-sha }} + PR_NUMBER: ${{ steps.set-pr-context.outputs.number }} + if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request' }} + steps: + - name: 'Get PR context' + id: set-pr-context + env: + # Token is required for the GH CLI: + GH_TOKEN: ${{ github.token }} + # Best practice for scripts is to reference via ENV at runtime. Avoid using GHA context expressions in the script content directly: + # https://github.com/docker-mailserver/docker-mailserver/pull/4247#discussion_r1827067475 + PR_TARGET_REPO: ${{ github.repository }} + # If the PR is from a fork, prefix it with `:`, otherwise only the PR branch name is relevant: + PR_BRANCH: |- + ${{ + (github.event.workflow_run.head_repository.owner.login != github.event.workflow_run.repository.owner.login) + && format('{0}:{1}', github.event.workflow_run.head_repository.owner.login, github.event.workflow_run.head_branch) + || github.event.workflow_run.head_branch + }} + # Use the GH CLI to query the PR branch, which provides the PR number and head SHA to assign as outputs: + # (`--jq` formats JSON to `key=value` pairs and renames `headRefOid` to `head-sha`) + run: | + gh pr view --repo "${PR_TARGET_REPO}" "${PR_BRANCH}" \ + --json 'number,headRefOid' \ + --jq '"number=\(.number)\nhead-sha=\(.headRefOid)"' \ + >> "${GITHUB_OUTPUT}" + + deploy-preview: name: 'Deploy Preview' - runs-on: ubuntu-22.04 - if: ${{ github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' }} + runs-on: ubuntu-24.04 + needs: [pr-context] + env: + # NOTE: Keep this in sync with the equivalent ENV in `docs-preview-prepare.yml`: + BUILD_DIR: docs/site/ + # PR head SHA (latest commit): + PR_HEADSHA: ${{ needs.pr-context.outputs.PR_HEADSHA }} + PR_NUMBER: ${{ needs.pr-context.outputs.PR_NUMBER }} + # Deploy URL preview prefix (the site name for this prefix is managed at Netlify): + PREVIEW_SITE_PREFIX: pullrequest-${{ needs.pr-context.outputs.PR_NUMBER }} steps: - - # ======================== # - # Restore workflow context # - # ======================== # - - # The official Github Action for downloading artifacts does not support multi-workflow - - name: 'Download build artifact' - uses: dawidd6/action-download-artifact@v2 + - name: 'Retrieve and extract the built docs preview' + uses: actions/download-artifact@v8 with: - github_token: ${{ secrets.GITHUB_TOKEN }} - run_id: ${{ github.event.workflow_run.id }} - workflow: docs-preview-prepare.yml name: preview-build - - - name: 'Extract build artifact' - run: tar -xf artifact.tar.zst - - - name: 'Restore preserved ENV' - run: cat pr.env >> "${GITHUB_ENV}" + path: ${{ env.BUILD_DIR }} + # These are needed due this approach relying on `workflow_run`, so that it can access the build artifact: + # (uploaded from the associated `docs-preview-prepare.yml` workflow run) + github-token: ${{ secrets.GITHUB_TOKEN }} + run-id: ${{ github.event.workflow_run.id }} # ==================== # # Deploy preview build # # ==================== # - # Manage workflow deployment status. `enable-commit-status` from `nwtgck/actions-netlify` would handle this, - # but presently does not work correctly via split workflow. It is useful in a split workflow as the 1st stage - # no longer indicates if the entire workflow/deployment was successful. - - name: 'Commit Status: Set Workflow Status as Pending' - uses: myrotvorets/set-commit-status-action@1.1.6 + # Manage workflow deployment status (Part 1/2): + # NOTE: + # - `workflow_run` trigger does not appear on the PR/commit checks status, only the initial prepare workflow triggered. + # This adds our own status check for this 2nd half of the workflow starting as `pending`, followed by `success` / `failure` at the end. + # - `enable-commit-status` from `nwtgck/actions-netlify` would have handled this, + # but the context `github.sha` that action tries to use references the primary branch commit that this workflow runs from, not the relevant PR commit. + - name: 'Commit Status (1/2) - Set Workflow Status as Pending' + uses: myrotvorets/set-commit-status-action@v2.0.1 with: token: ${{ secrets.GITHUB_TOKEN }} status: pending - # Should match `env.PR_HEADSHA` when triggered by `pull_request` event workflow, - # Avoids failure of ENV being unavailable if job fails early: - sha: ${{ github.event.workflow_run.head_sha }} + sha: ${{ env.PR_HEADSHA }} context: 'Deploy Preview (pull_request => workflow_run)' - name: 'Send preview build to Netlify' - uses: nwtgck/actions-netlify@v2.0 - id: preview + uses: nwtgck/actions-netlify@v3.0 + id: preview-netlify timeout-minutes: 1 env: NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} - NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} + NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} with: - github-token: ${{ secrets.GITHUB_TOKEN }} - # Fail the job early if credentials are missing / invalid: + # Fail the job when the required Netlify credentials are missing from ENV: fails-without-credentials: true - # Sets/creates the Netlify deploy URL prefix. - # Uses the PR number for uniqueness: - alias: ${{ env.NETLIFY_SITE_PREFIX }} + # Set/create the Netlify deploy URL prefix: + alias: ${{ env.PREVIEW_SITE_PREFIX }} # Only publish the contents of the build output: publish-dir: ${{ env.BUILD_DIR }} # Custom message for the deploy log on Netlify: - deploy-message: '${{ env.PR_TITLE }} (PR #${{ env.PR_NUMBER }} @ commit: ${{ env.PR_HEADSHA }})' - - # Note: Split workflow incorrectly references latest primary branch commit for deployment. - # Assign to non-default Deployment Environment for better management: - github-deployment-environment: documentation-previews - github-deployment-description: 'Preview deploy for documentation PRs' + deploy-message: 'Preview Build (PR #${{ env.PR_NUMBER }} @ commit: ${{ env.PR_HEADSHA }}' - # Note - PR context used by this action is incorrect. These features are broken with split workflow: - # https://github.com/nwtgck/actions-netlify/issues/545 # Disable unwanted action defaults: - # Disable adding deploy comment on pre-merge commit (Github creates this for PR diff): + # This input does not fallback to the GITHUB_TOKEN taken from context, nor log that it will skip extra features of the action when this input is not set: + # https://github.com/nwtgck/actions-netlify/issues/1219 + # github-token: ${{ secrets.GITHUB_TOKEN }} + # NOTE: These features won't work correctly when the triggered workflow is not run from the PR branch due to assumed `pull_request` context: + # https://github.com/nwtgck/actions-netlify/issues/545 + # Disable adding a comment to the commit belonging to context `github.sha` about the successful deployment (redundant and often wrong commit): enable-commit-comment: false - # Disable adding a "Netlify - Netlify deployment" check status: + # Disable adding a "Netlify - Netlify deployment" PR check status (workflow job status is sufficient): enable-commit-status: false - # Disable. We provide a custom PR comment in the next action: + # Disable adding a comment about successful deployment status to the PR. + # Prefer `marocchino/sticky-pull-request-comment` instead (more flexible and allows custom message): enable-pull-request-comment: false + # Opt-out of deployment feature: + # NOTE: + # - When affected by `nwtgck/actions-netlify/issues/545`, the deployments published reference the wrong commit and thus information. + # - While the feature creates or assigns a deployment to associate the build with, it is unrelated to the related environments feature (secrets/vars): + # https://github.com/nwtgck/actions-netlify/issues/538#issuecomment-833983970 + # https://docs.github.com/en/actions/managing-workflow-runs-and-deployments/managing-deployments/viewing-deployment-history + # https://docs.github.com/en/actions/managing-workflow-runs-and-deployments/managing-deployments/managing-environments-for-deployment + enable-github-deployment: false + # Assign to non-default Deployment Environment for better management: + # github-deployment-environment: documentation-previews + # github-deployment-description: 'Preview deploy for documentation PRs' # If a `netlify.toml` config is ever needed, enable this: # netlify-config-path: ./docs/netlify.toml - # If ever switching from Github Pages, enable this conditionally (false by default): + # If ever switching from Github Pages, enable this only when not deploying a preview build (false by default): # production-deploy: false - - name: 'Comment on PR: Add/Update deployment status' - uses: marocchino/sticky-pull-request-comment@v2 + - name: 'Comment on PR with preview link' + uses: marocchino/sticky-pull-request-comment@v3 with: number: ${{ env.PR_NUMBER }} header: preview-comment recreate: true message: | - [Documentation preview for this PR](${{ steps.preview.outputs.deploy-url }}) is ready! :tada: + [Documentation preview for this PR](${{ steps.preview-netlify.outputs.deploy-url }}) is ready! :tada: Built with commit: ${{ env.PR_HEADSHA }} - - name: 'Commit Status: Update deployment status' - uses: myrotvorets/set-commit-status-action@1.1.6 - # Always run this step regardless of job failing early: + # Manage workflow deployment status (Part 2/2): + - name: 'Commit Status (2/2) - Update deployment status' + uses: myrotvorets/set-commit-status-action@v2.0.1 + # Always run this step regardless of the job failing early: if: ${{ always() }} + # Custom status descriptions: env: DEPLOY_SUCCESS: Successfully deployed preview. DEPLOY_FAILURE: Failed to deploy preview. with: token: ${{ secrets.GITHUB_TOKEN }} status: ${{ job.status == 'success' && 'success' || 'failure' }} - sha: ${{ github.event.workflow_run.head_sha }} + sha: ${{ env.PR_HEADSHA }} context: 'Deploy Preview (pull_request => workflow_run)' description: ${{ job.status == 'success' && env.DEPLOY_SUCCESS || env.DEPLOY_FAILURE }} diff --git a/.github/workflows/docs-preview-prepare.yml b/.github/workflows/docs-preview-prepare.yml index a509ac778ff..d181683e11f 100644 --- a/.github/workflows/docs-preview-prepare.yml +++ b/.github/workflows/docs-preview-prepare.yml @@ -7,74 +7,68 @@ on: - '.github/workflows/scripts/docs/build-docs.sh' - '.github/workflows/docs-preview-prepare.yml' -# If the workflow for a PR is triggered multiple times, previous existing runs will be canceled. -# eg: Applying multiple suggestions from a review directly via the Github UI. -# Instances of the 2nd phase of this workflow (via `workflow_run`) presently lack concurrency limits due to added complexity. +# If this workflow is triggered while already running for the PR, cancel any earlier running instances: +# Instances of the 2nd phase of this workflow (via `workflow_run`) lack any concurrency limits due to added complexity. concurrency: group: deploypreview-pullrequest-${{ github.event.pull_request.number }} cancel-in-progress: true +env: + # Build output directory (created by the mkdocs-material container, keep this in sync with `build-docs.sh`): + BUILD_DIR: docs/site/ + # These two are only needed to construct `PREVIEW_URL`: + PREVIEW_SITE_NAME: dms-doc-previews + PREVIEW_SITE_PREFIX: pullrequest-${{ github.event.pull_request.number }} + # `pull_request` workflow is unreliable alone: Non-collaborator contributions lack access to secrets for security reasons. # A separate workflow (docs-preview-deploy.yml) handles the deploy after the potentially untrusted code is first run in this workflow. # See: https://securitylab.github.com/research/github-actions-preventing-pwn-requests/ permissions: + # Required by `actions/checkout` for git checkout: contents: read jobs: prepare-preview: name: 'Build Preview' - runs-on: ubuntu-22.04 - env: - BUILD_DIR: docs/site - NETLIFY_SITE_PREFIX: pullrequest-${{ github.event.pull_request.number }} - NETLIFY_SITE_NAME: dms-doc-previews + runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 + + # ================== # + # Build docs preview # + # ================== # - name: 'Build with mkdocs-material via Docker' - working-directory: docs + working-directory: docs/ env: - PREVIEW_URL: 'https://${NETLIFY_SITE_PREFIX}--${NETLIFY_SITE_NAME}.netlify.app/' - NETLIFY_BRANDING: 'Deploys by Netlify' + PREVIEW_URL: 'https://${{ env.PREVIEW_SITE_PREFIX }}--${{ env.PREVIEW_SITE_NAME }}.netlify.app/' run: | - # Adjust mkdocs.yml for preview build - sed -i "s|^site_url:.*|site_url: '${PREVIEW_URL}'|" mkdocs.yml + # Adjust `mkdocs.yml` for the preview build requirements: + # - Replace production `site_url` with the preview URL (only affects the canonical link: https://en.wikipedia.org/wiki/Canonical_link_element#HTML) + # - Prepend Netlify logo link to `copyright` content + sed -i "s|^site_url:.*|site_url: '${{ env.PREVIEW_URL }}'|" mkdocs.yml - # Insert sponsor branding into page content (Provider OSS plan requirement): - # Upstream does not provide a nicer maintainable way to do this.. - # Prepends HTML to copyright text and then aligns to the right side. + # Insert branding into page content (Netlify OSS plan requirement): + # - `mkdocs-material` does not provide a better way to do this. + # - Prepends HTML to the copyright text and then aligns the logo to the right-side of the page. + NETLIFY_BRANDING='Deploys by Netlify' sed -i "s|^copyright: '|copyright: '${NETLIFY_BRANDING}|" mkdocs.yml - # Need to override a CSS media query for parent element to always be full width: + # Override a CSS media query for the parent element to always be full width: echo '.md-footer-copyright { width: 100%; }' >> content/assets/css/customizations.css - ../.github/workflows/scripts/docs/build-docs.sh + # Build and prepare for upload: + echo "::group::Build (stdout)" + bash ../.github/workflows/scripts/docs/build-docs.sh + echo "::endgroup::" # ============================== # # Volley over to secure workflow # # ============================== # - # Minimize risk of upload failure by bundling files to a single compressed archive (tar + zstd). - # Bundles build dir and env file into a compressed archive, nested file paths will be preserved. - - name: 'Prepare artifact for transfer' - env: - # As a precaution, reference this value by an interpolated ENV var; - # instead of interpolating user controllable input directly in the shell script.. - # https://github.com/docker-mailserver/docker-mailserver/issues/2332#issuecomment-998326798 - PR_TITLE: ${{ github.event.pull_request.title }} - run: | - # Save ENV for transfer - { - echo "PR_HEADSHA=${{ github.event.pull_request.head.sha }}" - echo "PR_NUMBER=${{ github.event.pull_request.number }}" - echo "PR_TITLE=${PR_TITLE}" - echo "NETLIFY_SITE_PREFIX=${{ env.NETLIFY_SITE_PREFIX }}" - echo "BUILD_DIR=${{ env.BUILD_DIR }}" - } >> pr.env - tar --zstd -cf artifact.tar.zst pr.env ${{ env.BUILD_DIR }} - + # Archives directory `path` into a ZIP file: - name: 'Upload artifact for workflow transfer' - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v7 with: name: preview-build - path: artifact.tar.zst + path: ${{ env.BUILD_DIR }} retention-days: 1 diff --git a/.github/workflows/docs-production-deploy.yml b/.github/workflows/docs-production-deploy.yml index 0e898952bcd..f6a9968238c 100644 --- a/.github/workflows/docs-production-deploy.yml +++ b/.github/workflows/docs-production-deploy.yml @@ -26,9 +26,9 @@ jobs: permissions: contents: write name: 'Deploy Docs' - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 - name: 'Check if deploy is for a `v.` tag version instead of `edge`' if: startsWith(github.ref, 'refs/tags/') @@ -55,11 +55,11 @@ jobs: # Replace the tagged '${DOCS_VERSION}' in the 'canonical' link element of HTML files, # to point to the 'edge' version of docs as the authoritative source: find . -type f -name "*.html" -exec \ - sed -i "s|^\(.*>"${GITHUB_ENV}" + rm latest + ln -s "${DOCS_VERSION}" latest + + - name: 'Push update for `latest` symlink' + run: | + git config user.name ${{ env.GIT_USER }} + git config user.email ${{ env.GIT_EMAIL }} + git add latest + git commit -m "chore: Update \`latest\` symlink to point to ${{ env.DOCS_VERSION }}" + git push diff --git a/.github/workflows/generic_build.yml b/.github/workflows/generic_build.yml index c7857e4838a..89462cf5c1a 100644 --- a/.github/workflows/generic_build.yml +++ b/.github/workflows/generic_build.yml @@ -23,16 +23,16 @@ permissions: jobs: build-image: name: 'Build' - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 outputs: build-cache-key: ${{ steps.derive-image-cache-key.outputs.digest }} steps: - name: 'Checkout' - uses: actions/checkout@v3 + uses: actions/checkout@v6 with: submodules: recursive - # Can potentially be replaced by: `${{ hashFiles('target/**', 'Dockerfile', 'VERSION') }}` + # Can potentially be replaced by: `${{ hashFiles('target/**', 'Dockerfile') }}` # Must not be affected by file metadata changes and have a consistent sort order: # https://docs.github.com/en/actions/learn-github-actions/expressions#hashfiles # Keying by the relevant build context is more re-usable than a commit SHA. @@ -40,10 +40,7 @@ jobs: id: derive-image-cache-key shell: bash run: | - ADDITIONAL_FILES=( - 'Dockerfile' - 'VERSION' - ) + ADDITIONAL_FILES=( 'Dockerfile' ) # Recursively collect file paths from `target/` and pipe a list of # checksums to be sorted (by hash value) and finally generate a checksum @@ -64,7 +61,7 @@ jobs: # When full, the least accessed cache upload is evicted to free up storage. # https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows - name: 'Handle Docker build layer cache' - uses: actions/cache@v3 + uses: actions/cache@v5 with: path: /tmp/.buildx-cache key: cache-buildx-${{ steps.derive-image-cache-key.outputs.digest }} @@ -74,16 +71,16 @@ jobs: cache-buildx- - name: 'Set up QEMU' - uses: docker/setup-qemu-action@v2.1.0 + uses: docker/setup-qemu-action@v4.0.0 with: platforms: arm64 - name: 'Set up Docker Buildx' - uses: docker/setup-buildx-action@v2.4.1 + uses: docker/setup-buildx-action@v4.0.0 # NOTE: AMD64 can build within 2 minutes - name: 'Build images' - uses: docker/build-push-action@v4.0.0 + uses: docker/build-push-action@v7.0.0 with: context: . # Build at least the AMD64 image (which runs against the test suite). diff --git a/.github/workflows/generic_publish.yml b/.github/workflows/generic_publish.yml index ae94fc2dc7e..1042d7b6d95 100644 --- a/.github/workflows/generic_publish.yml +++ b/.github/workflows/generic_publish.yml @@ -14,16 +14,16 @@ permissions: jobs: publish-images: name: 'Publish' - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - name: 'Checkout' - uses: actions/checkout@v3 + uses: actions/checkout@v6 with: submodules: recursive - name: 'Prepare tags' id: prep - uses: docker/metadata-action@v4.3.0 + uses: docker/metadata-action@v6.0.0 with: images: | ${{ secrets.DOCKER_REPOSITORY }} @@ -35,18 +35,18 @@ jobs: type=semver,pattern={{major}}.{{minor}}.{{patch}} - name: 'Set up QEMU' - uses: docker/setup-qemu-action@v2.1.0 + uses: docker/setup-qemu-action@v4.0.0 with: platforms: arm64 - name: 'Set up Docker Buildx' - uses: docker/setup-buildx-action@v2.4.1 + uses: docker/setup-buildx-action@v4.0.0 # Try get the cached build layers from a prior `generic_build.yml` job. # NOTE: Until adopting `type=gha` scoped cache exporter (in `docker/build-push-action`), # only AMD64 image is expected to be cached, ARM images will build from scratch. - name: 'Retrieve image build from build cache' - uses: actions/cache@v3 + uses: actions/cache@v5 with: path: /tmp/.buildx-cache key: cache-buildx-${{ inputs.cache-key }} @@ -54,30 +54,25 @@ jobs: cache-buildx- - name: 'Login to DockerHub' - uses: docker/login-action@v2 + uses: docker/login-action@v4 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: 'Login to GitHub Container Registry' - uses: docker/login-action@v2 + uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: 'Acquire the image version' - id: get-version - shell: bash - run: echo "version=$(>"${GITHUB_OUTPUT}" - - name: 'Build and publish images' - uses: docker/build-push-action@v4.0.0 + uses: docker/build-push-action@v7.0.0 with: context: . build-args: | + DMS_RELEASE=${{ github.ref_type == 'tag' && github.ref_name || 'edge' }} VCS_REVISION=${{ github.sha }} - VCS_VERSION=${{ steps.get-version.outputs.version }} platforms: linux/amd64,linux/arm64 push: true tags: ${{ steps.prep.outputs.tags }} diff --git a/.github/workflows/generic_test.yml b/.github/workflows/generic_test.yml index 43c6e8e5be3..cd3a6b4f1fe 100644 --- a/.github/workflows/generic_test.yml +++ b/.github/workflows/generic_test.yml @@ -13,14 +13,14 @@ permissions: jobs: run-tests: name: 'Test' - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 strategy: matrix: part: [serial, parallel/set1, parallel/set2, parallel/set3] fail-fast: false steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v6 with: # Required to retrieve bats (core + extras): submodules: recursive @@ -29,7 +29,7 @@ jobs: # This should always be a cache-hit, thus `restore-keys` fallback is not used. # No new cache uploads should ever happen for this job. - name: 'Retrieve image built from build cache' - uses: actions/cache@v3 + uses: actions/cache@v5 with: path: /tmp/.buildx-cache key: cache-buildx-${{ inputs.cache-key }} @@ -38,12 +38,12 @@ jobs: # Ensures consistent BuildKit version (not coupled to Docker Engine), # and increased compatibility of the build cache vs mixing buildx drivers. - name: 'Set up Docker Buildx' - uses: docker/setup-buildx-action@v2.4.1 + uses: docker/setup-buildx-action@v4.0.0 # Importing from the cache should create the image within approx 30 seconds: # NOTE: `qemu` step is not needed as we only test for AMD64. - name: 'Build AMD64 image from cache' - uses: docker/build-push-action@v4.0.0 + uses: docker/build-push-action@v7.0.0 with: context: . tags: mailserver-testing:ci diff --git a/.github/workflows/generic_vulnerability-scan.yml b/.github/workflows/generic_vulnerability-scan.yml index 84022bcb2b5..b8bd111c039 100644 --- a/.github/workflows/generic_vulnerability-scan.yml +++ b/.github/workflows/generic_vulnerability-scan.yml @@ -19,16 +19,16 @@ jobs: permissions: contents: read # for actions/checkout to fetch code security-events: write # for github/codeql-action/upload-sarif to upload SARIF results - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - name: 'Checkout' - uses: actions/checkout@v3 + uses: actions/checkout@v6 # Get the cached build layers from the build job: # This should always be a cache-hit, thus `restore-keys` fallback is not used. # No new cache uploads should ever happen for this job. - name: 'Retrieve image built from build cache' - uses: actions/cache@v3 + uses: actions/cache@v5 with: path: /tmp/.buildx-cache key: cache-buildx-${{ inputs.cache-key }} @@ -37,12 +37,12 @@ jobs: # Ensures consistent BuildKit version (not coupled to Docker Engine), # and increased compatibility of the build cache vs mixing buildx drivers. - name: 'Set up Docker Buildx' - uses: docker/setup-buildx-action@v2.4.1 + uses: docker/setup-buildx-action@v4.0.0 # Importing from the cache should create the image within approx 30 seconds: # NOTE: `qemu` step is not needed as we only test for AMD64. - name: 'Build AMD64 image from cache' - uses: docker/build-push-action@v4.0.0 + uses: docker/build-push-action@v7.0.0 with: context: . tags: mailserver-testing:ci @@ -55,13 +55,13 @@ jobs: provenance: false - name: 'Run the Anchore Grype scan action' - uses: anchore/scan-action@v3.3.4 + uses: anchore/scan-action@v7.4.0 id: scan with: image: mailserver-testing:ci fail-build: false - name: 'Upload vulnerability report' - uses: github/codeql-action/upload-sarif@v2 + uses: github/codeql-action/upload-sarif@v4 with: sarif_file: ${{ steps.scan.outputs.sarif }} diff --git a/.github/workflows/handle_stalled.yml b/.github/workflows/handle_stalled.yml index 78bdb26b08b..fd5f87d5091 100644 --- a/.github/workflows/handle_stalled.yml +++ b/.github/workflows/handle_stalled.yml @@ -5,17 +5,17 @@ on: - cron: "0 1 * * *" permissions: + actions: write contents: read + issues: write + pull-requests: write jobs: stale: - permissions: - issues: write - pull-requests: write - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - name: Close stale issues - uses: actions/stale@v7 + uses: actions/stale@v10 with: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-stale: 20 diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index e9339a671b3..23f6fa20dff 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -1,6 +1,9 @@ name: Lint on: + # A workflow that creates a PR will not trigger this workflow, + # Providing a manual trigger as a workaround + workflow_dispatch: pull_request: push: branches: [ master ] @@ -10,10 +13,10 @@ permissions: jobs: lint: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v6 - name: Hadolint run: make hadolint diff --git a/.github/workflows/scheduled_builds.yml b/.github/workflows/scheduled_builds.yml index ef198a345fb..f12f84a118f 100644 --- a/.github/workflows/scheduled_builds.yml +++ b/.github/workflows/scheduled_builds.yml @@ -1,12 +1,14 @@ name: 'Deploy :edge on Schedule' on: + workflow_dispatch: schedule: - cron: 0 0 * * 5 permissions: contents: read packages: write + security-events: write jobs: build-images: diff --git a/.github/workflows/scripts/docs/build-docs.sh b/.github/workflows/scripts/docs/build-docs.sh index cf059709032..4eac8c4dd95 100755 --- a/.github/workflows/scripts/docs/build-docs.sh +++ b/.github/workflows/scripts/docs/build-docs.sh @@ -7,10 +7,11 @@ set -ex # `build --strict` ensures the build fails when any warnings are omitted. docker run \ --rm \ + --quiet \ --user "$(id -u):$(id -g)" \ - --volume "${PWD}:/docs" \ + --volume "./:/docs" \ --name "build-docs" \ - squidfunk/mkdocs-material:8.3.9 build --strict + squidfunk/mkdocs-material:9.6 build --strict # Remove unnecessary build artifacts: https://github.com/squidfunk/mkdocs-material/issues/2519 # site/ is the build output folder. diff --git a/.gitignore b/.gitignore index 50d22a228ec..79a4dc3cd85 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ ################################################# .env +compose.override.yaml docs/site/ docker-data/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 87f18c888cd..ec0af10293a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,699 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased](https://github.com/docker-mailserver/docker-mailserver/compare/v11.3.0...HEAD) +## [Unreleased](https://github.com/docker-mailserver/docker-mailserver/compare/v15.1.0...HEAD) > **Note**: Changes and additions listed here are contained in the `:edge` image tag. These changes may not be as stable as released changes. +### Fixed + +- **Rspamd:** + - Configuration changes now trigger a service reload instead of a restart ([#4632](https://github.com/docker-mailserver/docker-mailserver/pull/4632)) + - `expand_keys = true` has been removed from the Redis configuration ([#4689](https://github.com/docker-mailserver/docker-mailserver/pull/4689)) +- **Internal:** + - `ENABLE_QUOTAS=1` - When an alias has multiple addresses, the first local mailbox address found will be used for the Dovecot dummy account workaround ([#4581](https://github.com/docker-mailserver/docker-mailserver/pull/4581)) + - Change Detection service - Added support for responding to updated DMS config (_Rspamd and TLS certificates_) to `ACCOUNT_PROVISIONER=LDAP` ([#4627](https://github.com/docker-mailserver/docker-mailserver/pull/4627)) +- **Tests:** + - Make the helper method `_get_container_ip()` compatible with Docker 29 ([#4606](https://github.com/docker-mailserver/docker-mailserver/pull/4606)) + +### Removed + +- **SpamAssassin:** + - Removed Pyzor + Razor due to maintenance concerns. From observations it is unlikely to have any notable regression ([#4548](https://github.com/docker-mailserver/docker-mailserver/pull/4548)) + +### Updated + +- **Documentation:** + - The maintenance page (covering `watchtower` guidance) was revised and migrated to direct users to the maintained community fork [`nicholas-fedor/watchtower`](https://github.com/nicholas-fedor/watchtower) ([#4641](https://github.com/docker-mailserver/docker-mailserver/pull/4641)) +- **Internal:** + - Aligning with the change in upstream Debian, APT package repositories added by DMS have migrated the format from `.list` to `.sources` ([DEB822](https://repolib.readthedocs.io/en/latest/deb822-format.html)) ([#4556](https://github.com/docker-mailserver/docker-mailserver/pull/4556)) + - Third-party sourced CLI tools updated ([#4557](https://github.com/docker-mailserver/docker-mailserver/pull/4557)): + - `jaq` from `2.1.0` to [`2.3.0`](https://github.com/01mf02/jaq/releases/tag/v2.3.0) + - `step` CLI from `0.28.2` to [`0.28.7`](https://github.com/smallstep/cli/releases/tag/v0.28.7)) + - DMS logs now all output to STDERR (formerly only warning/error logs) (#[4586](https://github.com/docker-mailserver/docker-mailserver/pull/4586)) +- **Dovecot** + - Updated the FTS plugin Xapian from `1.9` to [`1.9.1`](https://github.com/grosjo/fts-xapian/releases/tag/1.9.1) which adds Dovecot 2.4 compatibility ([#4557](https://github.com/docker-mailserver/docker-mailserver/pull/4557)) +- **Postfix** + - Replaced `disable_dns_lookups` with `smtp_dns_support_level` in Amavis configuration ([#4568](https://github.com/docker-mailserver/docker-mailserver/pull/4568)) + +## [v15.1.0](https://github.com/docker-mailserver/docker-mailserver/compare/v15.1.0...HEAD) + +> [!NOTE] +> +> This release is the last release before we start with breaking changes for the transition to Debian 13. + +### Added + +- **Environment Variables:** + - [ENV can be declared with a `__FILE` suffix](https://docker-mailserver.github.io/docker-mailserver/v15.1/config/environment/) to read a value from a file during initial DMS setup scripts ([#4359](https://github.com/docker-mailserver/docker-mailserver/pull/4359)) + - Improved docs for the ENV `OVERRIDE_HOSTNAME` ([#4492](https://github.com/docker-mailserver/docker-mailserver/pull/4492)) +- **Internal:** + - [`DMS_CONFIG_POLL`](https://docker-mailserver.github.io/docker-mailserver/v15.1/config/environment/#dms_config_poll) supports adjusting the polling rate (seconds) for the change detection service `check-for-changes.sh` ([#4450](https://github.com/docker-mailserver/docker-mailserver/pull/4450)) + +### Fixes + +- **DKIM** + - `setup config dkim domain subdomain.example.com` no longer throws an error if the owner of config/opendkim/keys does not exist in the container ([#4517](https://github.com/docker-mailserver/docker-mailserver/pull/4517)) +- **Fail2Ban** + - Configure logrotate only when Fail2Ban is enabled ([#4493](https://github.com/docker-mailserver/docker-mailserver/pull/4523)) +- **Internal:** + - The DMS _Config Volume_ (`/tmp/docker-mailserver`) will now ensure it's file tree is accessible for services when the volume was created with missing executable bit ([#4487](https://github.com/docker-mailserver/docker-mailserver/pull/4487)) + - Removed the build-time hostname workaround for Postfix as Debian has since patched their post-install script ([#4493](https://github.com/docker-mailserver/docker-mailserver/pull/4493)) + - Fixed various typos across codebase ([#4552](https://github.com/docker-mailserver/docker-mailserver/pull/4552)) + +### Updates + +- **Documentation:** + - Added a compatibility note for a Dovecot + Solr 9.8 breaking change ([#4433](https://github.com/docker-mailserver/docker-mailserver/pull/4433)) + - Updated the Podman documentation with deprecation warnings and up-to-date technologies such as rootless Quadlets and Pasta networking ([#4183](https://github.com/docker-mailserver/docker-mailserver/pull/4183)) + - `setup config dkim` (rspamd) - Corrected the expected path for the generated `dkim_signing.conf` file is now found in the DMS config volume ([#4521](https://github.com/docker-mailserver/docker-mailserver/issues/4521)) +- **Internal:** + - Refactored `setup config dkim` (`open-dkim`) ([#4375](https://github.com/docker-mailserver/docker-mailserver/pull/4375)) + - `setup email list` and the default `ENABLE_QUOTAS=1` ENV now better communicates when config is incompatible ([#4453](https://github.com/docker-mailserver/docker-mailserver/pull/4453)) + +### Removed + +- **Fail2Ban** + - Removed `postfix-sasl` jail by default as it is covered by `postfix[mode=extra]` already ([#4535](https://github.com/docker-mailserver/docker-mailserver/pull/4535)) + +## [v15.0.2](https://github.com/docker-mailserver/docker-mailserver/releases/tag/v15.0.2) + +### Fixes + +- **Postfix** + - Avoid modifying the message body when filtering sender headers. This regression was introduced from [#4120](https://github.com/docker-mailserver/docker-mailserver/pull/4120) as part of DMS v15.0.0 ([#4429](https://github.com/docker-mailserver/docker-mailserver/pull/4429)) + +## [v15.0.1](https://github.com/docker-mailserver/docker-mailserver/releases/tag/v15.0.1) + +### Added + +- **Internal:** + - Added the Smallstep `step` CLI command for future internal usage ([#4376](https://github.com/docker-mailserver/docker-mailserver/pull/4376)) + +### Fixes + +- **Postfix:** + - `setup email restrict` generated configs now only prepend to `dms_smtpd_sender_restrictions` ([#4379](https://github.com/docker-mailserver/docker-mailserver/pull/4379)) +- **Rspamd:** + - Change detection support now monitors all files found within the DMS _Config Volume_ Rspamd directory ([#4418](https://github.com/docker-mailserver/docker-mailserver/pull/4418)) +- **Internal:** + - A permissions fix for `/var/log/mail` that was [added in DMS v15]((https://github.com/docker-mailserver/docker-mailserver/pull/4374)) no longer encounters an error when no log files are present during a container restart, such as with a `tmpfs` volume mount ([#4391](https://github.com/docker-mailserver/docker-mailserver/pull/4391)) + - The DMS _State Volume_ (`/var/mail-state`) will now ensure it's file tree is accessible for services when the volume was created with missing executable bit ([#4420](https://github.com/docker-mailserver/docker-mailserver/pull/4420)) + - The DMS _Config Volume_ (`/tmp/docker-mailserver`) now correctly updates permissions on container restarts ([#4417](https://github.com/docker-mailserver/docker-mailserver/pull/4417)) + +### Updates + +- **Internal:** + - Minor improvements to `_install_utils()` in `packages.sh` ([#4376](https://github.com/docker-mailserver/docker-mailserver/pull/4376)) + +## [v15.0.0](https://github.com/docker-mailserver/docker-mailserver/releases/tag/v15.0.0) + +### Breaking + +- **saslauthd** mechanism support via ENV `SASLAUTHD_MECHANISMS` with `pam`, `shadow`, `mysql` values has been removed. Only `ldap` and `rimap` remain supported ([#4259](https://github.com/docker-mailserver/docker-mailserver/pull/4259)) +- **getmail6** has been refactored: ([#4156](https://github.com/docker-mailserver/docker-mailserver/pull/4156)) + - The [DMS config volume](https://docker-mailserver.github.io/docker-mailserver/v15.0/config/advanced/optional-config/#volumes) now has support for `getmailrc_general.cf` for overriding [common default settings](https://docker-mailserver.github.io/docker-mailserver/v15.0/config/advanced/mail-getmail/#common-options). If you previously mounted this config file directly to `/etc/getmailrc_general` you should switch to our config volume support. + - Generated getmail configuration files no longer set the `message_log` option. Instead of individual log files per config, the [default base settings DMS configures](https://github.com/docker-mailserver/docker-mailserver/tree/v15.0.0/target/getmail/getmailrc_general) now enables `message_log_syslog`. This aligns with how other services in DMS log to syslog where it is captured in `mail.log`. + - Getmail configurations have changed location from the base of the DMS Config Volume, to the `getmail/` subdirectory. Any existing configurations **must be migrated manually.** + - **DMS v14 mistakenly** relocated the _getmail state directory_ to the _DMS Config Volume_ as a `getmail/` subdirectory. + - This has been corrected to `/var/lib/getmail` (_if you have mounted a DMS State Volume to `/var/mail-state`, `/var/lib/getmail` will be symlinked to `/var/mail-state/lib-getmail`_). + - To preserve this state when upgrading to DMS v15, **you must manually migrate `getmail/` from the _DMS Config Volume_ to `lib-getmail/` in the _DMS State Volume_.** + - `setup email delete ` now requires explicit confirmation if the mailbox data should be deleted ([#4365](https://github.com/docker-mailserver/docker-mailserver/pull/4365)). +- **Rspamd:** Removed deprecated file path check (_DMS config volume: `./rspamd-modules.conf` => `./rspamd/custom-commands.conf`_) ([#4373](https://github.com/docker-mailserver/docker-mailserver/pull/4373)) + +### Added + +- **Internal:** + - Add password confirmation to several `setup` CLI subcommands ([#4072](https://github.com/docker-mailserver/docker-mailserver/pull/4072)) + - Added a `debug getmail` subcommand to `setup` ([#4346](https://github.com/docker-mailserver/docker-mailserver/pull/4346)) + +### Updates + +- **Internal:** + - **Removed `VERSION` file** from the repo. Releases of DMS prior to v13 (Nov 2023) would check this to detect new releases ([#3677](https://github.com/docker-mailserver/docker-mailserver/issues/3677), [#4321](https://github.com/docker-mailserver/docker-mailserver/pull/4321)) + - During image build, ensure a secure connection when downloading the `fail2ban` package ([#4080](https://github.com/docker-mailserver/docker-mailserver/pull/4080)) +- **Documentation:** + - Account Management and Authentication pages have been rewritten and better organized ([#4122](https://github.com/docker-mailserver/docker-mailserver/pull/4122)) + - Add a caveat for `DMS_VMAIL_UID` not being compatible with `0` / root ([#4143](https://github.com/docker-mailserver/docker-mailserver/pull/4143)) +- **Getmail:** ([#4156](https://github.com/docker-mailserver/docker-mailserver/pull/4156)) + - Added `getmail` as a new service for `supervisor` to manage, replacing cron for periodic polling. + - IMAP/POP3 example configs added to our [`config-examples`](https://github.com/docker-mailserver/docker-mailserver/tree/v15.0.0/config-examples/getmail). + - ENV [`GETMAIL_POLL`](https://docker-mailserver.github.io/docker-mailserver/v15.0/config/environment/#getmail_poll) now supports values above 30 minutes. +- **Postfix:** + - By default opt-out from _Microsoft reactions_ for outbound mail ([#4120](https://github.com/docker-mailserver/docker-mailserver/pull/4120)) +- **Rspamd:** + - Updated GTube settings and tests ([#4191](https://github.com/docker-mailserver/docker-mailserver/pull/4191)) +- Updated externally installed software ([#4357](https://github.com/docker-mailserver/docker-mailserver/pull/4357)): + - `DOVECOT_COMMUNITY_REPO=1` custom image build ARG now supports the latest Dovecot [`2.4.x`](https://github.com/dovecot/core/releases/tag/2.4.0) (_DMS provides Dovecot `2.3.19` by default_) + - Dovecot FTS Xapian module (`1.7.12` => [`1.9.0`](https://github.com/grosjo/fts-xapian/releases/tag/1.9)) + - `jaq` (`1.3.0` => [`2.1.0`](https://github.com/01mf02/jaq/releases/tag/v2.1.0)) + - Fail2Ban (`1.0.2-2` => [`1.1.0`](https://github.com/fail2ban/fail2ban/releases/tag/1.1.0)) ([#4045](https://github.com/docker-mailserver/docker-mailserver/pull/4045)) + - Rspamd (`3.8.4` => [`3.11.0`](https://github.com/rspamd/rspamd/releases/tag/3.11.0)) - Implicitly upgraded during image build, as the third-party repo lacks version pinning support. + +### Fixes + +- **Dovecot:** + - The logwatch `ignore.conf` now also excludes Xapian messages about pending documents ([#4060](https://github.com/docker-mailserver/docker-mailserver/pull/4060)) + - `dovecot-fts-xapian` plugin was updated, fixing a regression with indexing ([#4095](https://github.com/docker-mailserver/docker-mailserver/pull/4095)) + - The "dummy account" workaround for _Dovecot Quota_ feature support no longer treats the alias as a regex when checking the Dovecot UserDB ([#4222](https://github.com/docker-mailserver/docker-mailserver/pull/4222)) +- **LDAP:** + - Correctly apply a compatibility fix for OAuth2 introduced in DMS `v13.3.1` which had not been applied to the actual LDAP config changes ([#4175](https://github.com/docker-mailserver/docker-mailserver/pull/4175)) +- **Internal:** + - The main `mail.log` (_which is piped to stdout via `tail`_) now correctly begins from the first log line of the active container run. Previously some daemon logs and potential warnings/errors were omitted ([#4146](https://github.com/docker-mailserver/docker-mailserver/pull/4146)) + - `start-mailserver.sh` removed unused `shopt -s inherit_errexit` ([#4161](https://github.com/docker-mailserver/docker-mailserver/pull/4161)) + - Fixed a regression introduced in DMS v14 where `postfix-main.cf` appended `stderr` output into `/etc/postfix/main.cf`, causing Postfix startup to fail ([#4147](https://github.com/docker-mailserver/docker-mailserver/pull/4147)) + - Fixed a regression introduced in DMS v14 to better support running `start-mailserver.sh` with container restarts, which now only skip calling `_setup()` ([#4323](https://github.com/docker-mailserver/docker-mailserver/pull/4323#issuecomment-2629559254), [#4374](https://github.com/docker-mailserver/docker-mailserver/pull/4374)) + - The command `swaks --help` is now functional ([#4282](https://github.com/docker-mailserver/docker-mailserver/pull/4282)) +- **Rspamd:** + - DKIM private key path checking is now performed only on paths that do not contain `$` ([#4201](https://github.com/docker-mailserver/docker-mailserver/pull/4201)) + +### CI + +- Removed `CONTRIBUTORS.md`, `.all-contributorsrc`, and workflow ([#4141](https://github.com/docker-mailserver/docker-mailserver/pull/4141)) +- Refactored the workflows to be more secure for generating documentation previews on PRs ([#4267](https://github.com/docker-mailserver/docker-mailserver/pull/4267), [#4264](https://github.com/docker-mailserver/docker-mailserver/pull/4264), [#4262](https://github.com/docker-mailserver/docker-mailserver/pull/4262), [#4247](https://github.com/docker-mailserver/docker-mailserver/pull/4247), [#4244](https://github.com/docker-mailserver/docker-mailserver/pull/4244)) + +## [v14.0.0](https://github.com/docker-mailserver/docker-mailserver/releases/tag/v14.0.0) + +The most noteworthy change of this release is the update of the container's base image from Debian 11 ("Bullseye") to Debian 12 ("Bookworm"). This update alone involves breaking changes and requires a careful update! + +### Breaking + +- **Updated base image to Debian 12** ([#3403](https://github.com/docker-mailserver/docker-mailserver/pull/3403)) + - Changed the default of `DOVECOT_COMMUNITY_REPO` to `0` (disabled) - the Dovecot community repo will (for now) not be the default when building the DMS. + - While Debian 12 (Bookworm) was released in June 2023 and the latest Dovecot `2.3.21` in Sep 2023, as of Jan 2024 there is no [Dovecot community repo available for Debian 12](https://repo.dovecot.org). + - This results in the Dovecot version being downgraded from `2.3.21` (DMS v13.3) to `2.3.19`, which [may affect functionality when you've explicitly configured for these features](https://github.com/dovecot/core/blob/30cde20f63650d8dcc4c7ad45418986f03159946/NEWS#L1-L158): + - OAuth2 (_mostly regarding JWT usage, or POST requests (`introspection_mode = post`) with `client_id` + `client_secret`_). + - Lua HTTP client (_DNS related_). + - Updated packages. For an overview, [we have a review comment on the PR that introduces Debian 12](https://github.com/docker-mailserver/docker-mailserver/pull/3403#issuecomment-1694563615) + - Notable major version bump: `openssl 3`, `clamav 1`, `spamassassin 4`, `redis-server 7`. + - Notable minor version bump: `postfix 3.5.23 => 3.7.9` + - Notable minor version bump + downgrade: `dovecot 2.3.13 => 2.3.19` (_Previous release provided `2.3.21` via community repo, `2.3.19` is now the default_) + - Updates to `packages.sh`: + - Removed custom installations of Fail2Ban, getmail6 and Rspamd + - Updated packages lists and added comments for maintainability +- OpenDMARC upgrade: `v1.4.0` => `v1.4.2` ([#3841](https://github.com/docker-mailserver/docker-mailserver/pull/3841)) + - Previous versions of OpenDMARC would place incoming mail from domains announcing `p=quarantine` (_that fail the DMARC check_) into the [Postfix "hold" queue](https://www.postfix.org/QSHAPE_README.html#hold_queue) until administrative intervention. + - [OpenDMARC v1.4.2 has disabled that feature by default](https://github.com/trusteddomainproject/OpenDMARC/issues/105), but it can be enabled again by adding the setting `HoldQuarantinedMessages true` to [`/etc/opendmarc.conf`](https://github.com/docker-mailserver/docker-mailserver/blob/v13.3.1/target/opendmarc/opendmarc.conf) (_provided from DMS_). + - [Our `user-patches.sh` feature](https://docker-mailserver.github.io/docker-mailserver/latest/config/advanced/override-defaults/user-patches/) provides a convenient approach to updating that config file. + - Please let us know if you disagree with the upstream default being carried with DMS, or the value of providing alternative configuration support within DMS. +- **Postfix:** + - Postfix upgrade from 3.5 to 3.7 ([#3403](https://github.com/docker-mailserver/docker-mailserver/pull/3403)) + - `compatibility_level` was raised from `2` to `3.6` + - Postfix has deprecated the usage of `whitelist` / `blacklist` in config parameters and logging in favor of `allowlist` / `denylist` and similar variations. ([#3403](https://github.com/docker-mailserver/docker-mailserver/pull/3403/files#r1306356328)) + - This [may affect monitoring / analysis of logs output from Postfix](https://www.postfix.org/COMPATIBILITY_README.html#respectful_logging) that expects to match patterns on the prior terminology used. + - DMS `main.cf` has renamed `postscreen_dnsbl_whitelist_threshold` to `postscreen_dnsbl_allowlist_threshold` as part of this change. + - `smtpd_relay_restrictions` (relay policy) is now evaluated after `smtpd_recipient_restrictions` (spam policy). Previously it was evaluated before `smtpd_recipient_restrictions`. Mail to be relayed via DMS must now pass through the spam policy first. + - The TLS fingerprint policy has changed the default from MD5 to SHA256 (_DMS does not modify this Postfix parameter, but may affect any user customizations that do_). +- **Dovecot** + - The "Junk" mailbox (folder) is now referenced by it's [special-use flag `\Junk`](https://docker-mailserver.github.io/docker-mailserver/v13.3/examples/use-cases/imap-folders/) instead of an explicit mailbox. ([#3925](https://github.com/docker-mailserver/docker-mailserver/pull/3925)) + - This provides compatibility for the Junk mailbox when it's folder name differs (_eg: Renamed to "Spam"_). + - Potential breakage if your deployment modifies our `spam_to_junk.sieve` sieve script (_which is created during container startup when ENV `MOVE_SPAM_TO_JUNK=1`_) that handles storing spam mail into a users "Junk" mailbox folder. + - **Removed support for Solr integration:** ([#4025](https://github.com/docker-mailserver/docker-mailserver/pull/4025)) + - This was a community contributed feature for FTS (Full Text Search), the docs advise using an image that has not been maintained for over 2 years and lacks ARM64 support. Based on user engagement over the years this feature has very niche value to continue to support, thus is being removed. + - If you use Solr, support can be restored if you're willing to contribute docs for the feature that resolves the concerns raised +- **Log:** + - The format of DMS specific logs (_from our scripts, not running services_) has been changed. The new format is ` : ` ([#4035](https://github.com/docker-mailserver/docker-mailserver/pull/4035)) +- **rsyslog:** + - Debian 12 adjusted the `rsyslog` configuration for the default file template from `RSYSLOG_TraditionalFileFormat` to `RSYSLOG_FileFormat` (_upstream default since 2012_). This change may affect you if you have any monitoring / analysis of log output (_eg: `mail.log` / `docker logs`_). + - The two formats are roughly equivalent to [RFC 3164](https://www.rfc-editor.org/rfc/rfc3164)) and [RFC 5424](https://datatracker.ietf.org/doc/html/rfc5424#section-1) respectively. + - A notable difference is the change to [RFC 3339](https://www.rfc-editor.org/rfc/rfc3339.html#appendix-A) timestamps (_a strict subset of ISO 8601_). The [previous non-standardized timestamp format was defined in RFC 3164](https://www.rfc-editor.org/rfc/rfc3164.html#section-4.1.2) as `Mmm dd hh:mm:ss`. + - To revert this change you can add `sedfile -i '1i module(load="builtin:omfile" template="RSYSLOG_TraditionalFileFormat")' /etc/rsyslog.conf` via [our `user-patches.sh` feature](https://docker-mailserver.github.io/docker-mailserver/v14.0/config/advanced/override-defaults/user-patches/). + - Rsyslog now creates fewer log files: + - The files `/var/log/mail/mail.{info,warn,err}` are no longer created. These files represented `/var/log/mail.log` filtered into separate priority levels. As `/var/log/mail.log` contains all mail related messages, these files (_and their rotated counterparts_) can be deleted safely. + - `/var/log/messages`, `/var/log/debug` and several other log files not relevant to DMS were configured by default by Debian previously. These are not part of the `/var/log/mail/` volume mount, so should not impact anyone. +- **Features:** + - The relay host feature was refactored ([#3845](https://github.com/docker-mailserver/docker-mailserver/pull/3845)) + - The only breaking change this should introduce is with the Change Detection service (`check-for-changes.sh`). + - When credentials are configured for relays, change events that trigger the relayhost logic now reapply the relevant Postfix settings: + - `smtp_sasl_auth_enable = yes` (_SASL auth to outbound MTA connections is enabled_) + - `smtp_sasl_security_options = noanonymous` (_credentials are mandatory for outbound mail delivery_) + - `smtp_tls_security_level = encrypt` (_the outbound MTA connection must always be secure due to credentials sent_) +- **Environment Variables:** + - `SA_SPAM_SUBJECT` has been renamed into `SPAM_SUBJECT` to become anti-spam service agnostic. ([#3820](https://github.com/docker-mailserver/docker-mailserver/pull/3820)) + - As this functionality is now handled in Dovecot via a Sieve script instead of the respective anti-spam service during Postfix processing, this feature will only apply to mail stored in Dovecot. If you have relied on this feature in a different context, it will no longer be available. + - Rspamd previously handled this functionality via the `rewrite_subject` action which as now been disabled by default in favor of the new approach with `SPAM_SUBJECT`. + - `SA_SPAM_SUBJECT` is now deprecated and will log a warning if used. The value is copied as a fallback to `SPAM_SUBJECT`. + - The default has changed to not prepend any prefix to the subject unless configured to do so. If you relied on the implicit prefix, you will now need to provide one explicitly. + - `undef` was previously supported as an opt-out with `SA_SPAM_SUBJECT`. This is no longer valid, the equivalent opt-out value is now an empty value (_or rather the omission of this ENV being configured_). + - The feature to include [`_SCORE_` tag](https://spamassassin.apache.org/full/4.0.x/doc/Mail_SpamAssassin_Conf.html#rewrite_header-subject-from-to-STRING) in your value to be replaced by the associated spam score is no longer available. +- **Supervisord:** + - `supervisor-app.conf` renamed to `dms-services.conf` ([#3908](https://github.com/docker-mailserver/docker-mailserver/pull/3908)) +- **Rspamd:** + - The Redis history key has been changed in order to not incorporate the hostname of the container (which is desirable in Kubernetes environments) ([#3927](https://github.com/docker-mailserver/docker-mailserver/pull/3927)) +- **Account Management:** + - Addresses (accounts) are now normalized to lowercase automatically and a warning is logged in case uppercase letters are supplied ([#4033](https://github.com/docker-mailserver/docker-mailserver/pull/4033)) + +### Added + +- **Documentation:** + - A guide for configuring a public server to relay inbound and outbound mail from DMS on a private server ([#3973](https://github.com/docker-mailserver/docker-mailserver/pull/3973)) +- **Environment Variables:** + - `LOGROTATE_COUNT` defines the number of files kept by logrotate ([#3907](https://github.com/docker-mailserver/docker-mailserver/pull/3907)) + - The fail2ban log file is now also taken into account by `LOGROTATE_COUNT` and `LOGROTATE_INTERVAL` ([#3915](https://github.com/docker-mailserver/docker-mailserver/pull/3915), [#3919](https://github.com/docker-mailserver/docker-mailserver/pull/3919)) + +- **Internal:** + - Regular container restarts are now better supported. Setup scripts that ran previously will now be skipped ([#3929](https://github.com/docker-mailserver/docker-mailserver/pull/3929)) + +### Updates + +- **Environment Variables:** + - `ONE_DIR` has been removed (legacy ENV) ([#3840](https://github.com/docker-mailserver/docker-mailserver/pull/3840)) + - It's only functionality remaining was to opt-out of run-time state consolidation with `ONE_DIR=0` (_when a volume was already mounted to `/var/mail-state`_). +- **Internal:** + - Changed the Postgrey whitelist retrieved during build to [source directly from Github](https://github.com/schweikert/postgrey/blob/master/postgrey_whitelist_clients) as the list is updated more frequently than the [author publishes at their website](https://postgrey.schweikert.ch) ([#3879](https://github.com/docker-mailserver/docker-mailserver/pull/3879)) + - Enable spamassassin only, when amavis is enabled too. ([#3943](https://github.com/docker-mailserver/docker-mailserver/pull/3943)) +- **Tests:** + - Refactored helper methods for sending e-mails with specific `Message-ID` headers and the helpers for retrieving + filtering logs, which together help isolate logs relevant to specific mail when multiple mails have been processed within a single test. ([#3786](https://github.com/docker-mailserver/docker-mailserver/pull/3786)) +- **Rspamd:** + - The `rewrite_subject` action, is now disabled by default. It has been replaced with the new `SPAM_SUBJECT` environment variable, which implements the functionality via a Sieve script instead which is anti-spam service agnostic ([#3820](https://github.com/docker-mailserver/docker-mailserver/pull/3820)) + - `RSPAMD_NEURAL` was added and is disabled by default. If switched on it will enable the experimental Rspamd "Neural network" module to add a layer of analysis to spam detection ([#3833](https://github.com/docker-mailserver/docker-mailserver/pull/3833)) + - The symbol weights of SPF, DKIM and DMARC have been adjusted again. Fixes a bug and includes more appropriate combinations of symbols ([#3913](https://github.com/docker-mailserver/docker-mailserver/pull/3913), [#3923](https://github.com/docker-mailserver/docker-mailserver/pull/3923)) +- **Dovecot:** + - `logwatch` now filters out non-error logs related to the status of the `index-worker` process for FTS indexing. ([#4012](https://github.com/docker-mailserver/docker-mailserver/pull/4012)) + - updated FTS Xapian from version 1.5.5 to 1.7.12 + +### Fixes + +- DMS config: + - Files that are parsed line by line are now more robust to parse by detecting and fixing line-endings ([#3819](https://github.com/docker-mailserver/docker-mailserver/pull/3819)) + - The override config `postfix-main.cf` now retains custom parameters intended for use with `postfix-master.cf` ([#3880](https://github.com/docker-mailserver/docker-mailserver/pull/3880)) +- Variables related to Rspamd are declared as `readonly`, which would cause warnings in the log when being re-declared; we now guard against this issue ([#3837](https://github.com/docker-mailserver/docker-mailserver/pull/3837)) +- Relay host feature refactored ([#3845](https://github.com/docker-mailserver/docker-mailserver/pull/3845)) + - `DEFAULT_RELAY_HOST` ENV can now also use the `RELAY_USER` + `RELAY_PASSWORD` ENV for supplying credentials. + - `RELAY_HOST` ENV no longer enforces configuring outbound SMTP to require credentials. Like `DEFAULT_RELAY_HOST` it can now configure a relay where credentials are optional. + - Restarting DMS should not be required when configuring relay hosts without these ENV, but solely via `setup relay ...`, as change detection events now apply relevant Postfix setting changes for supporting credentials too. +- Rspamd configuration: Add a missing comma in `local_networks` so that all internal IP addresses are actually considered as internal ([#3862](https://github.com/docker-mailserver/docker-mailserver/pull/3862)) +- Ensure correct SELinux security context labels for files and directories moved to the mail-state volume during setup ([#3890](https://github.com/docker-mailserver/docker-mailserver/pull/3890)) +- Use correct environment variable for fetchmail ([#3901](https://github.com/docker-mailserver/docker-mailserver/pull/3901)) +- When using `ENABLE_GETMAIL=1` the undocumented internal location `/var/lib/getmail/` usage has been removed. Only the config volume `/tmp/docker-mailserver/getmail/` location is supported when Getmail has not been configured to deliver mail to Dovecot as advised in the DMS docs ([#4018](https://github.com/docker-mailserver/docker-mailserver/pull/4018)) +- Dovecot dummy accounts (_virtual alias workaround for dovecot feature `ENABLE_QUOTAS=1`_) now correctly matches the home location of the user for that alias ([#3997](https://github.com/docker-mailserver/docker-mailserver/pull/3997)) + +## [v13.3.1](https://github.com/docker-mailserver/docker-mailserver/releases/tag/v13.3.1) + +### Fixes + +- **Dovecot:** + - Restrict the auth mechanisms for PassDB configs we manage (oauth2, passwd-file, ldap) ([#3812](https://github.com/docker-mailserver/docker-mailserver/pull/3812)) + - Prevents misleading auth failures from attempting to authenticate against a PassDB with incompatible auth mechanisms. + - When the new OAuth2 feature was enabled, it introduced false-positives with logged auth failures which triggered Fail2Ban to ban the IP. +- **Rspamd:** + - Ensure correct ownership (`_rspamd:_rspamd`) for the Rspamd DKIM directory + files `/tmp/docker-mailserver/rspamd/dkim/` ([#3813](https://github.com/docker-mailserver/docker-mailserver/pull/3813)) + +## [v13.3.0](https://github.com/docker-mailserver/docker-mailserver/releases/tag/v13.3.0) + +### Features + +- **Authentication with OIDC / OAuth 2.0** 🎉 + - DMS now supports authentication via OAuth2 (_via `XOAUTH2` or `OAUTHBEARER` SASL mechanisms_) from capable services (_like Roundcube_). + - This does not replace the need for an `ACCOUNT_PROVISIONER` (`FILE` / `LDAP`), which is required for an account to receive or send mail. + - Successful authentication (_via Dovecot PassDB_) still requires an existing account (_lookup via Dovecot UserDB_). +- **MTA-STS** (_Optional support for mandatory outgoing TLS encryption_) + - If enabled and the outbound recipient has an MTA-STS policy set, TLS is mandatory for delivering to that recipient. + - Enable via the ENV `ENABLE_MTA_STS=1` + - Supported by major email service providers like Gmail, Yahoo and Outlook. + +### Added + +- **Documentation:** + - An example for how to bind outbound SMTP connections to a specific network interface ([#3465](https://github.com/docker-mailserver/docker-mailserver/pull/3465)) + +### Updates + +- **Tests:** + - Revised OAuth2 test ([#3795](https://github.com/docker-mailserver/docker-mailserver/pull/3795)) + - Replace `wc -l` with `grep -c` ([#3752](https://github.com/docker-mailserver/docker-mailserver/pull/3752)) + - Revised testing of service process management (supervisord) to be more robust ([#3780](https://github.com/docker-mailserver/docker-mailserver/pull/3780)) + - Refactored mail sending ([#3747](https://github.com/docker-mailserver/docker-mailserver/pull/3747) & [#3772](https://github.com/docker-mailserver/docker-mailserver/pull/3772)): + - This change is a follow-up to [#3732](https://github.com/docker-mailserver/docker-mailserver/pull/3732) from DMS v13.2. + - `swaks` version is now the latest from Github releases instead of the Debian package. + - `_nc_wrapper`, `_send_mail` and related helpers expect the `.txt` filepath extension again. + - `sending.bash` helper methods were refactored to better integrate `swaks` and accommodate different usage contexts. + - `test/files/emails/existing/` files were removed similar to previous removal of SMTP auth files as they became redundant with `swaks`. +- **Internal:** + - Postfix is now configured with `smtputf8_enable = no` in our default `main.cf` config (_instead of during container startup_). ([#3750](https://github.com/docker-mailserver/docker-mailserver/pull/3750)) +- **Rspamd:** ([#3726](https://github.com/docker-mailserver/docker-mailserver/pull/3726)): + - Symbol scores for SPF, DKIM & DMARC were updated to more closely align with [RFC7489](https://www.rfc-editor.org/rfc/rfc7489#page-24). Please note that complete alignment is undesirable as other symbols may be added as well, which changes the overall score calculation again, see [this issue](https://github.com/docker-mailserver/docker-mailserver/issues/3690#issuecomment-1866871996) +- **Documentation:** + - Revised the SpamAssassin ENV docs to better communicate configuration and their relation to other ENV settings. ([#3756](https://github.com/docker-mailserver/docker-mailserver/pull/3756)) + - Detailed how mail received is assigned a spam score by Rspamd and processed accordingly ([#3773](https://github.com/docker-mailserver/docker-mailserver/pull/3773)) + +### Fixes + +- **Setup:** + - `setup` CLI - `setup dkim domain` now creates the keys files with the user owning the key directory ([#3783](https://github.com/docker-mailserver/docker-mailserver/pull/3783)) +- **Dovecot:** + - During container startup for Dovecot Sieve, `.sievec` source files compiled to `.svbin` now have their `mtime` adjusted post setup to ensure it is always older than the associated `.svbin` file. This avoids superfluous error logs for sieve scripts that don't actually need to be compiled again ([#3779](https://github.com/docker-mailserver/docker-mailserver/pull/3779)) +- **Internal:** + - `.gitattributes`: Always use LF line endings on checkout for files with shell script content ([#3755](https://github.com/docker-mailserver/docker-mailserver/pull/3755)) + - Fix missing 'jaq' binary for ARM architecture ([#3766](https://github.com/docker-mailserver/docker-mailserver/pull/3766)) + +## [v13.2.0](https://github.com/docker-mailserver/docker-mailserver/releases/tag/v13.2.0) + +### Security + +DMS is now secured against the [recently published spoofing attack "SMTP Smuggling"](https://www.postfix.org/smtp-smuggling.html) that affected Postfix ([#3727](https://github.com/docker-mailserver/docker-mailserver/pull/3727)): + +- Postfix upgraded from `3.5.18` to `3.5.23` which provides the [long-term fix with `smtpd_forbid_bare_newline = yes`](https://www.postfix.org/smtp-smuggling.html#long) +- If you are unable to upgrade to this release of DMS, you may follow [these instructions](https://github.com/docker-mailserver/docker-mailserver/issues/3719#issuecomment-1870865118) for applying the [short-term workaround](https://www.postfix.org/smtp-smuggling.html#short). +- This change should not cause compatibility concerns for legitimate mail clients, however if you use software like `netcat` to send mail to DMS (_like our test-suite previously did_) it may now be rejected (_especially with the the short-term workaround `smtpd_data_restrictions = reject_unauth_pipelining`_). +- **NOTE:** This Postfix update also includes the new parameter [`smtpd_forbid_bare_newline_exclusions`](https://www.postfix.org/postconf.5.html#smtpd_forbid_bare_newline_exclusions) which defaults to `$mynetworks` for excluding trusted mail clients excluded from the restriction. + - With our default `PERMIT_DOCKER=none` this is not a concern. + - Presently the Docker daemon config has `user-proxy: true` enabled by default. + - On a host that can be reached by IPv6, this will route to a DMS IPv4 only container implicitly through the Docker network bridge gateway which rewrites the source address. + - If your `PERMIT_DOCKER` setting allows that gateway IP, then it is part of `$mynetworks` and this attack would not be prevented from such connections. + - If this affects your deployment, refer to [our IPv6 docs](https://docker-mailserver.github.io/docker-mailserver/v13.2/config/advanced/ipv6/) for advice on handling IPv6 correctly in Docker. Alternatively [use our `postfix-main.cf`](https://docker-mailserver.github.io/docker-mailserver/v13.2/config/advanced/override-defaults/postfix/) to set `smtpd_forbid_bare_newline_exclusions=` as empty. + +### Updates + +- The test suite now uses `swaks` instead of `nc`, which has multiple benefits ([#3732](https://github.com/docker-mailserver/docker-mailserver/pull/3732)): + - `swaks` handles pipelining correctly, hence we can now use `reject_unauth_pipelining` in Postfix's configuration. + - `swaks` provides better CLI options that make many files superfluous. + - `swaks` can also replace `openssl s_client` and handles authentication on submission ports better. +- **Postfix:** + - We now defer rejection from unauthorized pipelining until the SMTP `DATA` command via `smtpd_data_restrictions` (_i.e. at the end of the mail transfer transaction_) ([#3744](https://github.com/docker-mailserver/docker-mailserver/pull/3744)) + - Previously our configuration only handled this during the client and recipient restriction stages. Postfix will flag this activity when encountered, but the rejection now is handled at `DATA` where unauthorized pipelining would have been valid from this point. + - If you had the Amavis service enabled (default), this restriction was already in place. Otherwise the concerns expressed with `smtpd_data_restrictions = reject_unauth_pipelining` from the security section above apply. We have permitted trusted clients (_`$mynetworks` or authenticated_) to bypass this restriction. + +## [v13.1.0](https://github.com/docker-mailserver/docker-mailserver/releases/tag/v13.1.0) + +### Added + +- **Dovecot:** + - ENV `ENABLE_IMAP` ([#3703](https://github.com/docker-mailserver/docker-mailserver/pull/3703)) +- **Tests:** + - You can now use `make run-local-instance` to run a DMS image that was built locally to test changes ([#3663](https://github.com/docker-mailserver/docker-mailserver/pull/3663)) +- **Internal:** + - Log a warning when update-check is enabled, but no stable release image is used ([#3684](https://github.com/docker-mailserver/docker-mailserver/pull/3684)) + +### Updates + +- **Documentation:** + - Debugging - Raise awareness in the troubleshooting page for a common misconfiguration when deviating from our advice by using a bare domain ([#3680](https://github.com/docker-mailserver/docker-mailserver/pull/3680)) + - Debugging - Raise awareness of temporary downtime during certificate renewal that can cause a failure to deliver local mail ([#3718](https://github.com/docker-mailserver/docker-mailserver/pull/3718)) +- **Internal:** + - Postfix configures `virtual_mailbox_maps` and `virtual_transport` during startup instead of using defaults (configured for Dovecot) via our `main.cf` ([#3681](https://github.com/docker-mailserver/docker-mailserver/pull/3681)) +- **Rspamd:** + - Upgraded to version `3.7.5`. This was previously inconsistent between our AMD64 (`3.5`) and ARM64 (`3.4`) images ([#3686](https://github.com/docker-mailserver/docker-mailserver/pull/3686)) + +### Fixed + +- **Internal:** + - The container startup welcome log message now references `DMS_RELEASE` ([#3676](https://github.com/docker-mailserver/docker-mailserver/pull/3676)) + - `VERSION` was incremented for prior releases to be notified of the v13.0.1 patch release ([#3676](https://github.com/docker-mailserver/docker-mailserver/pull/3676)) + - `VERSION` is no longer included in the image ([#3711](https://github.com/docker-mailserver/docker-mailserver/pull/3711)) + - Update-check: fix 'read' exit status ([#3688](https://github.com/docker-mailserver/docker-mailserver/pull/3688)) + - `ENABLE_QUOTAS=0` no longer tries to remove non-existent config ([#3715](https://github.com/docker-mailserver/docker-mailserver/pull/3715)) + - The `postgrey` service now writes logs to the supervisor directory like all other services. Previously this was `/var/log/mail/mail.log` ([#3724](https://github.com/docker-mailserver/docker-mailserver/pull/3724)) +- **Rspamd:** + - Switch to official arm64 packages to avoid segfaults ([#3686](https://github.com/docker-mailserver/docker-mailserver/pull/3686)) +- **CI / Automation:** + - The lint workflow can now be manually triggered by maintainers ([#3714]https://github.com/docker-mailserver/docker-mailserver/pull/3714) + +## [v13.0.1](https://github.com/docker-mailserver/docker-mailserver/releases/tag/v13.0.1) + +This patch release fixes two bugs that Rspamd users encountered with the `v13.0.0` release. Big thanks to the those that helped to identify these issues! ❤️ + +### Fixed + +- **Internal:** + - The update check service now queries the latest GH release for a version tag (_instead of from a `VERSION` file at the GH repo_). This should provide more reliable update notifications ([#3666](https://github.com/docker-mailserver/docker-mailserver/pull/3666)) +- **Rspamd:** + - The check for correct permission on the private key when signing e-mails with DKIM was flawed. The result was that a false warning was emitted ([#3669](https://github.com/docker-mailserver/docker-mailserver/pull/3669)) + - When [`RSPAMD_CHECK_AUTHENTICATED=0`][docs::env-rspamd-check-auth], DKIM signing for outbound e-mail was disabled, which is undesirable ([#3669](https://github.com/docker-mailserver/docker-mailserver/pull/3669)). **Make sure to check the documentation of [`RSPAMD_CHECK_AUTHENTICATED`][docs::env-rspamd-check-auth]**! + +[docs::env-rspamd-check-auth]: https://docker-mailserver.github.io/docker-mailserver/v13.0/config/environment/#rspamd_check_authenticated + +## [v13.0.0](https://github.com/docker-mailserver/docker-mailserver/releases/tag/v13.0.0) + +### Breaking + +- **LDAP:** + - ENV `LDAP_SERVER_HOST`, `DOVECOT_URIS`, and `SASLAUTHD_LDAP_SERVER` will now log an error if the LDAP URI scheme is missing. Previously there was an implicit fallback to `ldap://` ([#3522](https://github.com/docker-mailserver/docker-mailserver/pull/3522)) + - `ENABLE_LDAP=1` is no longer supported, please use `ACCOUNT_PROVISIONER=LDAP` ([#3507](https://github.com/docker-mailserver/docker-mailserver/pull/3507)) +- **Rspamd:** + - The deprecated path for the Rspamd custom commands file (`/tmp/docker-mailserver/rspamd-modules.conf`) now prevents successful startup. The correct path is `/tmp/docker-mailserver/rspamd/custom-commands.conf`. +- **Dovecot:** + - Dovecot mail storage per account in `/var/mail` previously shared the same path for the accounts home directory ([#3335](https://github.com/docker-mailserver/docker-mailserver/pull/3335)) + - The home directory now is a subdirectory `home/`. This change better supports sieve scripts. + - **NOTE:** The change has not yet been implemented for `ACCOUNT_PROVISIONER=LDAP`. +- **Postfix:** + - `/etc/postfix/master.cf` has renamed the "smtps" service to "submissions" ([#3235](https://github.com/docker-mailserver/docker-mailserver/pull/3235)) + - This is the modern `/etc/services` name for port 465, aligning with the similar "submission" port 587. + - If you have configured Proxy Protocol support with a reverse proxy via `postfix-master.cf` (_as [per our docs guide](https://docker-mailserver.github.io/docker-mailserver/v13.0/examples/tutorials/mailserver-behind-proxy/)_), you will want to update `smtps` to `submissions` there. + - Postfix now defaults to supporting DSNs (_[Delivery Status Notifications](https://github.com/docker-mailserver/docker-mailserver/pull/3572#issuecomment-1751880574)_) only for authenticated users (_via ports 465 + 587_). This is a security measure to reduce spammer abuse of your DMS instance as a backscatter source. ([#3572](https://github.com/docker-mailserver/docker-mailserver/pull/3572)) + - If you need to modify this change, please let us know by opening an issue / discussion. + - You can [opt out (_enable DSNs_) via the `postfix-main.cf` override support](https://docker-mailserver.github.io/docker-mailserver/v12.1/config/advanced/override-defaults/postfix/) using the contents: `smtpd_discard_ehlo_keywords =`. + - Likewise for authenticated users, the submission(s) ports (465 + 587) are configured internally via `master.cf` to keep DSNs enabled (_since authentication protects from abuse_). + + If necessary, DSNs for authenticated users can be disabled via the `postfix-master.cf` override with the following contents: + + ```cf + submission/inet/smtpd_discard_ehlo_keywords=silent-discard,dsn + submissions/inet/smtpd_discard_ehlo_keywords=silent-discard,dsn + ``` + +### Added + +- **Features:** + - `getmail` as an alternative to `fetchmail` ([#2803](https://github.com/docker-mailserver/docker-mailserver/pull/2803)) + - `setup` CLI - `setup fail2ban` gained a new `status ` subcommand ([#3455](https://github.com/docker-mailserver/docker-mailserver/pull/3455)) +- **Environment Variables:** + - `MARK_SPAM_AS_READ`. When set to `1`, marks incoming spam as "read" to avoid unwanted "new mail" notifications for junk mail ([#3489](https://github.com/docker-mailserver/docker-mailserver/pull/3489)) + - `DMS_VMAIL_UID` and `DMS_VMAIL_GID` allow changing the default ID values (`5000:5000`) for the Dovecot vmail user and group ([#3550](https://github.com/docker-mailserver/docker-mailserver/pull/3550)) + - `RSPAMD_CHECK_AUTHENTICATED` allows authenticated users to avoid additional security checks by Rspamd ([#3440](https://github.com/docker-mailserver/docker-mailserver/pull/3440)) +- **Documentation:** + - Use-case examples / tutorials: + - iOS mail push support ([#3513](https://github.com/docker-mailserver/docker-mailserver/pull/3513)) + - Guide for setting up Dovecot Authentication via Lua ([#3579](https://github.com/docker-mailserver/docker-mailserver/pull/3579)) + - Guide for integrating with the Crowdsec service ([#3651](https://github.com/docker-mailserver/docker-mailserver/pull/3651)) + - Debugging page: + - New compatibility section ([#3404](https://github.com/docker-mailserver/docker-mailserver/pull/3404)) + - Now advises how to (re)start DMS correctly ([#3654](https://github.com/docker-mailserver/docker-mailserver/pull/3654)) + - Better communicate distinction between DMS FQDN and DMS mail accounts ([#3372](https://github.com/docker-mailserver/docker-mailserver/pull/3372)) + - Traefik example now includes `passthrough=true` on implicit ports ([#3568](https://github.com/docker-mailserver/docker-mailserver/pull/3568)) + - Rspamd docs have received a variety of revisions ([#3318](https://github.com/docker-mailserver/docker-mailserver/pull/3318), [#3325](https://github.com/docker-mailserver/docker-mailserver/pull/3325), [#3329](https://github.com/docker-mailserver/docker-mailserver/pull/3329)) + - IPv6 config examples with content tabs ([#3436](https://github.com/docker-mailserver/docker-mailserver/pull/3436)) + - Mention [internet.nl](https://internet.nl/test-mail/) as another testing service ([#3445](https://github.com/docker-mailserver/docker-mailserver/pull/3445)) + - `setup alias add ...` CLI help message now includes an example for aliasing to multiple recipients ([#3600](https://github.com/docker-mailserver/docker-mailserver/pull/3600)) + - `SPAMASSASSIN_SPAM_TO_INBOX=1`, now emits a debug log to raise awareness that `SA_KILL` will be ignored ([#3360](https://github.com/docker-mailserver/docker-mailserver/pull/3360)) + - `CLAMAV_MESSAGE_SIZE_LIMIT` now logs a warning when the value exceeds what ClamAV is capable of supporting (4GiB max scan size [#3332](https://github.com/docker-mailserver/docker-mailserver/pull/3332), 2GiB max file size [#3341](https://github.com/docker-mailserver/docker-mailserver/pull/3341)) + - Added note to caution against changing `mydestination` in Postfix's `main.cf` ([#3316](https://github.com/docker-mailserver/docker-mailserver/pull/3316)) +- **Internal:** + - Added a wrapper to update Postfix configuration safely ([#3484](https://github.com/docker-mailserver/docker-mailserver/pull/3484), [#3503](https://github.com/docker-mailserver/docker-mailserver/pull/3503)) + - Add debug group to `packages.sh` ([#3578](https://github.com/docker-mailserver/docker-mailserver/pull/3578)) +- **Tests:** + - Additional linting check for BASH syntax ([#3369](https://github.com/docker-mailserver/docker-mailserver/pull/3369)) + +### Updates + +- **Misc:** + - Changed `setup config dkim` default key size to `2048` (`open-dkim`) ([#3508](https://github.com/docker-mailserver/docker-mailserver/pull/3508)) +- **Postfix:** + - Dropped special bits from `maildrop/` and `public/` directory permissions ([#3625](https://github.com/docker-mailserver/docker-mailserver/pull/3625)) +- **Rspamd:** + - Adjusted learning of ham ([#3334](https://github.com/docker-mailserver/docker-mailserver/pull/3334)) + - Adjusted `antivirus.conf` ([#3331](https://github.com/docker-mailserver/docker-mailserver/pull/3331)) + - `logrotate` setup + Rspamd log path + tests log helper fallback path ([#3576](https://github.com/docker-mailserver/docker-mailserver/pull/3576)) + - Setup during container startup is now more resilient ([#3578](https://github.com/docker-mailserver/docker-mailserver/pull/3578)) + - Changed DKIM default config location ([#3597](https://github.com/docker-mailserver/docker-mailserver/pull/3597)) + - Removed the symlink for the `override.d/` directory in favor of using `cp`, integrated into the changedetector service, added a `--force` option for the Rspamd DKIM management, and provided a dedicated helper script for common ENV variables ([#3599](https://github.com/docker-mailserver/docker-mailserver/pull/3599)) + - Required permissions are now verified for DKIM private key files ([#3627](https://github.com/docker-mailserver/docker-mailserver/pull/3627)) +- **Documentation:** + - Documentation aligned to Compose v2 conventions, `docker-compose` command changed to `docker compose`, `docker-compose.yaml` to `compose.yaml` ([#3295](https://github.com/docker-mailserver/docker-mailserver/pull/3295)) + - Restored missing edit button ([#3338](https://github.com/docker-mailserver/docker-mailserver/pull/3338)) + - Complete rewrite of the IPv6 page ([#3244](https://github.com/docker-mailserver/docker-mailserver/pull/3244), [#3531](https://github.com/docker-mailserver/docker-mailserver/pull/3531)) + - Complete rewrite of the "Update and Cleanup" maintenance page ([#3539](https://github.com/docker-mailserver/docker-mailserver/pull/3539), [#3583](https://github.com/docker-mailserver/docker-mailserver/pull/3583)) + - Improved debugging page advice on working with logs ([#3626](https://github.com/docker-mailserver/docker-mailserver/pull/3626), [#3640](https://github.com/docker-mailserver/docker-mailserver/pull/3640)) + - Clarified the default for ENV `FETCHMAIL_PARALLEL` ([#3603](https://github.com/docker-mailserver/docker-mailserver/pull/3603)) + - Removed port 25 from FAQ entry for mail client ports supporting authenticated submission ([#3496](https://github.com/docker-mailserver/docker-mailserver/pull/3496)) + - Updated home path in docs for Dovecot Sieve ([#3370](https://github.com/docker-mailserver/docker-mailserver/pull/3370), [#3650](https://github.com/docker-mailserver/docker-mailserver/pull/3650)) + - Fixed path to `rspamd.log` ([#3585](https://github.com/docker-mailserver/docker-mailserver/pull/3585)) + - "Optional Config" page now uses consistent lowercase convention for directory names ([#3629](https://github.com/docker-mailserver/docker-mailserver/pull/3629)) + - `CONTRIBUTORS.md`: Removed redundant "All Contributors" section ([#3638](https://github.com/docker-mailserver/docker-mailserver/pull/3638)) +- **Internal:** + - LDAP config improvements (Removed implicit `ldap://` LDAP URI scheme fallback) ([#3522](https://github.com/docker-mailserver/docker-mailserver/pull/3522)) + - Changed style conventions for internal scripts ([#3361](https://github.com/docker-mailserver/docker-mailserver/pull/3361), [#3364](https://github.com/docker-mailserver/docker-mailserver/pull/3364), [#3365](https://github.com/docker-mailserver/docker-mailserver/pull/3365), [#3366](https://github.com/docker-mailserver/docker-mailserver/pull/3366), [#3368](https://github.com/docker-mailserver/docker-mailserver/pull/3368), [#3464](https://github.com/docker-mailserver/docker-mailserver/pull/3464)) +- **CI / Automation:** + - `.gitattributes` now ensures files are committed with `eol=lf` ([#3527](https://github.com/docker-mailserver/docker-mailserver/pull/3527)) + - Revised the GitHub issue bug report template ([#3317](https://github.com/docker-mailserver/docker-mailserver/pull/3317), [#3381](https://github.com/docker-mailserver/docker-mailserver/pull/3381), [#3435](https://github.com/docker-mailserver/docker-mailserver/pull/3435)) + - Clarified that the issue tracker is not for personal support ([#3498](https://github.com/docker-mailserver/docker-mailserver/pull/3498), [#3502](https://github.com/docker-mailserver/docker-mailserver/pull/3502)) + - Bumped versions of miscellaneous software (also shoutout to @dependabot) ([#3371](https://github.com/docker-mailserver/docker-mailserver/pull/3371), [#3584](https://github.com/docker-mailserver/docker-mailserver/pull/3584), [#3504](https://github.com/docker-mailserver/docker-mailserver/pull/3504), [#3516](https://github.com/docker-mailserver/docker-mailserver/pull/3516)) +- **Tests:** + - Refactored LDAP tests to current conventions ([#3483](https://github.com/docker-mailserver/docker-mailserver/pull/3483)) + - Changed OpenLDAP image to `bitnami/openldap` ([#3494](https://github.com/docker-mailserver/docker-mailserver/pull/3494)) + - Revised LDAP config + setup ([#3514](https://github.com/docker-mailserver/docker-mailserver/pull/3514)) + - Added tests for the helper function `_add_to_or_update_postfix_main()` ([#3505](https://github.com/docker-mailserver/docker-mailserver/pull/3505)) + - EditorConfig Checker lint now uses a mount path to `/check` instead of `/ci` ([#3655](https://github.com/docker-mailserver/docker-mailserver/pull/3655)) + +### Fixed + +- **Security:** + - Fixed issue with concatenating `$dmarc_milter` and `$dkim_milter` in `main.cf` ([#3380](https://github.com/docker-mailserver/docker-mailserver/pull/3380)) + - Fixed Rspamd DKIM signing for inbound emails ([#3439](https://github.com/docker-mailserver/docker-mailserver/pull/3439), [#3453](https://github.com/docker-mailserver/docker-mailserver/pull/3453)) + - OpenDKIM key generation is no longer broken when Rspamd is also enabled ([#3535](https://github.com/docker-mailserver/docker-mailserver/pull/3535)) +- **Internal:** + - The "database" files (_for managing users and aliases_) now correctly filters within lookup query ([#3359](https://github.com/docker-mailserver/docker-mailserver/pull/3359)) + - `_setup_spam_to_junk()` no longer registered when `SMTP_ONLY=1` ([#3385](https://github.com/docker-mailserver/docker-mailserver/pull/3385)) + - Dovecot `fts_xapian` is now compiled from source to match the Dovecot package ABI ([#3373](https://github.com/docker-mailserver/docker-mailserver/pull/3373)) +- **CI:** + - Scheduled build now have the correct permissions to run successfully ([#3345](https://github.com/docker-mailserver/docker-mailserver/pull/3345)) +- **Documentation:** + - Miscellaneous spelling and wording improvements ([#3324](https://github.com/docker-mailserver/docker-mailserver/pull/3324), [#3330](https://github.com/docker-mailserver/docker-mailserver/pull/3330), [#3337](https://github.com/docker-mailserver/docker-mailserver/pull/3337), [#3339](https://github.com/docker-mailserver/docker-mailserver/pull/3339), [#3344](https://github.com/docker-mailserver/docker-mailserver/pull/3344), [#3367](https://github.com/docker-mailserver/docker-mailserver/pull/3367), [#3411](https://github.com/docker-mailserver/docker-mailserver/pull/3411), [#3443](https://github.com/docker-mailserver/docker-mailserver/pull/3443)) +- **Tests:** + - Run `pgrep` within the actual container ([#3553](https://github.com/docker-mailserver/docker-mailserver/pull/3553)) + - `lmtp_ip.bats` improved partial failure output ([#3552](https://github.com/docker-mailserver/docker-mailserver/pull/3552)) + - Improvements to LDIF test data ([#3506](https://github.com/docker-mailserver/docker-mailserver/pull/3506)) + - Normalized for `.gitattributes` + improved `eclint` coverage ([#3566](https://github.com/docker-mailserver/docker-mailserver/pull/3566)) + - Fixed ShellCheck linting for BATS tests ([#3347](https://github.com/docker-mailserver/docker-mailserver/pull/3347)) + +## [v12.1.0](https://github.com/docker-mailserver/docker-mailserver/releases/tag/v12.1.0) + +### Added + +- Rspamd: + - note about Rspamd's web interface ([#3245](https://github.com/docker-mailserver/docker-mailserver/pull/3245)) + - add greylisting option & code refactoring ([#3206](https://github.com/docker-mailserver/docker-mailserver/pull/3206)) + - added `HFILTER_HOSTNAME_UNKNOWN` and make it configurable ([#3248](https://github.com/docker-mailserver/docker-mailserver/pull/3248)) + - add option to re-enable `reject_unknown_client_hostname` after #3248 ([#3255](https://github.com/docker-mailserver/docker-mailserver/pull/3255)) + - add DKIM helper script ([#3286](https://github.com/docker-mailserver/docker-mailserver/pull/3286)) +- make `policyd-spf` configurable ([#3246](https://github.com/docker-mailserver/docker-mailserver/pull/3246)) +- add 'log' command to set up for Fail2Ban ([#3299](https://github.com/docker-mailserver/docker-mailserver/pull/3299)) +- `setup` command now expects accounts and aliases to be mutually exclusive ([#3270](https://github.com/docker-mailserver/docker-mailserver/pull/3270)) + +### Updated + +- update DKIM/DMARC/SPF docs ([#3231](https://github.com/docker-mailserver/docker-mailserver/pull/3231)) +- Fail2Ban: + - made config more aggressive ([#3243](https://github.com/docker-mailserver/docker-mailserver/pull/3243) & [#3288](https://github.com/docker-mailserver/docker-mailserver/pull/3288)) + - update fail2ban config examples with current DMS default values ([#3258](https://github.com/docker-mailserver/docker-mailserver/pull/3258)) + - make Fail2Ban log persistent ([#3269](https://github.com/docker-mailserver/docker-mailserver/pull/3269)) + - update F2B docs & bind mount links ([#3293](https://github.com/docker-mailserver/docker-mailserver/pull/3293)) +- Rspamd: + - improve Rspamd docs ([#3257](https://github.com/docker-mailserver/docker-mailserver/pull/3257)) + - script stabilization ([#3261](https://github.com/docker-mailserver/docker-mailserver/pull/3261) & [#3282](https://github.com/docker-mailserver/docker-mailserver/pull/3282)) + - remove WIP warnings ([#3283](https://github.com/docker-mailserver/docker-mailserver/pull/3283)) +- improve shutdown function by making PANIC_STRATEGY obsolete ([#3265](https://github.com/docker-mailserver/docker-mailserver/pull/3265)) +- update `bug_report.yml` ([#3275](https://github.com/docker-mailserver/docker-mailserver/pull/3275)) +- simplify `bug_report.yml` ([#3276](https://github.com/docker-mailserver/docker-mailserver/pull/3276)) +- revised the contributor workflow ([#2227](https://github.com/docker-mailserver/docker-mailserver/pull/2227)) + +### Changed + +- default registry changed from DockerHub (`docker.io`) to GHCR (`ghcr.io`) ([#3233](https://github.com/docker-mailserver/docker-mailserver/pull/3233)) +- consistent namings in docs ([#3242](https://github.com/docker-mailserver/docker-mailserver/pull/3242)) +- get all `policyd-spf` setup in one place ([#3263](https://github.com/docker-mailserver/docker-mailserver/pull/3263)) +- miscellaneous script improvements ([#3281](https://github.com/docker-mailserver/docker-mailserver/pull/3281)) +- update FAQ entries ([#3294](https://github.com/docker-mailserver/docker-mailserver/pull/3294)) + +### Fixed + +- GitHub Actions docs update workflow ([#3241](https://github.com/docker-mailserver/docker-mailserver/pull/3241)) +- fix dovecot: ldap mail delivery works ([#3252](https://github.com/docker-mailserver/docker-mailserver/pull/3252)) +- shellcheck: do not check .git folder ([#3267](https://github.com/docker-mailserver/docker-mailserver/pull/3267)) +- add missing -E for extended regexes in `smtpd_sender_restrictions` ([#3272](https://github.com/docker-mailserver/docker-mailserver/pull/3272)) +- fix setting `SRS_EXCLUDE_DOMAINS` during startup ([#3271](https://github.com/docker-mailserver/docker-mailserver/pull/3271)) +- remove superfluous `EOF` in `dmarc_dkim_spf.sh` ([#3266](https://github.com/docker-mailserver/docker-mailserver/pull/3266)) +- apply fixes to helpers when using `set -eE` ([#3285](https://github.com/docker-mailserver/docker-mailserver/pull/3285)) + +## [12.0.0](https://github.com/docker-mailserver/docker-mailserver/releases/tag/v12.0.0) + +Notable changes are: + +- Rspamd feature is promoted from preview status +- Services no longer use `chroot` +- Fail2Ban major version upgrade +- ARMv7 platform is no longer supported +- TLS 1.2 is the minimum supported protocol +- SMTP authentication on port 25 disabled +- The value of `smtpd_sender_restrictions` for Postfix has replaced the value ([#3127](https://github.com/docker-mailserver/docker-mailserver/pull/3127)): + - In `main.cf` with `$dms_smtpd_sender_restrictions` + - In `master.cf` inbound submissions ports 465 + 587 extend this inherited `smtpd` restriction with `$mua_sender_restrictions` + +### Added + +- **security**: Rspamd support: + - integration into scripts, provisioning of configuration & documentation ([#2902](https://github.com/docker-mailserver/docker-mailserver/pull/2902),[#3016](https://github.com/docker-mailserver/docker-mailserver/pull/3016),[#3039](https://github.com/docker-mailserver/docker-mailserver/pull/3039)) + - easily adjust options & modules ([#3059](https://github.com/docker-mailserver/docker-mailserver/pull/3059)) + - advanced documentation ([#3104](https://github.com/docker-mailserver/docker-mailserver/pull/3104)) + - make disabling Redis possible ([#3132](https://github.com/docker-mailserver/docker-mailserver/pull/3132)) + - persistence for Redis ([#3143](https://github.com/docker-mailserver/docker-mailserver/pull/3143)) + - integrate into `MOVE_SPAM_TO_JUNK` ([#3159](https://github.com/docker-mailserver/docker-mailserver/pull/3159)) + - make it possible to learn from user actions ([#3159](https://github.com/docker-mailserver/docker-mailserver/pull/3159)) +- heavily updated CI & tests: + - added functionality to send mail with a helper function ([#3026](https://github.com/docker-mailserver/docker-mailserver/pull/3026),[#3103](https://github.com/docker-mailserver/docker-mailserver/pull/3103),[#3105](https://github.com/docker-mailserver/docker-mailserver/pull/3105)) + - add a dedicated page for tests with more information ([#3019](https://github.com/docker-mailserver/docker-mailserver/pull/3019)) +- add information to Logwatch's mailer so `Envelope From` is properly set ([#3081](https://github.com/docker-mailserver/docker-mailserver/pull/3081)) +- add vulnerability scanning workflow & security policy ([#3106](https://github.com/docker-mailserver/docker-mailserver/pull/3106)) +- Add tools (ping & dig) to the image ([2989](https://github.com/docker-mailserver/docker-mailserver/pull/2989)) + +### Updates + +- Fail2Ban major version updated to v1.0.2 ([#2959](https://github.com/docker-mailserver/docker-mailserver/pull/2959)) +- heavily updated CI & tests: + - we now run more tests in parallel bringing down overall time to build and test AMD64 to 6 minutes ([#2938](https://github.com/docker-mailserver/docker-mailserver/pull/2938),[#3038](https://github.com/docker-mailserver/docker-mailserver/pull/3038),[#3018](https://github.com/docker-mailserver/docker-mailserver/pull/3018),[#3062](https://github.com/docker-mailserver/docker-mailserver/pull/3062)) + - remove CI ENV & disable fail-fast strategy ([#3065](https://github.com/docker-mailserver/docker-mailserver/pull/3065)) + - streamlined GH Actions runners ([#3025](https://github.com/docker-mailserver/docker-mailserver/pull/3025)) + - updated BATS & helper + minor updates to BATS variables ([#2988](https://github.com/docker-mailserver/docker-mailserver/pull/2988)) + - improved consistency and documentation for test helpers ([#3012](https://github.com/docker-mailserver/docker-mailserver/pull/3012)) +- improve the `clean` recipe (don't require `sudo` anymore) ([#3020](https://github.com/docker-mailserver/docker-mailserver/pull/3020)) +- improve Amavis setup routine ([#3079](https://github.com/docker-mailserver/docker-mailserver/pull/3079)) +- completely refactor README & parts of docs ([#3097](https://github.com/docker-mailserver/docker-mailserver/pull/3097)) +- TLS setup (self-signed) error message now includes `SS_CA_CERT` ([#3168](https://github.com/docker-mailserver/docker-mailserver/pull/3168)) +- Better default value for SA_KILL variable ([#3058](https://github.com/docker-mailserver/docker-mailserver/pull/3058)) + +### Fixed + +- `restrict-access` avoid inserting duplicates ([#3067](https://github.com/docker-mailserver/docker-mailserver/pull/3067)) +- correct the casing for Mime vs. MIME ([#3040](https://github.com/docker-mailserver/docker-mailserver/pull/3040)) +- Dovecot: + - Quota plugin is now properly configured via `mail_plugins` at setup ([#2958](https://github.com/docker-mailserver/docker-mailserver/pull/2958)) + - `quota-status` service (port 65265) now only binds to `127.0.0.1` ([#3057](https://github.com/docker-mailserver/docker-mailserver/pull/3057)) +- OpenDMARC - Change default policy to reject ([#2933](https://github.com/docker-mailserver/docker-mailserver/pull/2933)) +- Change Detection service - Use service `reload` instead of restarting process to minimize downtime ([#2947](https://github.com/docker-mailserver/docker-mailserver/pull/2947)) +- Slightly faster container startup via `postconf` workaround ([#2998](https://github.com/docker-mailserver/docker-mailserver/pull/2998)) +- Better group ownership to `/var/mail-state` + ClamAV in `Dockerfile` ([#3011](https://github.com/docker-mailserver/docker-mailserver/pull/3011)) +- Dropping Postfix `chroot` mode: + - Remove syslog socket created by Debian ([#3134](https://github.com/docker-mailserver/docker-mailserver/pull/3134)) + - Supervisor proxy signals for `postfix start-fg` via PID ([#3118](https://github.com/docker-mailserver/docker-mailserver/pull/3118)) +- Fixed several typos ([#2990](https://github.com/docker-mailserver/docker-mailserver/pull/2990)) ([#2993](https://github.com/docker-mailserver/docker-mailserver/pull/2993)) +- SRS setup fixed ([#3158](https://github.com/docker-mailserver/docker-mailserver/pull/3158)) +- Postsrsd restart loop fixed ([#3160](https://github.com/docker-mailserver/docker-mailserver/pull/3160)) +- Order of DKIM/DMARC milters matters ([#3082](https://github.com/docker-mailserver/docker-mailserver/pull/3082)) +- Make logrotate state persistant ([#3077](https://github.com/docker-mailserver/docker-mailserver/pull/3077)) + +### Changed + +- the Dovecot community repository is now the default ([#2901](https://github.com/docker-mailserver/docker-mailserver/pull/2901)) +- moved SASL authentication socket location ([#3131](https://github.com/docker-mailserver/docker-mailserver/pull/3131)) +- only add Amavis configuration to Postfix when enabled ([#3046](https://github.com/docker-mailserver/docker-mailserver/pull/3046)) +- improve bug report template ([#3080](https://github.com/docker-mailserver/docker-mailserver/pull/3080)) +- remove Postfix DNSBLs ([#3069](https://github.com/docker-mailserver/docker-mailserver/pull/3069)) +- bigger script updates: + - split `setup-stack.sh` ([#3115](https://github.com/docker-mailserver/docker-mailserver/pull/3115)) + - housekeeping & cleanup setup ([#3121](https://github.com/docker-mailserver/docker-mailserver/pull/3121),[#3123](https://github.com/docker-mailserver/docker-mailserver/pull/3123)) + - issue warning in case of improper restart ([#3129](https://github.com/docker-mailserver/docker-mailserver/pull/3129)) + - remove PostSRSD wrapper ([#3128](https://github.com/docker-mailserver/docker-mailserver/pull/3128)) + - miscellaneous small improvements ([#3144](https://github.com/docker-mailserver/docker-mailserver/pull/3144)) +- improve Postfix config for spoof protection ([#3127](https://github.com/docker-mailserver/docker-mailserver/pull/3127)) +- Change Detection service - Remove 10 sec start-up delay ([#3064](https://github.com/docker-mailserver/docker-mailserver/pull/3064)) +- Postfix: + - Stop using `chroot` + remove wrapper script ([#3033](https://github.com/docker-mailserver/docker-mailserver/pull/3033)) + - SMTP Authentication via port 25 disabled ([#3006](https://github.com/docker-mailserver/docker-mailserver/pull/3006)) +- Fail2Ban - Added support packages + remove wrapper script ([#3032](https://github.com/docker-mailserver/docker-mailserver/pull/3032)) +- Replace path with variable in mail_state.sh ([#3153](https://github.com/docker-mailserver/docker-mailserver/pull/3153)) + +### Removed + +- configomat (submodule) ([#3045](https://github.com/docker-mailserver/docker-mailserver/pull/3045)) +- Due to deprecation: + - ARMv7 image support ([#2943](https://github.com/docker-mailserver/docker-mailserver/pull/2943)) + - TLS 1.2 is now the minimum supported protocol ([#2945](https://github.com/docker-mailserver/docker-mailserver/pull/2945)) + - ENV `SASL_PASSWD` ([#2946](https://github.com/docker-mailserver/docker-mailserver/pull/2946)) +- Redundant: + - Makefile `backup` target ([#3000](https://github.com/docker-mailserver/docker-mailserver/pull/3000)) + - ENV `ENABLE_POSTFIX_VIRTUAL_TRANSPORT` ([#3004](https://github.com/docker-mailserver/docker-mailserver/pull/3004)) + - `gamin` package ([#3030](https://github.com/docker-mailserver/docker-mailserver/pull/3030)) + ## [11.3.1](https://github.com/docker-mailserver/docker-mailserver/releases/tag/v11.3.1) ### Fixed @@ -58,7 +747,11 @@ All notable changes to this project will be documented in this file. The format ### Summary -This release features a lot of small and medium-sized changes, many related to how the image is build and tested during CI. The build now requires Docker Buildkit as the ClamAV Signatures are added via `COPY --link ...` during build-time. Moreover, the build is now multi-stage. `ENABLE_LDAP` is now deprecated. +This release features a lot of small and medium-sized changes, many related to how the image is build and tested during CI. The build now multi-stage based and requires Docker Buildkit, as the ClamAV Signatures are added via `COPY --link ...` during build-time. + +### Deprecated + +- The environment variable `ENABLE_LDAP` is deprecated and will be removed in [13.0.0]. Use `ACCOUNT_PROVISIONER=LDAP` now. ### Added @@ -85,10 +778,6 @@ This release features a lot of small and medium-sized changes, many related to h - **build**: adjust build arguments - **build**: enhance build process -### Deprecated - -- The environment variable `ENABLE_LDAP` is deprecated and will be removed in [13.0.0]. Use `ACCOUNT_PROVISIONER=LDAP` now. - ### Removed - **configuration**: remove unnecessary configuration files @@ -155,8 +844,8 @@ In this release the relay-host support saw [significant internal refactoring](ht 1. **Many** minor improvements were made (cleanup & refactoring). Please refer to the section below to get an overview over all improvements. Moreover, there was a lot of cleanup in the scripts and in the tests. The documentation was adjusted accordingly. 2. New environment variables were added: - 1. [`CLAMAV_MESSAGE_SIZE_LIMIT`](https://docker-mailserver.github.io/docker-mailserver/v11.0/config/environment/#clamav_message_size_limit) - 2. [`TZ`](https://docker-mailserver.github.io/docker-mailserver/v11.0/config/environment/#tz) + 1. [`CLAMAV_MESSAGE_SIZE_LIMIT`](https://docker-mailserver.github.io/docker-mailserver/v11.0/config/environment/#clamav_message_size_limit) + 2. [`TZ`](https://docker-mailserver.github.io/docker-mailserver/v11.0/config/environment/#tz) 3. SpamAssassin KAM was added with [`ENABLE_SPAMASSASSIN_KAM`](https://docker-mailserver.github.io/docker-mailserver/v11.0/config/environment/#enable_spamassassin_kam). 4. The `fail2ban` command was reworked and can now ban IP addresses as well. 5. There were a few small fixes, especially when it comes to bugs in scripts and service restart loops (no functionality changes, only fixes of existing functionality). When building an image from the Dockerfile - Installation of Postfix on modern Linux distributions should now always succeed. @@ -164,56 +853,55 @@ In this release the relay-host support saw [significant internal refactoring](ht ### Merged Pull Requests -- **[improvement]** tests: remove legacy functions / tests by @casperklein in [#2434](https://github.com/docker-mailserver/docker-mailserver/pull/2434) -- **[improvement]** `PERMIT_DOCKER=none` as new default value by @casperklein in [#2424](https://github.com/docker-mailserver/docker-mailserver/pull/2424) -- **[improvement]** Adjust environment variables to more sensible defaults by @georglauterbach in [#2428](https://github.com/docker-mailserver/docker-mailserver/pull/2428) -- **[fix]** macOS linting support by @NorseGaud in [#2448](https://github.com/docker-mailserver/docker-mailserver/pull/2448) -- **[improvement]** Rename config examples directory by @casperklein in [#2438](https://github.com/docker-mailserver/docker-mailserver/pull/2438) -- **[docs]** FAQ - Update naked/bare domain section by @sportshead in [#2446](https://github.com/docker-mailserver/docker-mailserver/pull/2446) -- **[improvement]** Remove obsolete `setup.sh debug inspect` command from usage description by @casperklein in [#2454](https://github.com/docker-mailserver/docker-mailserver/pull/2454) -- **[feature]** Introduce `CLAMAV_MESSAGE_SIZE_LIMIT` env by @casperklein in [#2453](https://github.com/docker-mailserver/docker-mailserver/pull/2453) -- **[fix]** remove SA reload for KAM by @georglauterbach in [#2456](https://github.com/docker-mailserver/docker-mailserver/pull/2456) -- **[docs]** Enhance logrotate description by @casperklein in [#2469](https://github.com/docker-mailserver/docker-mailserver/pull/2469) -- **[improvement]** Remove macOS specific code / support + shellcheck should avoid python, regardless of permissions by @NorseGaud in [#2466](https://github.com/docker-mailserver/docker-mailserver/pull/2466) -- **[docs]** Update fail2ban.md by @casperklein in [#2484](https://github.com/docker-mailserver/docker-mailserver/pull/2484) -- **[fix]** Makefile: Remove backup/restore of obsolete config directory by @casperklein in [#2479](https://github.com/docker-mailserver/docker-mailserver/pull/2479) -- **[improvement]** scripts: small refactorings by @georglauterbach in [#2485](https://github.com/docker-mailserver/docker-mailserver/pull/2485) -- **[fix]** Building on Ubuntu 21.10 failing to install postfix by @NorseGaud in [#2468](https://github.com/docker-mailserver/docker-mailserver/pull/2468) -- **[improvement]** Use FQDN as `REPORT_SENDER` default value by @casperklein in [#2487](https://github.com/docker-mailserver/docker-mailserver/pull/2487) -- **[improvement]** Improve test, get rid of sleep by @casperklein in [#2492](https://github.com/docker-mailserver/docker-mailserver/pull/2492) -- **[feature]** scripts: new log by @georglauterbach in [#2493](https://github.com/docker-mailserver/docker-mailserver/pull/2493) -- **[fix]** Restart supervisord early by @casperklein in [#2494](https://github.com/docker-mailserver/docker-mailserver/pull/2494) -- **[improvement]** scripts: renamed function `_errex` -> `_exit_with_error` by @georglauterbach in [#2497](https://github.com/docker-mailserver/docker-mailserver/pull/2497) -- **[improvement]** Remove invalid URL from SPF message by @casperklein in [#2503](https://github.com/docker-mailserver/docker-mailserver/pull/2503) -- **[improvement]** scripts: refactored scripts located under `target/bin/` by @georglauterbach in [#2500](https://github.com/docker-mailserver/docker-mailserver/pull/2500) -- **[improvement]** scripts: refactoring & miscellaneous small changes by @georglauterbach in [#2499](https://github.com/docker-mailserver/docker-mailserver/pull/2499) -- **[improvement]** scripts: refactored `daemon-stack.sh` by @georglauterbach in [#2496](https://github.com/docker-mailserver/docker-mailserver/pull/2496) -- **[fix]** add compatibility for Bash 4 to setup.sh by @georglauterbach in [#2519](https://github.com/docker-mailserver/docker-mailserver/pull/2519) -- **[fix]** tests: disabled "quota exceeded" test by @georglauterbach in [#2511](https://github.com/docker-mailserver/docker-mailserver/pull/2511) -- **[fix]** typo in setup-stack.sh by @eltociear in [#2521](https://github.com/docker-mailserver/docker-mailserver/pull/2521) -- **[improvement]** scripts: introduce `_log` to `sedfile` by @georglauterbach in [#2507](https://github.com/docker-mailserver/docker-mailserver/pull/2507) -- **[feature]** create `.github/FUNDING.yml` by @georglauterbach in [#2512](https://github.com/docker-mailserver/docker-mailserver/pull/2512) -- **[improvement]** scripts: refactored `check-for-changes.sh` by @georglauterbach in [#2498](https://github.com/docker-mailserver/docker-mailserver/pull/2498) -- **[improvement]** scripts: remove `DMS_DEBUG` by @georglauterbach in [#2523](https://github.com/docker-mailserver/docker-mailserver/pull/2523) -- **[feature]** firewall: replace `iptables` with `nftables` by @georglauterbach in [#2505](https://github.com/docker-mailserver/docker-mailserver/pull/2505) -- **[improvement]** log: adjust level and message(s) slightly for four messages by @georglauterbach in [#2532](https://github.com/docker-mailserver/docker-mailserver/pull/2532) -- **[improvement]** log: introduce proper log level fallback and env getter function by @georglauterbach in [#2506](https://github.com/docker-mailserver/docker-mailserver/pull/2506) -- **[feature]** scripts: added `TZ` environment variable to set timezone by @georglauterbach in [#2530](https://github.com/docker-mailserver/docker-mailserver/pull/2530) -- **[improvement]** setup: added grace period for account creation by @georglauterbach in [#2531](https://github.com/docker-mailserver/docker-mailserver/pull/2531) -- **[improvement]** refactor: letsencrypt implicit location discovery by @polarathene in [#2525](https://github.com/docker-mailserver/docker-mailserver/pull/2525) -- **[improvement]** setup.sh/setup: show usage when no argument is given by @casperklein in [#2540](https://github.com/docker-mailserver/docker-mailserver/pull/2540) -- **[improvement]** Dockerfile: Remove not needed ENVs and add comment by @casperklein in [#2541](https://github.com/docker-mailserver/docker-mailserver/pull/2541) -- **[improvement]** chore: (setup-stack.sh) Fix a small typo by @polarathene in [#2552](https://github.com/docker-mailserver/docker-mailserver/pull/2552) -- **[feature]** Add ban feature to fail2ban script by @casperklein in [#2538](https://github.com/docker-mailserver/docker-mailserver/pull/2538) -- **[fix]** Fix changedetector restart loop by @casperklein in [#2548](https://github.com/docker-mailserver/docker-mailserver/pull/2548) -- **[improvement]** chore: Drop `setup.sh` DATABASE fallback ENV by @polarathene in [#2556](https://github.com/docker-mailserver/docker-mailserver/pull/2556) +- **[improvement]** tests: remove legacy functions / tests [#2434](https://github.com/docker-mailserver/docker-mailserver/pull/2434) +- **[improvement]** `PERMIT_DOCKER=none` as new default value [#2424](https://github.com/docker-mailserver/docker-mailserver/pull/2424) +- **[improvement]** Adjust environment variables to more sensible defaults [#2428](https://github.com/docker-mailserver/docker-mailserver/pull/2428) +- **[fix]** macOS linting support [#2448](https://github.com/docker-mailserver/docker-mailserver/pull/2448) +- **[improvement]** Rename config examples directory [#2438](https://github.com/docker-mailserver/docker-mailserver/pull/2438) +- **[docs]** FAQ - Update naked/bare domain section [#2446](https://github.com/docker-mailserver/docker-mailserver/pull/2446) +- **[improvement]** Remove obsolete `setup.sh debug inspect` command from usage description [#2454](https://github.com/docker-mailserver/docker-mailserver/pull/2454) +- **[feature]** Introduce `CLAMAV_MESSAGE_SIZE_LIMIT` env [#2453](https://github.com/docker-mailserver/docker-mailserver/pull/2453) +- **[fix]** remove SA reload for KAM [#2456](https://github.com/docker-mailserver/docker-mailserver/pull/2456) +- **[docs]** Enhance logrotate description [#2469](https://github.com/docker-mailserver/docker-mailserver/pull/2469) +- **[improvement]** Remove macOS specific code / support + shellcheck should avoid python, regardless of permissions [#2466](https://github.com/docker-mailserver/docker-mailserver/pull/2466) +- **[docs]** Update fail2ban.md [#2484](https://github.com/docker-mailserver/docker-mailserver/pull/2484) +- **[fix]** Makefile: Remove backup/restore of obsolete config directory [#2479](https://github.com/docker-mailserver/docker-mailserver/pull/2479) +- **[improvement]** scripts: small refactorings [#2485](https://github.com/docker-mailserver/docker-mailserver/pull/2485) +- **[fix]** Building on Ubuntu 21.10 failing to install postfix [#2468](https://github.com/docker-mailserver/docker-mailserver/pull/2468) +- **[improvement]** Use FQDN as `REPORT_SENDER` default value [#2487](https://github.com/docker-mailserver/docker-mailserver/pull/2487) +- **[improvement]** Improve test, get rid of sleep [#2492](https://github.com/docker-mailserver/docker-mailserver/pull/2492) +- **[feature]** scripts: new log [#2493](https://github.com/docker-mailserver/docker-mailserver/pull/2493) +- **[fix]** Restart supervisord early [#2494](https://github.com/docker-mailserver/docker-mailserver/pull/2494) +- **[improvement]** scripts: renamed function `_errex` -> `_exit_with_error` [#2497](https://github.com/docker-mailserver/docker-mailserver/pull/2497) +- **[improvement]** Remove invalid URL from SPF message [#2503](https://github.com/docker-mailserver/docker-mailserver/pull/2503) +- **[improvement]** scripts: refactored scripts located under `target/bin/` [#2500](https://github.com/docker-mailserver/docker-mailserver/pull/2500) +- **[improvement]** scripts: refactoring & miscellaneous small changes [#2499](https://github.com/docker-mailserver/docker-mailserver/pull/2499) +- **[improvement]** scripts: refactored `daemon-stack.sh` [#2496](https://github.com/docker-mailserver/docker-mailserver/pull/2496) +- **[fix]** add compatibility for Bash 4 to setup.sh [#2519](https://github.com/docker-mailserver/docker-mailserver/pull/2519) +- **[fix]** tests: disabled "quota exceeded" test [#2511](https://github.com/docker-mailserver/docker-mailserver/pull/2511) +- **[fix]** typo in setup-stack.sh [#2521](https://github.com/docker-mailserver/docker-mailserver/pull/2521) +- **[improvement]** scripts: introduce `_log` to `sedfile` [#2507](https://github.com/docker-mailserver/docker-mailserver/pull/2507) +- **[feature]** create `.github/FUNDING.yml` [#2512](https://github.com/docker-mailserver/docker-mailserver/pull/2512) +- **[improvement]** scripts: refactored `check-for-changes.sh` [#2498](https://github.com/docker-mailserver/docker-mailserver/pull/2498) +- **[improvement]** scripts: remove `DMS_DEBUG` [#2523](https://github.com/docker-mailserver/docker-mailserver/pull/2523) +- **[feature]** firewall: replace `iptables` with `nftables` [#2505](https://github.com/docker-mailserver/docker-mailserver/pull/2505) +- **[improvement]** log: adjust level and message(s) slightly for four messages [#2532](https://github.com/docker-mailserver/docker-mailserver/pull/2532) +- **[improvement]** log: introduce proper log level fallback and env getter function [#2506](https://github.com/docker-mailserver/docker-mailserver/pull/2506) +- **[feature]** scripts: added `TZ` environment variable to set timezone [#2530](https://github.com/docker-mailserver/docker-mailserver/pull/2530) +- **[improvement]** setup: added grace period for account creation [#2531](https://github.com/docker-mailserver/docker-mailserver/pull/2531) +- **[improvement]** refactor: letsencrypt implicit location discovery [#2525](https://github.com/docker-mailserver/docker-mailserver/pull/2525) +- **[improvement]** setup.sh/setup: show usage when no argument is given [#2540](https://github.com/docker-mailserver/docker-mailserver/pull/2540) +- **[improvement]** Dockerfile: Remove not needed ENVs and add comment [#2541](https://github.com/docker-mailserver/docker-mailserver/pull/2541) +- **[improvement]** chore: (setup-stack.sh) Fix a small typo [#2552](https://github.com/docker-mailserver/docker-mailserver/pull/2552) +- **[feature]** Add ban feature to fail2ban script [#2538](https://github.com/docker-mailserver/docker-mailserver/pull/2538) +- **[fix]** Fix changedetector restart loop [#2548](https://github.com/docker-mailserver/docker-mailserver/pull/2548) +- **[improvement]** chore: Drop `setup.sh` DATABASE fallback ENV [#2556](https://github.com/docker-mailserver/docker-mailserver/pull/2556) ## `v10.5.0` ### Critical Changes -1. This release fixes a critical issue for LDAP users, installing a needed package on Debian 11 - on build-time. Moreover, a race-condition was eliminated ([#2341](https://github.com/docker-mailserver/docker-mailserver/pull/2341)). +1. This release fixes a critical issue for LDAP users, installing a needed package on Debian 11 on build-time. Moreover, a race-condition was eliminated ([#2341](https://github.com/docker-mailserver/docker-mailserver/pull/2341)). 2. A resource leak in `check-for-changes.sh` was fixed ([#2401](https://github.com/docker-mailserver/docker-mailserver/pull/2401)) ### Other Minor Changes @@ -238,9 +926,9 @@ In this release the relay-host support saw [significant internal refactoring](ht - **[fix]** Fixed non-number-argument in `listmailuser` in [#2382](https://github.com/docker-mailserver/docker-mailserver/pull/2382) - **[fix]** docs: Fail2Ban - Fix links for rootless podman in [#2384](https://github.com/docker-mailserver/docker-mailserver/pull/2384) - **[fix]** docs(kubernetes): fix image name in example in [#2385](https://github.com/docker-mailserver/docker-mailserver/pull/2385) -- **[fix]** SSL documentation contains a small bug #2381 by @Twist235 in [#2383](https://github.com/docker-mailserver/docker-mailserver/pull/2383) +- **[fix]** SSL documentation contains a small bug #2381 [#2383](https://github.com/docker-mailserver/docker-mailserver/pull/2383) - **[fix]** get rid of subshell + `exec` in `helper-functions.sh` in [#2401](https://github.com/docker-mailserver/docker-mailserver/pull/2401) -- **[docs]** Rootless Podman security update by @p-fruck in [#2393](https://github.com/docker-mailserver/docker-mailserver/pull/2393) +- **[docs]** Rootless Podman security update [#2393](https://github.com/docker-mailserver/docker-mailserver/pull/2393) - **[fix]** fix: double occurrence of `/etc/postfix/regexp` in [#2397](https://github.com/docker-mailserver/docker-mailserver/pull/2397) - **[improvement]** consistently make 1 the default value for `SPAMASSASSIN_SPAM_TO_INBOX` in [#2361](https://github.com/docker-mailserver/docker-mailserver/pull/2361) - **[docs]** added sieve example for subaddress sorting in [#2410](https://github.com/docker-mailserver/docker-mailserver/pull/2410) @@ -384,12 +1072,12 @@ This release improves on `9.1.0` in many aspect, including general fixes, Fail2B ## `v9.1.0` -This release marks the breakpoint where the wiki was transferred to a [reworked documentation](https://docker-mailserver.github.io/docker-mailserver/edge/) +This release marks the breakpoint where the wiki was transferred to a [reworked documentation](https://docker-mailserver.github.io/docker-mailserver/latest/) - **[feat]** Introduce ENABLE_AMAVIS env ([#1866](https://github.com/docker-mailserver/docker-mailserver/pull/1866)) - **[docs]** Move wiki to gh-pages ([#1826](https://github.com/docker-mailserver/docker-mailserver/pull/1826)) - Special thanks to @polarathene 👨🏻‍💻 - You can [edit the docs](https://github.com/docker-mailserver/docker-mailserver/tree/master/docs/content) now directly with your code changes - - Documentation is now versioned related to docker image versions and viewable here: + - Documentation is now versioned related to docker image versions and viewable here: ## `v9.0.1` diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md deleted file mode 100644 index 83e766a2e0a..00000000000 --- a/CONTRIBUTORS.md +++ /dev/null @@ -1,1891 +0,0 @@ -Thanks goes to these wonderful people ✨ - -## Contributors - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - Thomas -
- Thomas VIAL -
-
- - Georg -
- Georg Lauterbach -
-
- - Casper/ -
- Casper -
-
- - Erik -
- Erik Wramner -
-
- - Brennan -
- Brennan Kinney -
-
- - Jean-Denis -
- Jean-Denis Vauguet -
-
- - Martin -
- Martin Schulze -
-
- - Frederic -
- Frederic Werner -
-
- - Josef -
- Josef Friedrich -
-
- - Johan -
- Johan Smits -
-
- - youtous/ -
- youtous -
-
- - 17Halbe/ -
- 17Halbe -
-
- - Nathan -
- Nathan Pierce -
-
- - Thorsten -
- Thorsten von Eicken -
-
- - Germain -
- Germain Masse -
-
- - 00angus/ -
- 00angus -
-
- - Paul -
- Paul Steinlechner -
-
- - Dominik -
- Dominik Winter -
-
- - Paul -
- Paul Adams -
-
- - Felix -
- Felix Bartels -
-
- - Sebastian -
- Sebastian Wiesendahl -
-
- - Steve -
- Steve Johnson -
-
- - André -
- André Stein -
-
- - William -
- William Desportes -
-
- - omarc1492/ -
- omarc1492 -
-
- - Christian -
- Christian Glahn -
-
- - Marek -
- Marek Walczak -
-
- - Kai -
- Kai Ren -
-
- - Kyle -
- Kyle Ondy -
-
- - Daniel -
- Daniel Panteleit -
-
- - Michael/ -
- Michael -
-
- - lukas/ -
- lukas -
-
- - Sascha -
- Sascha Scandella -
-
- - Lukáš -
- Lukáš Vasek -
-
- - Andreas -
- Andreas Perhab -
-
- - vortex852456/ -
- vortex852456 -
-
- - Christian -
- Christian Grasso -
-
- - Hans-Cees -
- Hans-Cees Speel -
-
- - Dashamir -
- Dashamir Hoxha -
-
- - GAVARD -
- GAVARD Ewann -
-
- - Jack -
- Jack Twilley -
-
- - Luke -
- Luke Cyca -
-
- - Oleg -
- Oleg Kainov -
-
- - Robert -
- Robert Dolca -
-
- - Thomas -
- Thomas Kilian -
-
- - Tobias -
- Tobias Rittig -
-
- - akmet/ -
- akmet -
-
- - Arne -
- Arne Kepp -
-
- - Dennis -
- Dennis Stumm -
-
- - Moritz -
- Moritz Marquardt -
-
- - pyy/ -
- pyy -
-
- - Anne/ -
- Anne -
-
- - Birkenstab/ -
- Birkenstab -
-
- - Brandon -
- Brandon Schmitt -
-
- - Cédric -
- Cédric Laubacher -
-
- - GrupoCITEC/ -
- GrupoCITEC -
-
- - Jairo -
- Jairo Llopis -
-
- - James/ -
- James -
-
- - Jarrod -
- Jarrod Smith -
-
- - Patrizio -
- Patrizio Bekerle -
-
- - Rubytastic2/ -
- Rubytastic2 -
-
- - Semir -
- Semir Patel -
-
- - Wolfgang -
- Wolfgang Ocker -
-
- - Zehir/ -
- Zehir -
-
- - guardiande/ -
- guardiande -
-
- - kamuri/ -
- kamuri -
-
- - davidszp/ -
- davidszp -
-
- - Andreas -
- Andreas Gerstmayr -
-
- - Marko -
- Marko J -
-
- - Michael -
- Michael Schmoock -
-
- - VanVan/ -
- VanVan -
-
- - Alexander -
- Alexander Elbracht -
-
- - Amin -
- Amin Vakil -
-
- - Andrew -
- Andrew Low -
-
- - Ask -
- Ask Bjørn Hansen -
-
- - Ben/ -
- Ben -
-
- - Christian -
- Christian Raue -
-
- - Darren -
- Darren McGrandle -
-
- - Dominik -
- Dominik Bruhn -
-
- - DuncanvR/ -
- DuncanvR -
-
- - Emanuele -
- Emanuele Mazzotta -
-
- - FL42/ -
- FL42 -
-
- - Guillaume -
- Guillaume Simon -
-
- - Ikko -
- Ikko Eltociear Ashimine -
-
- - James -
- James Fryer -
-
- - Millaguie/ -
- Millaguie -
-
- - Jeremy -
- Jeremy Shipman -
-
- - Jonas -
- Jonas Kalderstam -
-
- - Louis/ -
- Louis -
-
- - martinwepner/ -
- martinwepner -
-
- - Michael -
- Michael Als -
-
- - Morgan -
- Morgan Kesler -
-
- - Pablo -
- Pablo Castorino -
-
- - Philipp -
- Philipp Fruck -
-
- - Rainer -
- Rainer Rillke -
-
- - Bob -
- Bob Gregor -
-
- - r-pufky/ -
- r-pufky -
-
- - andymel/ -
- andymel -
-
- - bigpigeon/ -
- bigpigeon -
-
- - engelant/ -
- engelant -
-
- - j-marz/ -
- j-marz -
-
- - lokipo/ -
- lokipo -
-
- - msheakoski/ -
- msheakoski -
-
- - Felix/ -
- Felix -
-
- - Leon -
- Leon Busch-George -
-
- - Marius -
- Marius Panneck -
-
- - Thomas -
- Thomas Willems -
-
- - 0xflotus/ -
- 0xflotus -
-
- - Ivan -
- Ivan Fokeev -
-
- - 20th/ -
- 20th -
-
- - 2b/ -
- 2b -
-
- - Max:/ -
- Max: -
-
- - Achim -
- Achim Christ -
-
- - Adrian -
- Adrian Pistol -
-
- - Alexander -
- Alexander Kachkaev -
-
- - Alexander -
- Alexander Neu -
-
- - Bedniakov -
- Bedniakov Aleksei -
-
- - Andreas -
- Andreas Egli -
-
- - Andrew -
- Andrew Cornford -
-
- - Andrey -
- Andrey Likhodievskiy -
-
- - Arash -
- Arash Fatahzade -
-
- - Arthur -
- Arthur Outhenin-Chalandre -
-
- - Astro/ -
- Astro -
-
- - Benedict -
- Benedict Endemann -
-
- - Bogdan/ -
- Bogdan -
-
- - Charles -
- Charles Harris -
-
- - Christian -
- Christian Musa -
-
- - Christoph/ -
- Christoph -
-
- - Claus -
- Claus Beerta -
-
- - Damian -
- Damian Moore -
-
- - espitall/ -
- espitall -
-
- - Daniel -
- Daniel Karski -
-
- - Daniele -
- Daniele Bellavista -
-
- - Daniël -
- Daniël van den Berg -
-
- - Dingoz/ -
- Dingoz -
-
- - Dmitry -
- Dmitry R. -
-
- - Dorian -
- Dorian Ayllón -
-
- - Edmond -
- Edmond Varga -
-
- - Eduard -
- Eduard Knysh -
-
- - Elisei -
- Elisei Roca -
-
- - Erick -
- Erick Calder -
-
- - Erik -
- Erik Brakkee -
-
- - Huncode/ -
- Huncode -
-
- - Florian/ -
- Florian -
-
- - Florian -
- Florian Roks -
-
- - Franz -
- Franz Keferböck -
-
- - Frugan/ -
- Frugan -
-
- - Gabriel -
- Gabriel Euzet -
-
- - Gabriel -
- Gabriel Landais -
-
- - GiovanH/ -
- GiovanH -
-
- - H4R0/ -
- H4R0 -
-
- - Harry -
- Harry Youd -
-
- - Hugues -
- Hugues Granger -
-
- - Ian -
- Ian Andrews -
-
- - Influencer/ -
- Influencer -
-
- - jcalfee/ -
- jcalfee -
-
- - JS -
- JS Légaré -
-
- - Jeidnx/ -
- Jeidnx -
-
- - JiLleON/ -
- JiLleON -
-
- - Jiří -
- Jiří Kozlovský -
-
- - jmccl/ -
- jmccl -
-
- - Jurek -
- Jurek Barth -
-
- - JOnathan -
- JOnathan duMonT -
-
- - Kaan/ -
- Kaan -
-
- - Karthik -
- Karthik K -
-
- - KCrawley/ -
- KCrawley -
-
- - Khue -
- Khue Doan -
-
- - Lars -
- Lars Pötter -
-
- - Leo -
- Leo Winter -
-
- - MadsRC/ -
- MadsRC -
-
- - Mathieu -
- Mathieu Brunot -
-
- - Maximilian -
- Maximilian Hippler -
-
- - Michael -
- Michael G. -
-
- - Michael -
- Michael Jensen -
-
- - Michel -
- Michel Albert -
-
- - Mohammed -
- Mohammed Chotia -
-
- - Mohammed -
- Mohammed Noureldin -
-
- - Moritz -
- Moritz Poldrack -
-
- - Naveen/ -
- Naveen -
-
- - Nicholas -
- Nicholas Pepper -
-
- - Nick -
- Nick Pappas -
-
- - Nils -
- Nils Knappmeier -
-
- - Olivier -
- Olivier Picquenot -
-
- - Orville -
- Orville Q. Song -
-
- - Ovidiu -
- Ovidiu Predescu -
-
- - Petar -
- Petar Šegina -
-
- - Peter -
- Peter Hartmann -
-
- - Pierre-Yves -
- Pierre-Yves Rofes -
-
- - Remo -
- Remo E -
-
- - René -
- René Plötz -
-
- - Roman -
- Roman Seyffarth -
-
- - Sam -
- Sam Collins -
-
- - Scott -
- Scott Weldon -
-
- - Sebastian -
- Sebastian Straub -
-
- - Serge -
- Serge van den Boom -
-
- - Sergey -
- Sergey Nazaryev -
-
- - Shyim/ -
- Shyim -
-
- - Simon -
- Simon J Mudd -
-
- - Simon -
- Simon Schröter -
-
- - Stephan/ -
- Stephan -
-
- - Stig -
- Stig Otnes Kolstad -
-
- - Sven -
- Sven Kauber -
-
- - Sylvain -
- Sylvain Benner -
-
- - Sylvain -
- Sylvain Dumont -
-
- - TechnicLab/ -
- TechnicLab -
-
- - Thomas -
- Thomas Schmit -
-
- - Tin/ -
- Tin -
-
- - Torben -
- Torben Weibert -
-
- - Toru -
- Toru Hisai -
-
- - Trangar/ -
- Trangar -
-
- - Twist235/ -
- Twist235 -
-
- - Vasiliy -
- Vasiliy Gokoyev -
-
- - Victoria -
- Victoria Brekenfeld -
-
- - Vilius/ -
- Vilius -
-
- - Wim/ -
- Wim -
-
- - Y.C.Huang/ -
- Y.C.Huang -
-
- - arcaine2/ -
- arcaine2 -
-
- - awb99/ -
- awb99 -
-
- - brainkiller/ -
- brainkiller -
-
- - cternes/ -
- cternes -
-
- - dborowy/ -
- dborowy -
-
- - dimalo/ -
- dimalo -
-
- - eleith/ -
- eleith -
-
- - helmutundarnold/ -
- helmutundarnold -
-
- - hnws/ -
- hnws -
-
- - i-C-o-d-e-r/ -
- i-C-o-d-e-r -
-
- - idaadi/ -
- idaadi -
-
- - ixeft/ -
- ixeft -
-
- - jjtt/ -
- jjtt -
-
- - jose -
- jose nazario -
-
- - landergate/ -
- landergate -
-
- - magnus -
- magnus anderssen -
-
- - marios88/ -
- marios88 -
-
- - matrixes/ -
- matrixes -
-
- - mchamplain/ -
- mchamplain -
-
- - Jason -
- Jason Miller -
-
- - mplx/ -
- mplx -
-
- - odinis/ -
- odinis -
-
- - okami/ -
- okami -
-
- - olaf-mandel/ -
- olaf-mandel -
-
- - ontheair81/ -
- ontheair81 -
-
- - pravynandas/ -
- pravynandas -
-
- - presocratics/ -
- presocratics -
-
- - rhyst/ -
- rhyst -
-
- - schnippl0r/ -
- schnippl0r -
-
- - smargold476/ -
- smargold476 -
-
- - sportshead/ -
- sportshead -
-
- - squash/ -
- squash -
-
- - strarsis/ -
- strarsis -
-
- - tamueller/ -
- tamueller -
-
- - vivacarvajalito/ -
- vivacarvajalito -
-
- - wolkenschieber/ -
- wolkenschieber -
-
- - worldworm/ -
- worldworm -
-
- -## Further Contributors - -Also thanks goes to these wonderful people, that have contributed in various other ways than code lines ✨ - -[Emoji Key ✨ (and Contribution Types)](https://allcontributors.org/docs/en/emoji-key) - - - - - - - - -

matrixes

📝
- - - - - - -This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! - -____ - -Note: - -We started using [all-contributors](https://github.com/all-contributors/all-contributors) in July 2021. We will add contributors with their future PRs or Issues. Code contributions are added automatically. If you are [one of the 200+](https://github.com/docker-mailserver/docker-mailserver/graphs/contributors) that contributed to the project in the past and would like to see your name here too, please reach out! diff --git a/Dockerfile b/Dockerfile index 2e49b332717..e2f7fc0dc0c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,18 +1,18 @@ # syntax=docker.io/docker/dockerfile:1 -# This Dockerfile provides two stages: stage-base and stage-final +# This Dockerfile provides four stages: stage-base, stage-compile, stage-main and stage-final # This is in preparation for more granular stages (eg ClamAV and Fail2Ban split into their own) -# -# Base stage provides all packages, config, and adds scripts -# - -FROM docker.io/debian:11-slim AS stage-base - ARG DEBIAN_FRONTEND=noninteractive -ARG DOVECOT_COMMUNITY_REPO=1 +ARG DOVECOT_COMMUNITY_REPO=0 ARG LOG_LEVEL=trace +FROM docker.io/debian:12-slim AS stage-base + +ARG DEBIAN_FRONTEND +ARG DOVECOT_COMMUNITY_REPO +ARG LOG_LEVEL + SHELL ["/bin/bash", "-e", "-o", "pipefail", "-c"] # ----------------------------------------------- @@ -25,10 +25,34 @@ RUN </etc/cron.d/clamav-freshclam chmod 644 /etc/clamav/freshclam.conf sedfile -i 's/Foreground false/Foreground true/g' /etc/clamav/clamd.conf @@ -53,22 +80,14 @@ EOF # --- Dovecot ----------------------------------- # ----------------------------------------------- +# install fts_xapian plugin + +COPY --from=stage-compile dovecot-fts-xapian-*.deb / +RUN dpkg -i /dovecot-fts-xapian-*.deb && rm /dovecot-fts-xapian-*.deb + COPY target/dovecot/*.inc target/dovecot/*.conf /etc/dovecot/conf.d/ -COPY target/dovecot/sieve/ /etc/dovecot/sieve/ COPY target/dovecot/dovecot-purge.cron /etc/cron.d/dovecot-purge.disabled RUN chmod 0 /etc/cron.d/dovecot-purge.disabled -WORKDIR /usr/share/dovecot - -# hadolint ignore=SC2016,SC2086,SC2069 -RUN </etc/default/spamassassin sedfile -i -r 's/^\$INIT restart/supervisorctl restart amavis/g' /etc/spamassassin/sa-update-hooks.d/amavisd-new - mkdir -p /etc/spamassassin/kam/ + mkdir /etc/spamassassin/kam/ curl -sSfLo /etc/spamassassin/kam/kam.sa-channels.mcgrail.com.key https://mcgrail.com/downloads/kam.sa-channels.mcgrail.com.key EOF @@ -103,12 +131,10 @@ EOF COPY target/postsrsd/postsrsd /etc/default/postsrsd COPY target/postgrey/postgrey /etc/default/postgrey -COPY target/postgrey/postgrey.init /etc/init.d/postgrey RUN <>/etc/postfix-policyd-spf-python/policyd-spf.conf COPY target/fetchmail/fetchmailrc /etc/fetchmailrc_general +COPY target/getmail/getmailrc_general /etc/getmailrc_general +COPY target/getmail/getmail-service.sh /usr/local/bin/ COPY target/postfix/main.cf target/postfix/master.cf /etc/postfix/ # DH parameters for DHE cipher suites, ffdhe4096 is the official standard 4096-bit DH params now part of TLS 1.3 @@ -195,7 +215,7 @@ EOF RUN < /dev/null|/usr/bin/supervisorctl signal hup rsyslog >/dev/null|g' /usr/lib/rsyslog/rsyslog-rotate + # this change is for our alternative process manager rather than part of + # a fix related to the change preceding it. + echo -e '\n/usr/bin/supervisorctl signal hup rsyslog >/dev/null' >>/usr/lib/rsyslog/rsyslog-rotate EOF # ----------------------------------------------- @@ -225,6 +247,7 @@ EOF # ----------------------------------------------- COPY target/logwatch/maillog.conf /etc/logwatch/conf/logfiles/maillog.conf +COPY target/logwatch/ignore.conf /etc/logwatch/conf/ignore.conf # ----------------------------------------------- # --- Supervisord & Start ----------------------- @@ -242,12 +265,8 @@ RUN < -We provide a [dedicated documentation][documentation::web] hosted on GitHub Pages. Make sure to read it as it contains all the information necessary to set up and configure your mail server. The documentation is crafted with Markdown & [MkDocs Material](https://squidfunk.github.io/mkdocs-material/). +> [!TIP] +> Be sure to read [our documentation][documentation::web]. It provides guidance on initial setup of your mail server. -## :boom: Issues - -If you have issues, please search through [the documentation][documentation::web] **for your version** before opening an issue. The issue tracker is for issues, not for personal support. Make sure the version of the documentation matches the image version you're using! +> [!IMPORTANT] +> If you have issues, please search through [the documentation][documentation::web] **for your version** before opening an issue. +> +> The issue tracker is for issues, not for personal support. +> Make sure the version of the documentation matches the image version you're using! ## :link: Links to Useful Resources -1. [FAQ](https://docker-mailserver.github.io/docker-mailserver/edge/faq/) -2. [Usage](https://docker-mailserver.github.io/docker-mailserver/edge/usage/) -3. [Examples](https://docker-mailserver.github.io/docker-mailserver/edge/examples/tutorials/basic-installation/) -4. [Issues and Contributing](https://docker-mailserver.github.io/docker-mailserver/edge/contributing/issues-and-pull-requests/) +1. [FAQ](https://docker-mailserver.github.io/docker-mailserver/latest/faq/) +2. [Usage](https://docker-mailserver.github.io/docker-mailserver/latest/usage/) +3. [Examples](https://docker-mailserver.github.io/docker-mailserver/latest/examples/tutorials/basic-installation/) +4. [Issues and Contributing](https://docker-mailserver.github.io/docker-mailserver/latest/contributing/issues-and-pull-requests/) 5. [Release Notes](./CHANGELOG.md) -6. [Environment Variables](https://docker-mailserver.github.io/docker-mailserver/edge/config/environment/) -7. [Updating](https://docker-mailserver.github.io/docker-mailserver/edge/faq/#how-do-i-update-dms) +6. [Environment Variables](https://docker-mailserver.github.io/docker-mailserver/latest/config/environment/) +7. [Updating](https://docker-mailserver.github.io/docker-mailserver/latest/faq/#how-do-i-update-dms) ## :package: Included Services -- [Postfix](http://www.postfix.org) with SMTP or LDAP authentication and support for [extension delimiters](http://www.postfix.org/postconf.5.html#recipient_delimiter) (_mail to `you+extension@example.com` delivered to `you@example.com`_) -- [Dovecot](https://www.dovecot.org) with SASL, IMAP, POP3, LDAP, [basic Sieve support](https://docker-mailserver.github.io/docker-mailserver/edge/config/advanced/mail-sieve) and [quotas](https://docker-mailserver.github.io/docker-mailserver/edge/config/user-management/accounts#notes) +- [Postfix](http://www.postfix.org) with SMTP or LDAP authentication and support for [extension delimiters](https://docker-mailserver.github.io/docker-mailserver/latest/config/account-management/overview/#aliases) +- [Dovecot](https://www.dovecot.org) with SASL, IMAP, POP3, LDAP, [basic Sieve support](https://docker-mailserver.github.io/docker-mailserver/latest/config/advanced/mail-sieve) and [quotas](https://docker-mailserver.github.io/docker-mailserver/latest/config/account-management/overview/#quotas) - [Rspamd](https://rspamd.com/) - [Amavis](https://www.amavis.org/) - [SpamAssassin](http://spamassassin.apache.org/) supporting custom rules @@ -42,8 +47,10 @@ If you have issues, please search through [the documentation][documentation::web - [OpenDKIM](http://www.opendkim.org) & [OpenDMARC](https://github.com/trusteddomainproject/OpenDMARC) - [Fail2ban](https://www.fail2ban.org/wiki/index.php/Main_Page) - [Fetchmail](http://www.fetchmail.info/fetchmail-man.html) +- [Getmail6](https://getmail6.org/documentation.html) - [Postscreen](http://www.postfix.org/POSTSCREEN_README.html) - [Postgrey](https://postgrey.schweikert.ch/) - Support for [LetsEncrypt](https://letsencrypt.org/), manual and self-signed certificates -- A [setup script](https://docker-mailserver.github.io/docker-mailserver/edge/config/setup.sh) for easy configuration and maintenance +- A [setup script](https://docker-mailserver.github.io/docker-mailserver/latest/config/setup.sh) for easy configuration and maintenance - SASLauthd with LDAP authentication +- OAuth2 authentication (_via `XOAUTH2` or `OAUTHBEARER` SASL mechanisms_) diff --git a/VERSION b/VERSION deleted file mode 100644 index 0a47c95bb14..00000000000 --- a/VERSION +++ /dev/null @@ -1 +0,0 @@ -11.3.1 diff --git a/docker-compose.yml b/compose.yaml similarity index 53% rename from docker-compose.yml rename to compose.yaml index ee5f7a86e1d..2d5491d95d8 100644 --- a/docker-compose.yml +++ b/compose.yaml @@ -1,17 +1,14 @@ services: mailserver: - image: docker.io/mailserver/docker-mailserver:latest + image: ghcr.io/docker-mailserver/docker-mailserver:latest container_name: mailserver - # If the FQDN for your mail-server is only two labels (eg: example.com), - # you can assign this entirely to `hostname` and remove `domainname`. - hostname: mail - domainname: example.com + # Provide the FQDN of your mail server here (Your DNS MX record should point to this value) + hostname: mail.example.com env_file: mailserver.env # More information about the mail-server ports: - # https://docker-mailserver.github.io/docker-mailserver/edge/config/security/understanding-the-ports/ - # To avoid conflicts with yaml base-60 float, DO NOT remove the quotation marks. + # https://docker-mailserver.github.io/docker-mailserver/latest/config/security/understanding-the-ports/ ports: - - "25:25" # SMTP (explicit TLS => STARTTLS) + - "25:25" # SMTP (explicit TLS => STARTTLS, Authentication is DISABLED => use port 465/587 instead) - "143:143" # IMAP4 (explicit TLS => STARTTLS) - "465:465" # ESMTP (implicit TLS) - "587:587" # ESMTP (explicit TLS => STARTTLS) @@ -24,9 +21,10 @@ services: - /etc/localtime:/etc/localtime:ro restart: always stop_grace_period: 1m - cap_add: - - NET_ADMIN + # Uncomment if using `ENABLE_FAIL2BAN=1`: + # cap_add: + # - NET_ADMIN healthcheck: - test: "ss --listening --tcp | grep -P 'LISTEN.+:smtp' || exit 1" + test: "ss --listening --ipv4 --tcp | grep --silent ':smtp' || exit 1" timeout: 3s retries: 0 diff --git a/config-examples/dovecot.cf b/config-examples/dovecot.cf index 0222fc909f1..d9010b7ac09 100644 --- a/config-examples/dovecot.cf +++ b/config-examples/dovecot.cf @@ -1,4 +1,4 @@ # File for additional dovecot configurations. -# For more informations read https://doc.dovecot.org/configuration_manual/quick_configuration/ +# For more information read https://doc.dovecot.org/configuration_manual/quick_configuration/ #mail_max_userip_connections = 50 diff --git a/config-examples/fail2ban-fail2ban.cf b/config-examples/fail2ban-fail2ban.cf index 8ed2833c89a..00e9a25fec5 100644 --- a/config-examples/fail2ban-fail2ban.cf +++ b/config-examples/fail2ban-fail2ban.cf @@ -5,11 +5,11 @@ # Changes: in most of the cases you should not modify this # file, but provide customizations in fail2ban.local file, e.g.: # -# [Definition] +# [DEFAULT] # loglevel = DEBUG # -[Definition] +[DEFAULT] # Option: loglevel # Notes.: Set the log level output. @@ -19,26 +19,26 @@ # NOTICE # INFO # DEBUG -# Values: [ LEVEL ] Default: ERROR +# Values: [ LEVEL ] Default: INFO # -#loglevel = INFO +loglevel = INFO # Option: logtarget -# Notes.: Set the log target. This could be a file, SYSLOG, STDERR or STDOUT. +# Notes.: Set the log target. This could be a file, SYSTEMD-JOURNAL, SYSLOG, STDERR or STDOUT. # Only one log target can be specified. # If you change logtarget from the default value and you are # using logrotate -- also adjust or disable rotation in the # corresponding configuration file # (e.g. /etc/logrotate.d/fail2ban on Debian systems) -# Values: [ STDOUT | STDERR | SYSLOG | SYSOUT | FILE ] Default: STDERR +# Values: [ STDOUT | STDERR | SYSLOG | SYSOUT | SYSTEMD-JOURNAL | FILE ] Default: STDERR # -#logtarget = /var/log/fail2ban.log +logtarget = /var/log/fail2ban.log # Option: syslogsocket # Notes: Set the syslog socket file. Only used when logtarget is SYSLOG # auto uses platform.system() to determine predefined paths # Values: [ auto | FILE ] Default: auto -#syslogsocket = auto +syslogsocket = auto # Option: socket # Notes.: Set the socket file. This is used to communicate with the daemon. Do @@ -46,24 +46,47 @@ # communicate with the server afterwards. # Values: [ FILE ] Default: /var/run/fail2ban/fail2ban.sock # -#socket = /var/run/fail2ban/fail2ban.sock +socket = /var/run/fail2ban/fail2ban.sock # Option: pidfile # Notes.: Set the PID file. This is used to store the process ID of the # fail2ban server. # Values: [ FILE ] Default: /var/run/fail2ban/fail2ban.pid # -#pidfile = /var/run/fail2ban/fail2ban.pid +pidfile = /var/run/fail2ban/fail2ban.pid + +# Option: allowipv6 +# Notes.: Allows IPv6 interface: +# Default: auto +# Values: [ auto yes (on, true, 1) no (off, false, 0) ] Default: auto +#allowipv6 = auto # Options: dbfile # Notes.: Set the file for the fail2ban persistent data to be stored. -# A value of ":memory:" means database is only stored in memory +# A value of ":memory:" means database is only stored in memory # and data is lost when fail2ban is stopped. # A value of "None" disables the database. # Values: [ None :memory: FILE ] Default: /var/lib/fail2ban/fail2ban.sqlite3 -#dbfile = /var/lib/fail2ban/fail2ban.sqlite3 +dbfile = /var/lib/fail2ban/fail2ban.sqlite3 # Options: dbpurgeage # Notes.: Sets age at which bans should be purged from the database # Values: [ SECONDS ] Default: 86400 (24hours) -#dbpurgeage = 1d +dbpurgeage = 1d + +# Options: dbmaxmatches +# Notes.: Number of matches stored in database per ticket (resolvable via +# tags / in actions) +# Values: [ INT ] Default: 10 +dbmaxmatches = 10 + +[Definition] + + +[Thread] + +# Options: stacksize +# Notes.: Specifies the stack size (in KiB) to be used for subsequently created threads, +# and must be 0 or a positive integer value of at least 32. +# Values: [ SIZE ] Default: 0 (use platform or configured default) +#stacksize = 0 diff --git a/config-examples/fail2ban-jail.cf b/config-examples/fail2ban-jail.cf index 9611e7e0b66..41e9fbe1379 100644 --- a/config-examples/fail2ban-jail.cf +++ b/config-examples/fail2ban-jail.cf @@ -1,14 +1,14 @@ [DEFAULT] # "bantime" is the number of seconds that a host is banned. -bantime = 3h +bantime = 1w # A host is banned if it has generated "maxretry" during the last "findtime" # seconds. -findtime = 10m +findtime = 1w # "maxretry" is the number of failures before a host get banned. -maxretry = 3 +maxretry = 6 # "ignoreip" can be a list of IP addresses, CIDR masks or DNS hosts. Fail2ban # will not ban a host which matches an address in this list. Several addresses @@ -25,9 +25,9 @@ enabled = true [postfix] enabled = true - -[postfix-sasl] -enabled = true +# For a reference on why this mode was chose, see +# https://github.com/docker-mailserver/docker-mailserver/issues/3256#issuecomment-1511188760 +mode = extra # This jail is used for manual bans. # To ban an IP address use: setup.sh fail2ban ban diff --git a/config-examples/getmail/getmailrc_general.cf b/config-examples/getmail/getmailrc_general.cf new file mode 100644 index 00000000000..73fc443f222 --- /dev/null +++ b/config-examples/getmail/getmailrc_general.cf @@ -0,0 +1,11 @@ +# https://getmail6.org/configuration.html#conf-options + +[options] +verbose = 0 +read_all = false +delete = false +max_messages_per_session = 500 +received = false +delivered_to = false +message_log_syslog = true + diff --git a/config-examples/getmail/imap-example.cf b/config-examples/getmail/imap-example.cf new file mode 100644 index 00000000000..fa4fb7d6eb3 --- /dev/null +++ b/config-examples/getmail/imap-example.cf @@ -0,0 +1,13 @@ +# https://getmail6.org/configuration.html + +[retriever] +type = SimpleIMAPSSLRetriever +server = imap.gmail.com +username = alice +password = notsecure + +[destination] +type = MDA_external +path = /usr/lib/dovecot/deliver +allow_root_commands = true +arguments =("-d","user1@example.com") diff --git a/config-examples/getmail/pop3-example.cf b/config-examples/getmail/pop3-example.cf new file mode 100644 index 00000000000..dde60c8710c --- /dev/null +++ b/config-examples/getmail/pop3-example.cf @@ -0,0 +1,13 @@ +# https://getmail6.org/configuration.html + +[retriever] +type = SimplePOP3SSLRetriever +server = pop3.gmail.com +username = alice +password = notsecure + +[destination] +type = MDA_external +path = /usr/lib/dovecot/deliver +allow_root_commands = true +arguments =("-d","user1@example.com") diff --git a/demo-setups/fetchmail-compose.yaml b/demo-setups/fetchmail-compose.yaml new file mode 100644 index 00000000000..f6e1ddb9e3a --- /dev/null +++ b/demo-setups/fetchmail-compose.yaml @@ -0,0 +1,60 @@ +# Docs: https://docker-mailserver.github.io/docker-mailserver/v15.0/config/advanced/mail-fetchmail +# Additional context, with CLI commands for verification: +# https://github.com/orgs/docker-mailserver/discussions/3994#discussioncomment-9290570 + +services: + dms-fetch: + image: ghcr.io/docker-mailserver/docker-mailserver:latest # :15.0 + hostname: mail.example.test + environment: + ENABLE_FETCHMAIL: 1 + # We change this setting to 10 for quicker testing: + FETCHMAIL_POLL: 10 + # Link the DNS lookup `remote.test` to resolve to the `dms-remote` container IP (for `@remote.test` address): + # This is only for this example, since no real DNS service is configured, this is a Docker internal DNS feature: + links: + - "dms-remote:remote.test" + # NOTE: Optional, You only need to publish ports if you want to verify via your own mail client. + #ports: + # - "465:465" # ESMTP (implicit TLS) + # - "993:993" # IMAP4 (implicit TLS) + # You'd normally use `volumes` here but for simplicity of the example, all config is contained within `compose.yaml`: + configs: + - source: dms-accounts-fetch + target: /tmp/docker-mailserver/postfix-accounts.cf + - source: fetchmail + target: /tmp/docker-mailserver/fetchmail.cf + + dms-remote: + image: ghcr.io/docker-mailserver/docker-mailserver:latest # :15.0 + hostname: mail.remote.test + environment: + # Allows for us send a test mail easily by trusting any mail client run within this container (`swaks`): + PERMIT_DOCKER: container + # Alternatively, trust and accept any mail received from clients in same subnet of dms-fetch: + #PERMIT_DOCKER: connected-networks + configs: + - source: dms-accounts-remote + target: /tmp/docker-mailserver/postfix-accounts.cf + +# Using the Docker Compose `configs.content` feature instead of volume mounting separate files. +# NOTE: This feature requires Docker Compose v2.23.1 (Nov 2023) or newer: +# https://github.com/compose-spec/compose-spec/pull/446 +configs: + fetchmail: + content: | + poll 'mail.remote.test' proto imap + user 'jane.doe@remote.test' + pass 'secret' + is 'john.doe@example.test' + no sslcertck + + # DMS requires an account to complete setup, configure one for each instance: + # NOTE: Both accounts are configured with the same password (SHA512-CRYPT hashed), `secret`. + dms-accounts-fetch: + content: | + john.doe@example.test|{SHA512-CRYPT}$$6$$sbgFRCmQ.KWS5ryb$$EsWrlYosiadgdUOxCBHY0DQ3qFbeudDhNMqHs6jZt.8gmxUwiLVy738knqkHD4zj4amkb296HFqQ3yDq4UXt8. + + dms-accounts-remote: + content: | + jane.doe@remote.test|{SHA512-CRYPT}$$6$$sbgFRCmQ.KWS5ryb$$EsWrlYosiadgdUOxCBHY0DQ3qFbeudDhNMqHs6jZt.8gmxUwiLVy738knqkHD4zj4amkb296HFqQ3yDq4UXt8. diff --git a/demo-setups/relay-compose.yaml b/demo-setups/relay-compose.yaml new file mode 100644 index 00000000000..7346dd87eb0 --- /dev/null +++ b/demo-setups/relay-compose.yaml @@ -0,0 +1,147 @@ +# Docs: https://docker-mailserver.github.io/docker-mailserver/v15.0/config/advanced/mail-forwarding/relay-hosts/ +# Additional context, with CLI commands for verification: +# https://github.com/docker-mailserver/docker-mailserver/issues/4136#issuecomment-2253693490 + +services: + # This would represent your actual DMS container: + dms-sender: + image: mailserver/docker-mailserver:latest # :15.0 + hostname: mail.example.test + environment: + # All outbound mail will be relayed through this host + # (change the port to 587 if you do not want the postfix-main.cf override) + - DEFAULT_RELAY_HOST=[smtp.relay-service.test]:465 + # Your relay host credentials. + # (since the relay in the example is DMS, the relay account username is a full email address) + - RELAY_USER=relay-user@relay-service.test + - RELAY_PASSWORD=secret + # The mail client (swaks) needs to connect with TLS: + - SSL_TYPE=manual + - SSL_KEY_PATH=/tmp/tls/key.pem + - SSL_CERT_PATH=/tmp/tls/cert.pem + # You would usually have `volumes` instead of this `configs`: + configs: + - source: dms-main + target: /tmp/docker-mailserver/postfix-main.cf + - source: dms-accounts + target: /tmp/docker-mailserver/postfix-accounts.cf + # Authenticating on port 587 or 465 enforces TLS requirement: + - source: tls-cert + target: /tmp/tls/cert.pem + - source: tls-key + target: /tmp/tls/key.pem + # This is only needed if you want to verify the TLS cert chain with swaks + # (normally with public CA providers like LetsEncrypt this file is already available to a mail client) + - source: tls-ca-cert + target: /tmp/tls/ca-cert.pem + + # Pretend this is your third-party relay service: + dms-relay: + image: mailserver/docker-mailserver:latest # :15.0 + hostname: smtp.relay-service.test + environment: + # WORKAROUND: Bypass security checks from the mail-client (dms-sender container) + # (avoids needing expected DNS records to run this example) + - PERMIT_DOCKER=connected-networks + # TLS is required when relaying to dms-relay via ports 587 / 465 + # (dms-relay will then relay the mail to dms-destination over port 25) + - SSL_TYPE=manual + - SSL_KEY_PATH=/tmp/tls/key.pem + - SSL_CERT_PATH=/tmp/tls/cert.pem + configs: + - source: dms-accounts-relay + target: /tmp/docker-mailserver/postfix-accounts.cf + - source: tls-cert + target: /tmp/tls/cert.pem + - source: tls-key + target: /tmp/tls/key.pem + + # Pretend this is another mail server that your target recipient belongs to (like Gmail): + dms-destination: + image: mailserver/docker-mailserver:latest # :15.0 + hostname: mail.destination.test + # WORKAROUND: dms-relay must be able to resolve DNS for `@destination.test` to the IP of this container: + # Normally a MX record would direct mail to the MTA (eg: `mail.destination.test`) + networks: + default: + aliases: + - destination.test + environment: + # WORKAROUND: Same workaround as needed for dms-relay + - PERMIT_DOCKER=connected-networks + configs: + - source: dms-accounts-destination + target: /tmp/docker-mailserver/postfix-accounts.cf + +# Using the Docker Compose `configs.content` feature instead of volume mounting separate files. +# NOTE: This feature requires Docker Compose v2.23.1 (Nov 2023) or newer: +# https://github.com/compose-spec/compose-spec/pull/446 +configs: + # `postfix-main.cf`, a single line change to make all outbound SMTP connections over implicit TLS instead of the default explicit TLS (StartTLS). + # NOTE: If you need to only selectively relay mail, you would need to instead adjust this on the relay service in `/etc/postfix/master.cf`, + # However DMS presently modifies this when using the DMS Relay Host feature support, which may override `postfix-master.cf` or `user-patches.sh` due to `check-for-changes.sh`. + dms-main: + content: | + smtp_tls_wrappermode=yes + + # DMS expects an account to be configured to run, this example provides accounts already created. + # Login credentials: + # user: "john.doe@example.test" password: "secret" + # user: "relay-user@relay-service.test" password: "secret" + # user: "jane.doe@destination.test" password: "secret" + dms-accounts: + # NOTE: `$` needed to be repeated to escape it, + # which opts out of the `compose.yaml` variable interpolation feature. + content: | + john.doe@example.test|{SHA512-CRYPT}$$6$$sbgFRCmQ.KWS5ryb$$EsWrlYosiadgdUOxCBHY0DQ3qFbeudDhNMqHs6jZt.8gmxUwiLVy738knqkHD4zj4amkb296HFqQ3yDq4UXt8. + + dms-accounts-relay: + content: | + relay-user@relay-service.test|{SHA512-CRYPT}$$6$$o65y1ZXC4ooOPLwZ$$7TF1nYowEtNJpH6BwJBgdj2pPAxaCvhIKQA6ww5zdHm/AA7aemY9eoHC91DOgYNaKj1HLxSeWNDdvrp6mbtUY. + + dms-accounts-destination: + content: | + jane.doe@destination.test|{SHA512-CRYPT}$$6$$o65y1ZXC4ooOPLwZ$$7TF1nYowEtNJpH6BwJBgdj2pPAxaCvhIKQA6ww5zdHm/AA7aemY9eoHC91DOgYNaKj1HLxSeWNDdvrp6mbtUY. + + # TLS files: + # - Use an ECDSA cert that's been signed by a self-signed CA for TLS cert verification. + # - This cert is only valid for mail.example.test, mail.destination.test, smtp.relay-service.test + + # `swaks` run in the container will need to reference this CA cert file for successful verification (optional). + tls-ca-cert: + content: | + -----BEGIN CERTIFICATE----- + MIIBfTCCASKgAwIBAgIRAMAZttlRlkcuSun0yV0z4RwwCgYIKoZIzj0EAwIwHDEa + MBgGA1UEAxMRU21hbGxzdGVwIFJvb3QgQ0EwHhcNMjEwMTAxMDAwMDAwWhcNMzEw + MTAxMDAwMDAwWjAcMRowGAYDVQQDExFTbWFsbHN0ZXAgUm9vdCBDQTBZMBMGByqG + SM49AgEGCCqGSM49AwEHA0IABJX2hCtoK3+bM5I3rmyApXLJ1gOcVhtoSSwM8XXR + SEl25Kkc0n6mINuMK8UrBkiBUgexf6CYayx3xVr9TmMkg4KjRTBDMA4GA1UdDwEB + /wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/AgEBMB0GA1UdDgQWBBQD8sBrApbyYyqU + y+/TlwGynx2V5jAKBggqhkjOPQQDAgNJADBGAiEAi8N2eOETI+6hY3+G+kzNMd3K + Sd3Ke8b++/nlwr5Fb/sCIQDYAjpKp/MpTDWICeHC2tcB5ptxoTdWkTBuG4rKcktA + 0w== + -----END CERTIFICATE----- + + tls-key: + content: | + -----BEGIN EC PRIVATE KEY----- + MHcCAQEEIOc6wqZmSDmT336K4O26dMk1RCVc0+cmnsO2eK4P5K5yoAoGCCqGSM49 + AwEHoUQDQgAEFOWNgekKKvUZE89vJ7henUYxODYIvCiHitRc2ylwttjqt1KUY1cp + q3jof2fhURHfBUH3dHPXLHig5V9Jw5gqeg== + -----END EC PRIVATE KEY----- + + tls-cert: + content: | + -----BEGIN CERTIFICATE----- + MIIB9DCCAZqgAwIBAgIQE53a/y2c//YXRsz2kLm6gDAKBggqhkjOPQQDAjAcMRow + GAYDVQQDExFTbWFsbHN0ZXAgUm9vdCBDQTAeFw0yMTAxMDEwMDAwMDBaFw0zMTAx + MDEwMDAwMDBaMBkxFzAVBgNVBAMTDlNtYWxsc3RlcCBMZWFmMFkwEwYHKoZIzj0C + AQYIKoZIzj0DAQcDQgAEFOWNgekKKvUZE89vJ7henUYxODYIvCiHitRc2ylwttjq + t1KUY1cpq3jof2fhURHfBUH3dHPXLHig5V9Jw5gqeqOBwDCBvTAOBgNVHQ8BAf8E + BAMCB4AwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBSz + w74g+O6dcBbwienD70D8A9ESmDAfBgNVHSMEGDAWgBQD8sBrApbyYyqUy+/TlwGy + nx2V5jBMBgNVHREERTBDghFtYWlsLmV4YW1wbGUudGVzdIIVbWFpbC5kZXN0aW5h + dGlvbi50ZXN0ghdzbXRwLnJlbGF5LXNlcnZpY2UudGVzdDAKBggqhkjOPQQDAgNI + ADBFAiEAoety5oClZtuBMkvlUIWRmWlyg1VIOZ544LSEbplsIhcCIHb6awMwNdXP + m/xHjFkuwH1+UjDDRW53Ih7KZoLrQ6Cp + -----END CERTIFICATE----- diff --git a/docs/content/assets/css/customizations.css b/docs/content/assets/css/customizations.css index 49ede009896..7255a8377a5 100644 --- a/docs/content/assets/css/customizations.css +++ b/docs/content/assets/css/customizations.css @@ -16,12 +16,16 @@ If you want to append instead, switch `::before` to `::after`. src: url('../fonts/external-link.woff') format('woff'); } -/* Matches the two nav link classes that start with `http` `href` values, regular docs pages use relative URLs instead. */ -.md-tabs__link[href^="http"]::before, .md-nav__link[href^="http"]::before { +/* + Since mkdocs-material 9.5.5 broke support in our docs from DMS v13.3.1, we now use our own class name, + which has been included for the two external nav links in mkdocs.yml via workaround (insert HTML). +*/ +.icon-external-link::before { display: inline-block; /* treat similar to text */ font-family: 'external-link'; content:'\0041'; /* represents "A" which our font renders as an icon instead of the "A" glyph */ font-size: 80%; /* icon is a little too big by default, scale it down */ + margin-right: 4px; } /* ============================================================================================================= */ @@ -98,3 +102,42 @@ div.md-content article.md-content__inner a.toclink code { .highlight.no-copy .md-clipboard { display: none; } /* ============================================================================================================= */ + +/* Make the left-sidebar nav categories better distinguished from page links (bold text) */ +.md-nav__item--nested > .md-nav__link { + font-weight: 700; +} + +/* ============================================================================================================= */ + +/* + TaskList style for a pro/con list. Presently only used for this type of list in the kubernetes docs. + Uses a custom icon for the unchecked (con) state: :octicons-x-circle-fill-24: + https://github.com/squidfunk/mkdocs-material/discussions/6811#discussioncomment-8700795 + + TODO: Can better scope the style under a class name when migrating to block extension syntax: + https://github.com/facelessuser/pymdown-extensions/discussions/1973 +*/ + +:root { + --md-tasklist-icon--failed: url('data:image/svg+xml;charset=utf-8,'); +} + +.md-typeset [type="checkbox"] + .task-list-indicator::before { + background-color: rgb(216, 87, 48); + -webkit-mask-image: var(--md-tasklist-icon--failed); + mask-image: var(--md-tasklist-icon--failed); +} + +/* More suitable shade of green */ +.md-typeset [type=checkbox]:checked+.task-list-indicator:before { + background-color: rgb(97, 216, 42); +} + +/* Tiny layout shift */ +[dir=ltr] .md-typeset .task-list-indicator:before { + left: -1.6em; + top: 1px; +} + +/* ============================================================================================================= */ diff --git a/docs/content/config/account-management/overview.md b/docs/content/config/account-management/overview.md new file mode 100644 index 00000000000..d67eec577d7 --- /dev/null +++ b/docs/content/config/account-management/overview.md @@ -0,0 +1,252 @@ +# Account Management - Overview + +This page provides a technical reference for account management in DMS. + +!!! note "Account provisioners and alternative authentication support" + + Each [`ACCOUNT_PROVISIONER`][docs::env::account-provisioner] has a separate page for configuration guidance and caveats: + + - [`FILE` provisioner docs][docs::account-provisioner::file] + - [`LDAP` provisioner docs][docs::account-provisioner::ldap] + + Authentication from the provisioner can be supplemented with additional methods: + + - [OAuth2 / OIDC][docs::account-auth::oauth2] (_allow login from an external authentication service_) + - [Master Accounts][docs::account-auth::master-accounts] (_access the mailbox of any DMS account_) + + --- + + For custom authentication requirements, you could [implement this with Lua][docs::examples::auth-lua]. + +## Accounts + +!!! info + + To receive or send mail, you'll need to provision user accounts into DMS (_as each provisioner page documents_). + + --- + + A DMS account represents a user with their _login username_ + password, and optional config like aliases and quota. + + - Sending mail from different addresses **does not require** aliases or separate accounts. + - Each account is configured with a _primary email address_ that a mailbox is associated to. + +??? info "Primary email address" + + The email address associated to an account creates a mailbox. This address is relevant: + + - When DMS **receives mail** for that address as the recipient (_or an alias that resolves to it_), to identify which mailbox to deliver into. + - With **mail submission**: + - `SPOOF_PROTECTION=1` **restricts the sender address** to the DMS account email address (_unless additional sender addresses have been permitted via supported config_). + - `SPOOF_PROTECTION=0` allows DMS accounts to **use any sender address** (_only a single DMS account is necessary to send mail with different sender addresses_). + + --- + + For more details, see the [Technical Overview](#technical-overview) section. + +??? note "Support for multiple mail domains" + + No extra configuration in DMS is required after provisioning an account with an email address. + + - The DNS records for a domain should direct mail to DMS and allow DMS to send mail on behalf of that domain. + - DMS does not need TLS certificates for your mail domains, only for the DMS FQDN (_the `hostname` setting_). + +??? warning "Choosing a compatible email address" + + An email address should conform to the standard [permitted charset and format][email-syntax::valid-charset-format] (`local-part@domain-part`). + + --- + + DMS has features that need to reserve special characters to work correctly. Ensure those characters are not present in email addresses you configure for DMS, otherwise disable / opt-out of the feature. + + - [Sub-addressing](#sub-addressing) is enabled by default with `+` as the _tag delimiter_. The tag can be changed, feature opt-out when the tag is explicitly unset. + +### Aliases + +!!! info + + Aliases allow receiving mail: + + - As an alternative delivery address for a DMS account mailbox. + - To redirect / forward to an external address outside of DMS like `@gmail.com`. + +??? abstract "Technical Details (_Local vs Virtual aliases_)" + + Aliases are managed through Postfix which supports _local_ and _virtual_ aliases: + + - **Local aliases** are for mail routed to the [`local` delivery agent][postfix::delivery-agent::local] (see [associated alias config format][postfix::config-table::local-alias]) + - You rarely need to configure this. It is used internally for system unix accounts belonging to the services running in DMS (_including `root`_). + - `postmaster` may be a local alias to `root`, and `root` to a virtual alias or real email address. + - Any mail sent through the `local` delivery agent will not be delivered to an inbox managed by Dovecot (_unless you have configured a local alias to redirect mail to a valid address or alias_). + - The domain-part of an these aliases belongs to your DMS FQDN (_`hostname: mail.example.com`, thus `user@mail.example.com`_). Technically there is no domain-part at this point, that context is used when routing delivery, the local delivery agent only knows of the local-part (_an alias or unix account_). + - [**Virtual aliases**][postfix-docs::virtual-alias] are for mail routed to the [`virtual` delivery agent][postfix::delivery-agent::virtual] (see [associated alias config format][postfix::config-table::virtual-alias]) + - When alias support in DMS is discussed without the context of being a local or virtual alias, it's likely the virtual kind (_but could also be agnostic_). + - The domain-part of an these aliases belongs to a mail domain managed by DMS (_like `user@example.com`_). + + !!! tip "Verify alias resolves correctly" + + You can run `postmap -q ` in the container to verify an alias resolves to the expected target. If the target is also an alias, the command will not expand that alias to resolve the actual recipient(s). + + For the `FILE` provisioner, an example would be: `postmap -q alias1@example.com /etc/postfix/virtual`. For the `LDAP` provisioner you'd need to adjust the table path. + + !!! info "Side effect - Dovecot Quotas (`ENABLE_QUOTAS=1`)" + + As a side effect of the alias workaround for the `FILE` provisioner with this feature, aliases can be used for account login. This is not intentional. + +### Quotas + +!!! info + + Enables mail clients with the capability to query a mailbox for disk-space used and capacity limit. + + - This feature is enabled by default, opt-out via [`ENABLE_QUOTAS=0`][docs::env::enable-quotas] + - **Not implemented** for the LDAP provisioner (_PR welcome! View the [feature request for implementation advice][gh-issue::dms-feature-request::dovecot-quotas-ldap]_) + +??? tip "How are quotas useful?" + + Without quota limits for disk storage, a mailbox could fill up the available storage which would cause delivery failures to all mailboxes. + + Quotas help by preventing that abuse, so that only a mailbox exceeding the assigned quota experiences a delivery failure instead of negatively impacting others (_provided disk space is available_). + +??? abstract "Technical Details" + + The [Dovecot Quotas feature][gh-pr::dms-feature::dovecot-quotas] is configured by enabling the [Dovecot `imap-quota` plugin][dovecot-docs::plugin::imap-quota] and using the [`count` quota backend][dovecot-docs::config::quota-backend-count]. + + --- + + **Dovecot workaround for Postfix aliases** + + When mail is delivered to DMS, Postfix will query Dovecot with the recipient(s) to verify quota has not been exceeded. + + This allows early rejection of mail arriving to DMS, preventing a spammer from taking advantage of a [backscatter][wikipedia::backscatter] source if the mail was accepted by Postfix, only to later be rejected by Dovecot for storage when the quota limit was already reached. + + However, Postfix does not resolve aliases until after the incoming mail is accepted. + + 1. Postfix queries Dovecot (_a [`check_policy_service` restriction tied to the Dovecot `quota-status` service][dms::workaround::dovecot-quotas::notes-1]_) with the recipient (_the alias_). + 2. `dovecot: auth: passwd-file(alias@example.com): unknown user` is logged, Postfix is then informed that the recipient mailbox is not full even if it actually was (_since no such user exists in the Dovecot UserDB_). + 3. However, when the real mailbox address that the alias would later resolve into does have a quota that exceeded the configured limit, Dovecot will refuse the mail delivery from Postfix which introduces a backscatter source for spammers. + + As a [workaround to this problem with the `ENABLE_QUOTAS=1` feature][dms::workaround::dovecot-quotas::summary], DMS will add aliases as fake users into Dovecot UserDB (_that are configured with the same data as the real address the alias would resolve to, thus sharing the same mailbox location and quota limit_). This allows Postfix to properly be aware of an aliased mailbox having exceeded the allowed quota. + + **NOTE:** This workaround **only supports** aliases to a single target recipient of a real account address / mailbox. + + - Additionally, aliases that resolve to another alias or to an external address would both fail the UserDB lookup, unable to determine if enough storage is available. + - A proper fix would [implement a Postfix policy service][dms::workaround::dovecot-quotas::notes-2] that could correctly resolve aliases to valid entries in the Dovecot UserDB, querying the `quota-status` service and returning that response to Postfix. + +## Sub-addressing + +!!! info + + [Subaddressing][wikipedia::subaddressing] (_aka **Plus Addressing** or **Address Tags**_) is a feature that allows you to receive mail to an address which includes a tag appended to the `local-part` of a valid account address. + + - A subaddress has a tag delimiter (_default: `+`_), followed by the tag: `+@` + - The subaddress `user+github@example.com` would deliver mail to the same mailbox as `user@example.com`. + - Tags are dynamic. Anything between the `+` and `@` is understood as the tag, no additional configuration required. + - Only the first occurrence of the tag delimiter is recognized. Any additional occurrences become part of the tag value itself. + +??? tip "When is subaddressing useful?" + + A common use-case is to use a unique tag for each service you register your email address with. + + - Routing delivery to different folders in your mailbox based on the tag (_via a [Sieve filter][docs::sieve::subaddressing]_). + - Data leaks or bulk sales of email addresses. + - If spam / phishing mail you receive has not removed the tag, you will have better insight into where your address was compromised from. + - When the expected tag is missing, this additionally helps identify bad actors. Especially when mail delivery is routed to subfolders by tag. + - For more use-cases, view the end of [this article][web::subaddress-use-cases]. + +??? tip "Changing the tag delimiter" + + Add `recipient_delimiter = +` to these config override files (_replacing `+` with your preferred delimiter_): + + - Postfix: `docker-data/dms/config/postfix-main.cf` + - Dovecot: `docker-data/dms/config/dovecot.cf` + +??? tip "Opt-out of subaddressing" + + Follow the advice to change the tag delimiter, but instead set an empty value (`recipient_delimiter =`). + +??? warning "Only for receiving, not sending" + + Do not attempt to send mail from these tagged addresses, they are not equivalent to aliases. + + This feature is only intended to be used when a mail client sends to a DMS managed recipient address. While DMS does not restrict the sender address you choose to send mail from (_provided `SPOOF_PROTECTION` has not been enabled_), it is often [forbidden by mail services][ms-exchange-docs::limitations]. + +??? abstract "Technical Details" + + The configured tag delimiter (`+`) allows both Postfix and Dovecot to recognize subaddresses. Without this feature configured, the subaddresses would be considered as separate mail accounts rather than routed to a common account address. + + --- + + Internally DMS has the tag delimiter configured by: + + - Applying the Postfix `main.cf` setting: [`recipient_delimiter = +`][postfix-docs::recipient-delimiter] + - Dovecot has the equivalent setting set as `+` by default: [`recipient_delimiter = +`][dovecot-docs::config::recipient-delimiter] + +## Technical Overview + +!!! info + + This section provides insight for understanding how Postfix and Dovecot services are involved. It is intended as a reference for maintainers and contributors. + + - **Postfix** - Handles when mail is delivered (inbound) to DMS, or sent (outbound) from DMS. + - **Dovecot** - Manages access and storage for mail delivered to the DMS account mailboxes of your users. + +??? abstract "Technical Details - Postfix (Inbound vs Outbound)" + + Postfix needs to know how to handle inbound and outbound mail by asking these queries: + + === "Inbound" + + - What mail domains is DMS responsible for handling? (_for accepting mail delivered_) + - What are valid mail addresses for those mail domains? (_reject delivery for users that don't exist_) + - Are there any aliases to redirect mail to 1 or more users, or forward to externally? + + === "Outbound" + + - When `SPOOF_PROTECTION=1`, how should DMS restrict the sender address? (_eg: Users may only send mail from their associated mailbox address_) + +??? abstract "Technical Details - Dovecot (Authentication)" + + Dovecot additionally handles authenticating user accounts for sending and retrieving mail: + + - Over the ports for IMAP and POP3 connections (_110, 143, 993, 995_). + - As the default configured SASL provider, which Postfix delegates user authentication through (_for the submission(s) ports 465 & 587_). Saslauthd can be configured as an alternative SASL provider. + + Dovecot splits all authentication lookups into two categories: + + - A [PassDB][dovecot::docs::passdb] lookup most importantly authenticates the user. It may also provide any other necessary pre-login information. + - A [UserDB][dovecot::docs::userdb] lookup retrieves post-login information specific to a user. + +[docs::env::account-provisioner]: ../environment.md#account_provisioner +[docs::account-provisioner::file]: ./provisioner/file.md +[docs::account-provisioner::ldap]: ./provisioner/ldap.md +[docs::account-auth::oauth2]: ./supplementary/oauth2.md +[docs::account-auth::master-accounts]: ./supplementary/master-accounts.md +[docs::examples::auth-lua]: ../../examples/use-cases/auth-lua.md +[email-syntax::valid-charset-format]: https://stackoverflow.com/questions/2049502/what-characters-are-allowed-in-an-email-address/2049510#2049510 + +[postfix-docs::virtual-alias]: http://www.postfix.org/VIRTUAL_README.html#virtual_alias +[postfix-docs::recipient-delimiter]: http://www.postfix.org/postconf.5.html#recipient_delimiter +[dovecot-docs::config::recipient-delimiter]: https://doc.dovecot.org/settings/core/#core_setting-recipient_delimiter +[postfix::delivery-agent::local]: https://www.postfix.org/local.8.html +[postfix::delivery-agent::virtual]: https://www.postfix.org/virtual.8.html +[postfix::config-table::local-alias]: https://www.postfix.org/aliases.5.html +[postfix::config-table::virtual-alias]: https://www.postfix.org/virtual.5.html + +[docs::env::enable-quotas]: ../environment.md#enable_quotas +[gh-issue::dms-feature-request::dovecot-quotas-ldap]: https://github.com/docker-mailserver/docker-mailserver/issues/2957 +[dovecot-docs::config::quota-backend-count]: https://doc.dovecot.org/configuration_manual/quota/quota_count/#quota-backend-count +[dovecot-docs::plugin::imap-quota]: https://doc.dovecot.org/settings/plugin/imap-quota-plugin/ +[gh-pr::dms-feature::dovecot-quotas]: https://github.com/docker-mailserver/docker-mailserver/pull/1469 +[wikipedia::backscatter]: https://en.wikipedia.org/wiki/Backscatter_%28email%29 +[dms::workaround::dovecot-quotas::notes-1]: https://github.com/docker-mailserver/docker-mailserver/issues/2091#issuecomment-954298788 +[dms::workaround::dovecot-quotas::notes-2]: https://github.com/docker-mailserver/docker-mailserver/pull/2248#issuecomment-953754532 +[dms::workaround::dovecot-quotas::summary]: https://github.com/docker-mailserver/docker-mailserver/pull/2248#issuecomment-955088677 + +[docs::sieve::subaddressing]: ../advanced/mail-sieve.md#subaddress-mailbox-routing +[web::subaddress-use-cases]: https://www.codetwo.com/admins-blog/plus-addressing +[wikipedia::subaddressing]: https://en.wikipedia.org/wiki/Email_address#Sub-addressing +[ms-exchange-docs::limitations]: https://learn.microsoft.com/en-us/exchange/recipients-in-exchange-online/plus-addressing-in-exchange-online#using-plus-addresses + +[dovecot::docs::passdb]: https://doc.dovecot.org/configuration_manual/authentication/password_databases_passdb +[dovecot::docs::userdb]: https://doc.dovecot.org/configuration_manual/authentication/user_databases_userdb diff --git a/docs/content/config/account-management/provisioner/file.md b/docs/content/config/account-management/provisioner/file.md new file mode 100644 index 00000000000..5b74ffcc0a0 --- /dev/null +++ b/docs/content/config/account-management/provisioner/file.md @@ -0,0 +1,206 @@ +--- +title: 'Account Management | Provisioner (File)' +--- + +# Provisioner - File + +## Management via the `setup` CLI + +The best way to manage DMS accounts and related config files is through our `setup` CLI provided within the container. + +!!! example "Using the `setup` CLI" + + Try the following within the DMS container (`docker exec -it bash`): + + - Add an account: `setup email add ` + - Add an alias: `setup alias add ` + - Learn more about the available subcommands via: `setup help` + + ```bash + # Starts a basic DMS instance and then shells into the container to use the `setup` CLI: + docker run --rm -itd --name dms --hostname mail.example.com mailserver/docker-mailserver + docker exec -it dms bash + + # Create an account: + setup email add hello@example.com your-password-here + + # Create an alias: + setup alias add your-alias-here@example.com hello@example.com + + # Limit the mailbox capacity to 10 MiB: + setup quota set hello@example.com 10M + ``` + + ??? tip "Secure password input" + + When you don't provide a password to the command, you will be prompted for one. This avoids the password being captured in your shell history. + + ```bash + # As you input your password it will not update. + # Press the ENTER key to apply the hidden password input. + $ setup email add hello@example.com + Enter Password: + Confirm Password: + ``` + +!!! note "Account removal via `setup email del`" + + When you remove a DMS account with this command, it will also remove any associated aliases and quota. + + The command will also prompt for deleting the account mailbox from disk, or can be forced with the `-y` flag. + +## Config Reference + +These config files belong to the [Config Volume][docs::volumes::config]. + +### Accounts + +!!! info + + **Config file:** `docker-data/dms/config/postfix-accounts.cf` + + --- + + The config format is line-based with two fields separated by the delimiter `|`: + + - **User:** The primary email address for the account mailbox to use. + - **Password:** A SHA512-CRYPT hash of the account password (_in this example it is `secret`_). + + ??? tip "Password hash without the `setup email add` command" + + A compatible password hash can be generated with: + + ```bash + doveadm pw -s SHA512-CRYPT -u hello@example.com -p secret + ``` + +!!! example "`postfix-accounts.cf` config file" + + In this example DMS manages mail for the domain `example.com`: + + ```cf title="postfix-accounts.cf" + hello@example.com|{SHA512-CRYPT}$6$W4rxRQwI6HNMt9n3$riCi5/OqUxnU8eZsOlZwoCnrNgu1gBGPkJc.ER.LhJCu7sOg9i1kBrRIistlBIp938GdBgMlYuoXYUU5A4Qiv0 + ``` + + --- + + **Dovecot "extra fields"** + + [Appending a third column will customize "extra fields"][gh-issue::provisioner-file::accounts-extra-fields] when converting account data into a Dovecot UserDB entry. + + DMS is not aware of these customizations beyond carrying them over, expect potential for bugs when this feature breaks any assumed conventions used in the scripts (_such as changing the mailbox path or type_). + +!!! note + + Account creation will normalize the provided email address to lowercase, as DMS does not support multiple case-sensitive address variants. + + The email address chosen will also represent the _login username_ credential for mail clients to authenticate with. + +### Aliases + +!!! info + + **Config file:** `docker-data/dms/config/postfix-virtual.cf` + + --- + + The config format is line-based with key value pairs (**alias** --> **target address**), with white-space as a delimiter. + +!!! example "`postfix-virtual.cf` config file" + + In this example DMS manages mail for the domain `example.com`: + + ```cf-extra title="postfix-virtual.cf" + # Alias delivers to an existing account: + alias1@example.com hello@example.com + + # Alias forwards to an external email address: + alias2@example.com external-account@gmail.com + ``` + +??? warning "Known Issues" + + **`setup` CLI prevents an alias and account sharing an address:** + + You cannot presently add a new account (`setup email add`) or alias (`setup alias add`) with an address which already exists as an alias or account in DMS. + + This [restriction was enforced][gh-issue::bugs::account-alias-overlap] due to [problems it could cause][gh-issue::bugs::account-alias-overlap-problem], although there are [use-cases where you may legitimately require this functionality][gh-issue::feature-request::allow-account-alias-overlap]. + + For now you must manually edit the `postfix-virtual.cf` file as a workaround. There are no run-time checks outside of the `setup` CLI related to this restriction. + + --- + + **Wildcard catch-all support (`@example.com`):** + + While this type of alias without a local-part is supported, you must keep in mind that aliases in Postfix have a higher precedence than a real address associated to a DMS account. + + As a result, the wildcard is matched first and will direct mail for that entire domain to the alias target address. To work around this, [you will need an alias for each non-alias address of that domain][gh-issue::bugs::wildcard-catchall]. + + Additionally, Postfix will read the alias config and choose the alias value that matches the recipient address first. Ensure your more specific aliases for the domain are declared above the wildcard alias in the config file. + + --- + + **Aliasing to another alias or multiple recipients:** + + [While aliasing to multiple recipients is possible][gh-discussions::no-support::alias-multiple-targets], DMS does not officially support that. + + - You may experience issues when our feature integrations don't expect more than one target per alias. + - These concerns also apply to the usage of nested aliases (_where the recipient target provided is to an alias instead of a real address_). An example is the [incompatibility with `setup alias add`][gh-issue::bugs::alias-nested]. + +#### Configuring RegEx aliases + +!!! info + + **Config file:** `docker-data/dms/config/postfix-regexp.cf` + + --- + + This config file is similar to the above `postfix-virtual.cf`, but the alias value is instead configured with a regex pattern. + + There is **no `setup` CLI support** for this feature, it is config only. + +!!! example "`postfix-regexp.cf` config file" + + Deliver all mail for `test` users to `qa@example.com` instead: + + ```cf-extra title="postfix-regexp.cf" + # Remember to escape regex tokens like `.` => `\.`, otherwise + # your alias pattern may be more permissive than you intended: + /^test[0-9][0-9]*@example\.com/ qa@example.com + ``` + +??? abstract "Technical Details" + + `postfix-virtual.cf` has precedence, `postfix-regexp.cf` will only be checked if no alias match was found in `postfix-virtual.cf`. + + These files are both copied internally to `/etc/postfix/` and configured in `main.cf` for the `virtual_alias_maps` setting. As `postfix-virtual.cf` is declared first for that setting, it will be processed before using `postfix-regexp.cf` as a fallback. + +### Quotas + +!!! info + + **Config file:** `docker-data/dms/config/dovecot-quotas.cf` + + ---- + + The config format is line-based with two fields separated by the delimiter `:`: + + - **Dovecot UserDB account:** The user DMS account. It should have a matching field in `postfix-accounts.cf`. + - **Quota limit:** Expressed in bytes (_binary unit suffix is supported: `M` => `MiB`, `G` => `GiB`_). + +!!! example "`dovecot-quotas.cf` config file" + + For the account with the mailbox address of `hello@example.com`, it may not exceed 5 GiB in storage: + + ```cf-extra title="dovecot-quotas.cf" + hello@example.com:5G + ``` + +[docs::volumes::config]: ../../advanced/optional-config.md#volumes-config +[gh-issue::provisioner-file::accounts-extra-fields]: https://github.com/docker-mailserver/docker-mailserver/issues/4117 +[gh-issue::feature-request::allow-account-alias-overlap]: https://github.com/docker-mailserver/docker-mailserver/issues/3528 +[gh-issue::bugs::account-alias-overlap-problem]: https://github.com/docker-mailserver/docker-mailserver/issues/3350#issuecomment-1550528898 +[gh-issue::bugs::account-alias-overlap]: https://github.com/docker-mailserver/docker-mailserver/issues/3022#issuecomment-1807816689 +[gh-issue::bugs::wildcard-catchall]: https://github.com/docker-mailserver/docker-mailserver/issues/3022#issuecomment-1610452561 +[gh-issue::bugs::alias-nested]: https://github.com/docker-mailserver/docker-mailserver/issues/3622#issuecomment-1794504849 +[gh-discussions::no-support::alias-multiple-targets]: https://github.com/orgs/docker-mailserver/discussions/3805#discussioncomment-8215417 diff --git a/docs/content/config/advanced/auth-ldap.md b/docs/content/config/account-management/provisioner/ldap.md similarity index 95% rename from docs/content/config/advanced/auth-ldap.md rename to docs/content/config/account-management/provisioner/ldap.md index 822145cfbfa..607ee3d5f63 100644 --- a/docs/content/config/advanced/auth-ldap.md +++ b/docs/content/config/account-management/provisioner/ldap.md @@ -1,10 +1,10 @@ --- -title: 'Advanced | LDAP Authentication' +title: 'Account Management | Provisioner (LDAP)' --- ## Introduction -Getting started with ldap and `docker-mailserver` we need to take 3 parts in account: +Getting started with ldap and DMS we need to take 3 parts in account: - `postfix` for incoming & outgoing email - `dovecot` for accessing mailboxes @@ -26,7 +26,7 @@ Those variables contain the LDAP lookup filters for postfix, using `%s` as the p - Technically, there is no difference between `ALIAS` and `GROUP`, but ideally you should use `ALIAS` for personal aliases for a singular person (like `ceo@example.org`) and `GROUP` for multiple people (like `hr@example.org`). - ...for outgoing email, the sender address is put through the `SENDERS` filter, and only if the authenticated user is one of the returned entries, the email can be sent. - This only applies if `SPOOF_PROTECTION=1`. - - If the `SENDERS` filter is missing, the `USER`, `ALIAS` and `GROUP` filters will be used in in a disjunction (OR). + - If the `SENDERS` filter is missing, the `USER`, `ALIAS` and `GROUP` filters will be used in a disjunction (OR). - To for example allow users from the `admin` group to spoof any sender email address, and to force everyone else to only use their personal mailbox address for outgoing email, you can use something like this: `(|(memberOf=cn=admin,*)(mail=%s))` ???+ example @@ -34,7 +34,6 @@ Those variables contain the LDAP lookup filters for postfix, using `%s` as the p A really simple `LDAP_QUERY_FILTER` configuration, using only the _user filter_ and allowing only `admin@*` to spoof any sender addresses. ```yaml - - ENABLE_LDAP=1 # with the :edge tag, use ACCOUNT_PROVISIONER - LDAP_START_TLS=yes - ACCOUNT_PROVISIONER=LDAP - LDAP_SERVER_HOST=ldap.example.org @@ -57,7 +56,7 @@ These variables specify the LDAP filters that dovecot uses to determine if a use This is split into the following two lookups, both using `%u` as the placeholder for the full login name ([see dovecot documentation for a full list of placeholders](https://doc.dovecot.org/configuration_manual/config_file/config_variables/)). Usually you only need to set `DOVECOT_USER_FILTER`, in which case it will be used for both filters. - `DOVECOT_USER_FILTER` is used to get the account details (uid, gid, home directory, quota, ...) of a user. -- `DOVECOT_PASS_FILTER` is used to get the password information of the user, and is in pretty much all cases identical to `DOVECOT_USER_FILTER` (which is the default behaviour if left away). +- `DOVECOT_PASS_FILTER` is used to get the password information of the user, and is in pretty much all cases identical to `DOVECOT_USER_FILTER` (which is the default behavior if left away). If your directory doesn't have the [postfix-book schema](https://github.com/variablenix/ldap-mail-schema/blob/master/postfix-book.schema) installed, then you must change the internal attribute handling for dovecot. For this you have to change the `pass_attr` and the `user_attr` mapping, as shown in the example below: @@ -191,7 +190,7 @@ The changes on the configurations necessary to work with Active Directory (**onl ```yaml services: mailserver: - image: docker.io/mailserver/docker-mailserver:latest + image: ghcr.io/docker-mailserver/docker-mailserver:latest container_name: mailserver hostname: mail.example.com @@ -215,7 +214,6 @@ The changes on the configurations necessary to work with Active Directory (**onl - ENABLE_POSTGREY=1 # >>> Postfix LDAP Integration - - ENABLE_LDAP=1 # with the :edge tag, use ACCOUNT_PROVISIONER - ACCOUNT_PROVISIONER=LDAP - LDAP_SERVER_HOST=ldap.example.org - LDAP_BIND_DN=cn=admin,ou=users,dc=example,dc=org @@ -253,7 +251,7 @@ The changes on the configurations necessary to work with Active Directory (**onl ```yaml services: mailserver: - image: docker.io/mailserver/docker-mailserver:latest + image: ghcr.io/docker-mailserver/docker-mailserver:latest container_name: mailserver hostname: mail.example.com @@ -284,7 +282,6 @@ The changes on the configurations necessary to work with Active Directory (**onl # <<< SASL Authentication # >>> Postfix Ldap Integration - - ENABLE_LDAP=1 # with the :edge tag, use ACCOUNT_PROVISIONER - ACCOUNT_PROVISIONER=LDAP - LDAP_SERVER_HOST= - LDAP_SEARCH_BASE=dc=mydomain,dc=loc @@ -307,5 +304,5 @@ The changes on the configurations necessary to work with Active Directory (**onl - NET_ADMIN ``` -[docs-environment]: ../environment.md -[docs-userpatches]: ./override-defaults/user-patches.md +[docs-environment]: ../../environment.md +[docs-userpatches]: ../../advanced/override-defaults/user-patches.md diff --git a/docs/content/config/account-management/supplementary/master-accounts.md b/docs/content/config/account-management/supplementary/master-accounts.md new file mode 100644 index 00000000000..a8665a83eef --- /dev/null +++ b/docs/content/config/account-management/supplementary/master-accounts.md @@ -0,0 +1,70 @@ +--- +title: 'Account Management | Master Accounts (Dovecot)' +hide: + - toc # Hide Table of Contents for this page +--- + +This feature is useful for administrative tasks like hot backups. + +!!! note + + This feature is presently [not supported with `ACCOUNT_PROVISIONER=LDAP`][dms::feature::dovecot-master-accounts::caveat-ldap]. + +!!! info + + A _Master Account_: + + - Can login as any user (DMS account) and access their mailbox. + - Is not associated to a separate DMS account, nor is it a DMS account itself. + + --- + + **`setup` CLI support** + + Use the `setup dovecot-master ` commands. These are roughly equivalent to the `setup email` subcommands. + + --- + + **Config file:** `docker-data/dms/config/dovecot-masters.cf` + + The config format is the same as [`postfix-accounts.cf` for `ACCOUNT_PROVISIONER=FILE`][docs::account-management::file::accounts]. + + The only difference is the account field has no `@domain-part` suffix, it is only a username. + +??? abstract "Technical Details" + + [The _Master Accounts_ feature][dms::feature::dovecot-master-accounts] in DMS configures the [Dovecot Master Users][dovecot-docs::auth::master-users] feature with the Dovecot setting [`auth_master_user_separator`][dovecot-docs::config::auth-master-user-separator] (_where the default value is `*`_). + +## Login via Master Account + +!!! info + + To login as another DMS account (`user@example.com`) with POP3 or IMAP, use the following credentials format: + + - Username: `*` (`user@example.com*admin`) + - Password: `` + +!!! example "Verify login functionality" + + In the DMS container, you can verify with the `testsaslauthd` command: + + ```bash + # Prerequisites: + # A regular DMS account to test login through a Master Account: + setup email add user@example.com secret + # Add a new Master Account: + setup dovecot-master add admin top-secret + ``` + + ```bash + # Login with credentials format as described earlier: + testsaslauthd -u 'user@example.com*admin' -p 'top-secret' + ``` + + Alternatively, any mail client should be able to login the equivalent credentials. + +[dms::feature::dovecot-master-accounts]: https://github.com/docker-mailserver/docker-mailserver/pull/2535 +[dms::feature::dovecot-master-accounts::caveat-ldap]: https://github.com/docker-mailserver/docker-mailserver/pull/2535#issuecomment-1118056745 +[dovecot-docs::auth::master-users]: https://doc.dovecot.org/configuration_manual/authentication/master_users/ +[dovecot-docs::config::auth-master-user-separator]: https://doc.dovecot.org/settings/core/#core_setting-auth_master_user_separator +[docs::account-management::file::accounts]: ../provisioner/file.md#accounts diff --git a/docs/content/config/account-management/supplementary/oauth2.md b/docs/content/config/account-management/supplementary/oauth2.md new file mode 100644 index 00000000000..fb74aeec44f --- /dev/null +++ b/docs/content/config/account-management/supplementary/oauth2.md @@ -0,0 +1,145 @@ +--- +title: 'Account Management | OAuth2 Support' +hide: + - toc # Hide Table of Contents for this page +--- + +# Authentication - OAuth2 / OIDC + +This feature enables support for delegating DMS account authentication through to an external _Identity Provider_ (IdP). + +!!! warning "Receiving mail requires a DMS account to exist" + + If you expect DMS to receive mail, you must provision an account into DMS in advance. Otherwise DMS has no awareness of your externally manmaged users and will reject delivery. + + There are [plans to implement support to provision users through a SCIM 2.0 API][dms-feature-request::scim-api]. An IdP that can operate as a SCIM Client (eg: Authentik) would then integrate with DMS for user provisioning. Until then you must keep your user accounts in sync manually via your configured [`ACCOUNT_PROVISIONER`][docs::env::account-provisioner]. + +??? info "How the feature works" + + 1. A **mail client must have support** to acquire an OAuth2 token from your IdP (_however many clients lack generic OAuth2 / OIDC provider support_). + 2. The mail client then provides that token as the user password via the login mechanism `XOAUTH2` or `OAUTHBEARER`. + 3. DMS (Dovecot) will then check the validity of that token against the Authentication Service it was configured with. + 4. If the response returned is valid for the user account, authentication is successful. + + [**XOAUTH2**][google::xoauth2-docs] (_Googles widely adopted implementation_) and **OAUTHBEARER** (_the newer variant standardized by [RFC 7628][rfc::7628] in 2015_) are supported as standards for verifying that a OAuth Bearer Token (_[RFC 6750][rfc::6750] from 2012_) is valid at the identity provider that created the token. The token itself in both cases is expected to be can an opaque _Access Token_, but it is possible to use a JWT _ID Token_ (_which encodes additional information into the token itself_). + + A mail client like Thunderbird has limited OAuth2 / OIDC support. The software maintains a hard-coded list of providers supported. Roundcube is a webmail client that does have support for generic providers, allowing you to integrate with a broader range of IdP services. + + --- + + **Documentation for this feature is WIP** + + See the [initial feature support][dms-feature::oauth2-pr] and [existing issues][dms-feature::oidc-issues] for guidance that has not yet been documented officially. + +??? tip "Verify authentication works" + + If you have a compatible mail client you can verify login through that. + + --- + + ??? example "CLI - Verify with `curl`" + + ```bash + # Shell into your DMS container: + docker exec -it dms bash + + # Adjust these variables for the methods below to use: + export AUTH_METHOD='OAUTHBEARER' USER_ACCOUNT='hello@example.com' ACCESS_TOKEN='DMS_YWNjZXNzX3Rva2Vu' + + # Authenticate via IMAP (Dovecot): + curl --silent --url 'imap://localhost:143' \ + --login-options "AUTH=${AUTH_METHOD}" --user "${USER_ACCOUNT}" --oauth2-bearer "${ACCESS_TOKEN}" \ + --request 'LOGOUT' \ + && grep "dovecot: imap-login: Login: user=<${USER_ACCOUNT}>, method=${AUTH_METHOD}" /var/log/mail/mail.log + + # Authenticate via SMTP (Postfix), sending a mail with the same sender(from) and recipient(to) address: + # NOTE: `curl` seems to require `--upload-file` with some mail content provided to test SMTP auth. + curl --silent --url 'smtp://localhost:587' \ + --login-options "AUTH=${AUTH_METHOD}" --user "${USER_ACCOUNT}" --oauth2-bearer "${ACCESS_TOKEN}" \ + --mail-from "${USER_ACCOUNT}" --mail-rcpt "${USER_ACCOUNT}" --upload-file - <<< 'RFC 5322 content - not important' \ + && grep "postfix/submission/smtpd.*, sasl_method=${AUTH_METHOD}, sasl_username=${USER_ACCOUNT}" /var/log/mail/mail.log + ``` + + --- + + **Troubleshooting:** + + - Add `--verbose` to the curl options. This will output the protocol exchange which includes if authentication was successful or failed. + - The above example chains the `curl` commands with `grep` on DMS logs (_for Dovecot and Postfix services_). When not running `curl` from the DMS container, ensure you check the logs correctly, or inspect the `--verbose` output instead. + + !!! warning "`curl` bug with `XOAUTH2`" + + [Older releases of `curl` have a bug with `XOAUTH2` support][gh-issue::curl::xoauth2-bug] since `7.80.0` (Nov 2021) but fixed from `8.6.0` (Jan 2024). It treats `XOAUTH2` as `OAUTHBEARER`. + + If you use `docker exec` to run `curl` from within DMS, the current DMS v14 release (_Debian 12 with curl `7.88.1`_) is affected by this bug. + +## Config Examples + +### Authentik with Roundcube + +This example assumes you have already set up: + +- A working DMS server +- An Authentik server ([documentation][authentik::docs::install]) +- A Roundcube server ([docker image][roundcube::dockerhub-image] or [bare metal install][roundcube::docs::install]) + +!!! example "Setup Instructions" + + === "1. Docker Mailserver" + + Update your Docker Compose ENV config to include: + + ```env title="compose.yaml" + services: + mailserver: + env: + # Enable the feature: + - ENABLE_OAUTH2=1 + # Specify the user info endpoint URL of the oauth2 server for token inspection: + - OAUTH2_INTROSPECTION_URL=https://authentik.example.com/application/o/userinfo/ + ``` + + === "2. Authentik" + + 1. Create a new OAuth2 provider. + 2. Note the client id and client secret. Roundcube will need this. + 3. Set the allowed redirect url to the equivalent of `https://roundcube.example.com/index.php/login/oauth` for your RoundCube instance. + + === "3. Roundcube" + + Add the following to `oauth2.inc.php` ([documentation][roundcube::docs::config]): + + ```php + $config['oauth_provider'] = 'generic'; + $config['oauth_provider_name'] = 'Authentik'; + $config['oauth_client_id'] = ''; + $config['oauth_client_secret'] = ''; + $config['oauth_auth_uri'] = 'https://authentik.example.com/application/o/authorize/'; + $config['oauth_token_uri'] = 'https://authentik.example.com/application/o/token/'; + $config['oauth_identity_uri'] = 'https://authentik.example.com/application/o/userinfo/'; + + // Optional: disable SSL certificate check on HTTP requests to OAuth server. For possible values, see: + // http://docs.guzzlephp.org/en/stable/request-options.html#verify + $config['oauth_verify_peer'] = false; + + $config['oauth_scope'] = 'email openid profile'; + $config['oauth_identity_fields'] = ['email']; + + // Boolean: automatically redirect to OAuth login when opening Roundcube without a valid session + $config['oauth_login_redirect'] = false; + ``` + +[dms-feature::oauth2-pr]: https://github.com/docker-mailserver/docker-mailserver/pull/3480 +[dms-feature::oidc-issues]: https://github.com/docker-mailserver/docker-mailserver/issues?q=label%3Afeature%2Fauth-oidc +[docs::env::account-provisioner]: ../../environment.md#account_provisioner +[dms-feature-request::scim-api]: https://github.com/docker-mailserver/docker-mailserver/issues/4090 + +[google::xoauth2-docs]: https://developers.google.com/gmail/imap/xoauth2-protocol#the_sasl_xoauth2_mechanism +[rfc::6750]: https://datatracker.ietf.org/doc/html/rfc6750 +[rfc::7628]: https://datatracker.ietf.org/doc/html/rfc7628 +[gh-issue::curl::xoauth2-bug]: https://github.com/curl/curl/issues/10259#issuecomment-1907192556 + +[authentik::docs::install]: https://goauthentik.io/docs/installation/ +[roundcube::dockerhub-image]: https://hub.docker.com/r/roundcube/roundcubemail +[roundcube::docs::install]: https://github.com/roundcube/roundcubemail/wiki/Installation +[roundcube::docs::config]: https://github.com/roundcube/roundcubemail/wiki/Configuration diff --git a/docs/content/config/advanced/dovecot-master-accounts.md b/docs/content/config/advanced/dovecot-master-accounts.md deleted file mode 100755 index f5788cc4693..00000000000 --- a/docs/content/config/advanced/dovecot-master-accounts.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -title: 'Advanced | Dovecot master accounts' ---- - -## Introduction - -A dovecot master account is able to login as any configured user. This is useful for administrative tasks like hot backups. - -## Configuration - -It is possible to create, update, delete and list dovecot master accounts using `setup.sh`. See `setup.sh help` for usage. - -This feature is presently [not supported with LDAP](https://github.com/docker-mailserver/docker-mailserver/pull/2535). - -## Logging in - -Once a master account is configured, it is possible to connect to any users mailbox using this account. Log in over POP3/IMAP using the following credential scheme: - -Username: `*` - -Password: `` \ No newline at end of file diff --git a/docs/content/config/advanced/full-text-search.md b/docs/content/config/advanced/full-text-search.md index efd21d2306b..2fa6eb6445d 100644 --- a/docs/content/config/advanced/full-text-search.md +++ b/docs/content/config/advanced/full-text-search.md @@ -6,7 +6,7 @@ title: 'Advanced | Full-Text Search' Full-text search allows all messages to be indexed, so that mail clients can quickly and efficiently search messages by their full text content. Dovecot supports a variety of community supported [FTS indexing backends](https://doc.dovecot.org/configuration_manual/fts/). -`docker-mailserver` comes pre-installed with two plugins that can be enabled with a dovecot config file. +DMS comes pre-installed with two plugins that can be enabled with a dovecot config file. Please be aware that indexing consumes memory and takes up additional disk space. @@ -16,7 +16,7 @@ The [dovecot-fts-xapian](https://github.com/grosjo/fts-xapian) plugin makes use The indexes will be stored as a subfolder named `xapian-indexes` inside your local `mail-data` folder (_`/var/mail` internally_). With the default settings, 10GB of email data may generate around 4GB of indexed data. -While indexing is memory intensive, you can configure the plugin to limit the amount of memory consumed by the index workers. With Xapian being small and fast, this plugin is a good choice for low memory environments (2GB) as compared to Solr. +While indexing is memory intensive, you can configure the plugin to limit the amount of memory consumed by the index workers. With Xapian being small and fast, this plugin is a good choice for low memory environments (2GB). #### Setup @@ -35,7 +35,7 @@ While indexing is memory intensive, you can configure the plugin to limit the am # disable indexing of folders # fts_autoindex_exclude = \Trash - # Index attachements + # Index attachments # fts_decoder = decode2text } @@ -55,16 +55,14 @@ While indexing is memory intensive, you can configure the plugin to limit the am adjust the settings to tune for your desired memory limits, exclude folders and enable searching text inside of attachments -2. Update `docker-compose.yml` to load the previously created dovecot plugin config file: +2. Update `compose.yaml` to load the previously created dovecot plugin config file: ```yaml - version: '3.8' services: mailserver: - image: docker.io/mailserver/docker-mailserver:latest + image: ghcr.io/docker-mailserver/docker-mailserver:latest container_name: mailserver - hostname: mail - domainname: example.com + hostname: mail.example.com env_file: mailserver.env ports: - "25:25" # SMTP (explicit TLS => STARTTLS) @@ -88,29 +86,29 @@ While indexing is memory intensive, you can configure the plugin to limit the am 3. Recreate containers: ``` - docker-compose down - docker-compose up -d + docker compose down + docker compose up -d ``` 4. Initialize indexing on all users for all mail: ``` - docker-compose exec mailserver doveadm index -A -q \* + docker compose exec mailserver doveadm index -A -q \* ``` 5. Run the following command in a daily cron job: ``` - docker-compose exec mailserver doveadm fts optimize -A + docker compose exec mailserver doveadm fts optimize -A ``` - Or like the [Spamassassin example][docs-faq-sa-learn-cron] shows, you can instead use `cron` from within `docker-mailserver` to avoid potential errors if the mail-server is not running: + Or like the [Spamassassin example][docs-faq-sa-learn-cron] shows, you can instead use `cron` from within DMS to avoid potential errors if the mail server is not running: ??? example Create a _system_ cron file: ```sh - # in the docker-compose.yml root directory + # in the compose.yaml root directory mkdir -p ./docker-data/dms/cron # if you didn't have this folder before touch ./docker-data/dms/cron/fts_xapian chown root:root ./docker-data/dms/cron/fts_xapian @@ -129,65 +127,21 @@ While indexing is memory intensive, you can configure the plugin to limit the am 0 4 * * * root doveadm fts optimize -A ``` - Then with `docker-compose.yml`: + Then with `compose.yaml`: ```yaml services: mailserver: - image: docker.io/mailserver/docker-mailserver:latest + image: ghcr.io/docker-mailserver/docker-mailserver:latest volumes: - ./docker-data/dms/cron/fts_xapian:/etc/cron.d/fts_xapian ``` - -### Solr - -The [dovecot-solr Plugin](https://wiki2.dovecot.org/Plugins/FTS/Solr) is used in conjunction with [Apache Solr](https://lucene.apache.org/solr/) running in a separate container. This is quite straightforward to setup using the following instructions. - -Solr is a mature and fast indexing backend that runs on the JVM. The indexes are relatively compact compared to the size of your total email. - -However, Solr also requires a fair bit of RAM. While Solr is [highly tuneable](https://solr.apache.org/guide/7_0/query-settings-in-solrconfig.html), it may require a bit of testing to get it right. - -#### Setup - -1. `docker-compose.yml`: - - ```yaml - solr: - image: lmmdock/dovecot-solr:latest - volumes: - - ./docker-data/dms/config/dovecot/solr-dovecot:/opt/solr/server/solr/dovecot - restart: always - - mailserver: - depends_on: - - solr - image: docker.io/mailserver/docker-mailserver:latest - ... - volumes: - ... - - ./docker-data/dms/config/dovecot/10-plugin.conf:/etc/dovecot/conf.d/10-plugin.conf:ro - ... - ``` - -2. `./docker-data/dms/config/dovecot/10-plugin.conf`: - - ```conf - mail_plugins = $mail_plugins fts fts_solr - - plugin { - fts = solr - fts_autoindex = yes - fts_solr = url=http://solr:8983/solr/dovecot/ - } - ``` - -3. Recreate containers: `docker-compose down ; docker-compose up -d` - -4. Flag all user mailbox FTS indexes as invalid, so they are rescanned on demand when they are next searched: `docker-compose exec mailserver doveadm fts rescan -A` - #### Further Discussion See [#905](https://github.com/docker-mailserver/docker-mailserver/issues/905) +Attempting to enable commented out features in the config example above [may not be functional][gh::xapian-decode2text]. + [docs-faq-sa-learn-cron]: ../../faq.md#how-can-i-make-spamassassin-better-recognize-spam +[gh::xapian-decode2text]: https://github.com/orgs/docker-mailserver/discussions/4461#discussioncomment-13002388 diff --git a/docs/content/config/advanced/ipv6.md b/docs/content/config/advanced/ipv6.md index 56c02ee484a..7e9c904cffb 100644 --- a/docs/content/config/advanced/ipv6.md +++ b/docs/content/config/advanced/ipv6.md @@ -2,47 +2,216 @@ title: 'Advanced | IPv6' --- -## Background - -If your container host supports IPv6, then `docker-mailserver` will automatically accept IPv6 connections by way of the docker host's IPv6. However, incoming mail will fail SPF checks because they will appear to come from the IPv4 gateway that docker is using to proxy the IPv6 connection (`172.20.0.1` is the gateway). - -This can be solved by supporting IPv6 connections all the way to the `docker-mailserver` container. - -## Setup steps - -```diff -+++ b/serv/docker-compose.yml -@@ -1,4 +1,4 @@ --version: '2' -+version: '2.1' - -@@ -32,6 +32,16 @@ services: - -+ ipv6nat: -+ image: robbertkl/ipv6nat -+ restart: always -+ network_mode: "host" -+ cap_add: -+ - NET_ADMIN -+ - SYS_MODULE -+ volumes: -+ - /var/run/docker.sock:/var/run/docker.sock:ro -+ - /lib/modules:/lib/modules:ro - -@@ -306,4 +316,13 @@ networks: - -+ default: -+ driver: bridge -+ enable_ipv6: true -+ ipam: -+ driver: default -+ config: -+ - subnet: fd00:0123:4567::/48 -+ gateway: fd00:0123:4567::1 +!!! bug "Ample Opportunities for Issues" + + Numerous bug reports have been raised in the past about IPv6. Please make sure your setup around DMS is correct when using IPv6! + +## IPv6 networking problems with Docker defaults + +### What can go wrong? + +If your host system supports IPv6 and an `AAAA` DNS record exists to direct IPv6 traffic to DMS, you may experience issues when an IPv6 connection is made: + +- The original client IP is replaced with the gateway IP of a docker network. +- Connections fail or hang. + +The impact of losing the real IP of the client connection can negatively affect DMS: + +- Users unable to login (_Fail2Ban action triggered by repeated login failures all seen as from the same internal Gateway IP_) +- Mail inbound to DMS is rejected (_[SPF verification failure][gh-issue-1438-spf], IP mismatch_) +- Delivery failures from [sender reputation][sender-score] being reduced (_due to [bouncing inbound mail][gh-issue-3057-bounce] from rejected IPv6 clients_) +- Some services may be configured to trust connecting clients within the containers subnet, which includes the Gateway IP. This can risk bypassing or relaxing security measures, such as exposing an [open relay][wikipedia-openrelay]. + +### Why does this happen? + +When the host network receives a connection to a containers published port, it is routed to the containers internal network managed by Docker (_typically a bridge network_). + +By default, the Docker daemon only assigns IPv4 addresses to containers, thus it will only accept IPv4 connections (_unless a `docker-proxy` process is listening, which the default daemon setting `userland-proxy: true` enables_). With the daemon setting `userland-proxy: true` (default), IPv6 connections from the host can also be accepted and routed to containers (_even when they only have IPv4 addresses assigned_). `userland-proxy: false` will require the container to have atleast an IPv6 address assigned. + +This can be problematic for IPv6 host connections when internally the container is no longer aware of the original client IPv6 address, as it has been proxied through the IPv4 or IPv6 gateway address of it's connected network (_eg: `172.17.0.1` - Docker allocates networks from a set of [default subnets][docker-subnets]_). + +This can be fixed by enabling a Docker network to assign IPv6 addresses to containers, along with some additional configuration. Alternatively you could configure the opposite to prevent IPv6 connections being made. + +## Prevent IPv6 connections + +- Avoiding an `AAAA` DNS record for your DMS FQDN would prevent resolving an IPv6 address to connect to. +- You can also use `userland-proxy: false`, which will fail to establish a remote connection to DMS (_provided no IPv6 address was assigned_). + +!!! tip "With UFW or Firewalld" + + When one of these firewall frontends are active, remote clients should fail to connect instead of being masqueraded as the docker network gateway IP. Keep in mind that this only affects remote clients, it does not affect local IPv6 connections originating within the same host. + +## Enable proper IPv6 support + +You can enable IPv6 support in Docker for container networks, however [compatibility concerns][docs-compat] may affect your success. + +The [official Docker documentation on enabling IPv6][docker-docs-enable-ipv6] has been improving and is a good resource to reference. + +Enable `ip6tables` support so that Docker will manage IPv6 networking rules as well. This will allow for IPv6 NAT to work like the existing IPv4 NAT already does for your containers, avoiding the above issue with external connections having their IP address seen as the container network gateway IP (_provided an IPv6 address is also assigned to the container_). + +!!! example "Configure the following in `/etc/docker/daemon.json`" + + ```json + { + "ip6tables": true, + "experimental" : true, + "userland-proxy": true + } + ``` + + - `experimental: true` is currently required for `ip6tables: true` to work. + - `userland-proxy` setting [can potentially affect connection behavior][gh-pull-3244-proxy] for local connections. + + Now restart the daemon if it's running: `systemctl restart docker`. + +Next, configure a network with an IPv6 subnet for your container with any of these examples: + +???+ example "Create an IPv6 ULA subnet" + + ??? info "About these examples" + + These examples are focused on a [IPv6 ULA subnet][wikipedia-ipv6-ula] which is suitable for most users as described in the next section. + + - You may prefer a subnet size smaller than `/64` (eg: `/112`, which still provides over 65k IPv6 addresses), especially if instead configuring for an IPv6 GUA subnet. + - The network will also implicitly be assigned an IPv4 subnet (_from the Docker daemon config `default-address-pools`_). + + === "User-defined Network" + + The preferred approach is with [user-defined networks][docker-docs-ipv6-create-custom] via `compose.yaml` (recommended) or CLI with `docker network create`: + + === "Compose" + + Create the network in `compose.yaml` and attach a service to it: + + ```yaml title="compose.yaml" + services: + mailserver: + networks: + - dms-ipv6 + + networks: + dms-ipv6: + enable_ipv6: true + ipam: + config: + - subnet: fd00:cafe:face:feed::/64 + ``` + + ??? tip "Override the implicit `default` network" + + You can optionally avoid the service assignment by [overriding the `default` user-defined network that Docker Compose generates][docker-docs-network-compose-default]. Just replace `dms-ipv6` with `default`. + + The Docker Compose `default` bridge is not affected by settings for the default `bridge` (aka `docker0`) in `/etc/docker/daemon.json`. + + ??? tip "Using the network outside of this `compose.yaml`" + + To reference this network externally (_from other compose files or `docker run`_), assign the [networks `name` key in `compose.yaml`][docker-docs-network-external]. + + === "CLI" + + Create the network via a CLI command (_which can then be used with `docker run --network dms-ipv6`_): + + ```bash + docker network create --ipv6 --subnet fd00:cafe:face:feed::/64 dms-ipv6 + ``` + + Optionally reference it from one or more `compose.yaml` files: + + ```yaml title="compose.yaml" + services: + mailserver: + networks: + - dms-ipv6 + + networks: + dms-ipv6: + external: true + ``` + + === "Default Bridge (daemon)" + + !!! warning "This approach is discouraged" + + The [`bridge` network is considered legacy][docker-docs-network-bridge-legacy]. + + Add these two extra IPv6 settings to your daemon config. They only apply to the [default `bridge` docker network][docker-docs-ipv6-create-default] aka `docker0` (_which containers are attached to by default when using `docker run`_). + + ```json title="/etc/docker/daemon.json" + { + "ipv6": true, + "fixed-cidr-v6": "fd00:cafe:face:feed::/64", + } + ``` + + Compose projects can also use this network via `network_mode`: + + ```yaml title="compose.yaml" + services: + mailserver: + network_mode: bridge + ``` + +!!! danger "Do not use `2001:db8:1::/64` for your private subnet" + + The `2001:db8` address prefix is [reserved for documentation][wikipedia-ipv6-reserved]. Avoid creating a subnet with this prefix. + + Presently this is used in examples for Dockers IPv6 docs as a placeholder, while mixed in with private IPv4 addresses which can be misleading. + +### Configuring an IPv6 subnet + +If you've [configured IPv6 address pools in `/etc/docker/daemon.json`][docker-docs-ipv6-supernets], you do not need to specify a subnet explicitly. Otherwise if you're unsure what value to provide, here's a quick guide (_Tip: Prefer IPv6 ULA, it's the least hassle_): + +- `fd00:cafe:face:feed::/64` is an example of a [IPv6 ULA subnet][wikipedia-ipv6-ula]. ULA addresses are akin to the [private IPv4 subnets][wikipedia-ipv4-private] you may already be familiar with. You can use that example, or choose your own ULA address. This is a good choice for getting Docker containers to their have networks support IPv6 via NAT like they already do by default with IPv4. +- IPv6 without NAT, using public address space like your server is assigned belongs to an [IPv6 GUA subnet][wikipedia-ipv6-gua]. + - Typically these will be a `/64` block assigned to your host, but this varies by provider. + - These addresses do not need to publish ports of a container to another IP to be publicly reached (_thus `ip6tables: true` is not required_), you will want a firewall configured to manage which ports are accessible instead as no NAT is involved. Note that this may not be desired if the container should also be reachable via the host IPv4 public address. + - You may want to subdivide the `/64` into smaller subnets for Docker to use only portions of the `/64`. This can reduce some routing features, and [require additional setup / management via a NDP Proxy][gh-pull-3244-gua] for your public interface to know of IPv6 assignments managed by Docker and accept external traffic. + +### Verify remote IP is correct + +With Docker CLI or Docker Compose, run a `traefik/whoami` container with your IPv6 docker network and port 80 published. You can then send a curl request (or via address in the browser) from another host (as your remote client) with an IPv6 network, the `RemoteAddr` value returned should match your client IPv6 address. + +```bash +docker run --rm -d --network dms-ipv6 -p 80:80 traefik/whoami +# On a different host, replace `2001:db8::1` with your DMS host IPv6 address +curl --max-time 5 http://[2001:db8::1]:80 ``` -## Further Discussion +!!! warning "IPv6 gateway IP" + + If instead of the remote IPv6 address, you may notice the gateway IP for the IPv6 subnet your DMS container belongs to. + + This will happen when DMS has an IPv6 IP address assigned, for the same reason as with IPv4, `userland-proxy: true`. It indicates that your `daemon.json` has not been configured correctly or had the updated config applied for `ip6tables :true` + `experimental: true`. Make sure you used `systemctl restart docker` after updating `daemon.json`. + +!!! info "IPv6 ULA address priority" + + DNS lookups that have records for both IPv4 and IPv6 addresses (_eg: `localhost`_) may prefer IPv4 over IPv6 (ULA) for private addresses, whereas for public addresses IPv6 has priority. This shouldn't be anything to worry about, but can come across as a surprise when testing your IPv6 setup on the same host instead of from a remote client. + + The preference can be controlled with [`/etc/gai.conf`][networking-gai], and appears was configured this way based on [the assumption that IPv6 ULA would never be used with NAT][networking-gai-blog]. It should only affect the destination resolved for outgoing connections, which for IPv6 ULA should only really affect connections between your containers / host. In future [IPv6 ULA may also be prioritized][networking-gai-rfc]. + +[docker-subnets]: https://straz.to/2021-09-08-docker-address-pools/#what-are-the-default-address-pools-when-no-configuration-is-given-vanilla-pools +[sender-score]: https://senderscore.org/assess/get-your-score/ +[gh-issue-1438-spf]: https://github.com/docker-mailserver/docker-mailserver/issues/1438 +[gh-issue-3057-bounce]: https://github.com/docker-mailserver/docker-mailserver/pull/3057#issuecomment-1416700046 +[wikipedia-openrelay]: https://en.wikipedia.org/wiki/Open_mail_relay + +[docs-compat]: ../debugging.md#compatibility + +[gh-pull-3244-proxy]: https://github.com/docker-mailserver/docker-mailserver/pull/3244#issuecomment-1603436809 +[docker-docs-enable-ipv6]: https://docs.docker.com/config/daemon/ipv6/ +[docker-docs-ipv6-create-custom]: https://docs.docker.com/config/daemon/ipv6/#create-an-ipv6-network +[docker-docs-ipv6-create-default]: https://docs.docker.com/config/daemon/ipv6/#use-ipv6-for-the-default-bridge-network +[docker-docs-ipv6-supernets]: https://docs.docker.com/config/daemon/ipv6/#dynamic-ipv6-subnet-allocation +[docker-docs-network-external]: https://docs.docker.com/compose/compose-file/06-networks/#name +[docker-docs-network-compose-default]: https://docs.docker.com/compose/networking/#configure-the-default-network +[docker-docs-network-bridge-legacy]: https://docs.docker.com/network/drivers/bridge/#use-the-default-bridge-network -See [#1438][github-issue-1438] +[wikipedia-ipv6-reserved]: https://en.wikipedia.org/wiki/IPv6_address#Documentation +[wikipedia-ipv4-private]: https://en.wikipedia.org/wiki/Private_network#Private_IPv4_addresses +[wikipedia-ipv6-ula]: https://en.wikipedia.org/wiki/Unique_local_address +[wikipedia-ipv6-gua]: https://en.wikipedia.org/wiki/IPv6#Global_addressing +[gh-pull-3244-gua]: https://github.com/docker-mailserver/docker-mailserver/pull/3244#issuecomment-1528984894 -[github-issue-1438]: https://github.com/docker-mailserver/docker-mailserver/issues/1438 +[networking-gai]: https://linux.die.net/man/5/gai.conf +[networking-gai-blog]: https://thomas-leister.de/en/lxd-prefer-ipv6-outgoing/ +[networking-gai-rfc]:https://datatracker.ietf.org/doc/html/draft-ietf-v6ops-ula diff --git a/docs/content/config/advanced/kubernetes.md b/docs/content/config/advanced/kubernetes.md index a84e1064fc7..04075cf016b 100644 --- a/docs/content/config/advanced/kubernetes.md +++ b/docs/content/config/advanced/kubernetes.md @@ -4,516 +4,800 @@ title: 'Advanced | Kubernetes' ## Introduction -This article describes how to deploy `docker-mailserver` to Kubernetes. Please note that there is also a [Helm chart] available. +This article describes how to deploy DMS to Kubernetes. We highly recommend everyone to use our community [DMS Helm chart][github-web::docker-mailserver-helm]. -!!! attention "Requirements" +!!! note "Requirements" - We assume basic knowledge about Kubernetes from the reader. Moreover, we assume the reader to have a basic understanding of mail servers. Ideally, the reader has deployed `docker-mailserver` before in an easier setup with Docker (Compose). + 1. Basic knowledge about Kubernetes from the reader. + 2. A basic understanding of mail servers. + 3. Ideally, the reader has already deployed DMS before with a simpler setup (_`docker run` or Docker Compose_). -!!! warning "About Support for Kubernetes" +!!! warning "Limited Support" - Please note that Kubernetes **is not** officially supported and we do not build images specifically designed for it. When opening an issue, please remember that only Docker & Docker Compose are officially supported. + DMS **does not officially support Kubernetes**. This content is entirely community-supported. If you find errors, please open an issue and raise a PR. - This content is entirely community-supported. If you find errors, please open an issue and provide a PR. +## Manually Writing Manifests -## Manifests +If using our Helm chart is not viable for you, here is some guidance to start with your own manifests. -### Configuration + +!!! quote "" -We want to provide the basic configuration in the form of environment variables with a `ConfigMap`. Note that this is just an example configuration; tune the `ConfigMap` to your needs. + === "`ConfigMap`" -```yaml ---- -apiVersion: v1 -kind: ConfigMap - -metadata: - name: mailserver.environment - -immutable: false - -data: - TLS_LEVEL: modern - POSTSCREEN_ACTION: drop - OVERRIDE_HOSTNAME: mail.example.com - FAIL2BAN_BLOCKTYPE: drop - POSTMASTER_ADDRESS: postmaster@example.com - UPDATE_CHECK_INTERVAL: 10d - POSTFIX_INET_PROTOCOLS: ipv4 - ONE_DIR: '1' - ENABLE_CLAMAV: '1' - ENABLE_POSTGREY: '0' - ENABLE_FAIL2BAN: '1' - AMAVIS_LOGLEVEL: '-1' - SPOOF_PROTECTION: '1' - MOVE_SPAM_TO_JUNK: '1' - ENABLE_UPDATE_CHECK: '1' - ENABLE_SPAMASSASSIN: '1' - SUPERVISOR_LOGLEVEL: warn - SPAMASSASSIN_SPAM_TO_INBOX: '1' - - # here, we provide an example for the SSL configuration - SSL_TYPE: manual - SSL_CERT_PATH: /secrets/ssl/rsa/tls.crt - SSL_KEY_PATH: /secrets/ssl/rsa/tls.key -``` - -We can also make use of user-provided configuration files, e.g. `user-patches.sh`, `postfix-accounts.cf` and more, to adjust `docker-mailserver` to our likings. We encourage you to have a look at [Kustomize][kustomize] for creating `ConfigMap`s from multiple files, but for now, we will provide a simple, hand-written example. This example is absolutely minimal and only goes to show what can be done. - -```yaml ---- -apiVersion: v1 -kind: ConfigMap + Provide the basic configuration via environment variables with a `ConfigMap`. -metadata: - name: mailserver.files + !!! example -data: - postfix-accounts.cf: | - test@example.com|{SHA512-CRYPT}$6$someHashValueHere - other@example.com|{SHA512-CRYPT}$6$someOtherHashValueHere -``` + Below is only an example configuration, adjust the `ConfigMap` to your own needs. -!!! attention "Static Configuration" + ```yaml + --- + apiVersion: v1 + kind: ConfigMap - With the configuration shown above, you can **not** dynamically add accounts as the configuration file mounted into the mail server can not be written to. + metadata: + name: mailserver.environment - Use persistent volumes for production deployments. + immutable: false -### Persistence + data: + TLS_LEVEL: modern + POSTSCREEN_ACTION: drop + OVERRIDE_HOSTNAME: mail.example.com + FAIL2BAN_BLOCKTYPE: drop + POSTMASTER_ADDRESS: postmaster@example.com + UPDATE_CHECK_INTERVAL: 10d + POSTFIX_INET_PROTOCOLS: ipv4 + ENABLE_CLAMAV: '1' + ENABLE_POSTGREY: '0' + ENABLE_FAIL2BAN: '1' + AMAVIS_LOGLEVEL: '-1' + SPOOF_PROTECTION: '1' + MOVE_SPAM_TO_JUNK: '1' + ENABLE_UPDATE_CHECK: '1' + ENABLE_SPAMASSASSIN: '1' + SUPERVISOR_LOGLEVEL: warn + SPAMASSASSIN_SPAM_TO_INBOX: '1' -Thereafter, we need persistence for our data. Make sure you have a storage provisioner and that you choose the correct `storageClassName`. + # here, we provide an example for the SSL configuration + SSL_TYPE: manual + SSL_CERT_PATH: /secrets/ssl/rsa/tls.crt + SSL_KEY_PATH: /secrets/ssl/rsa/tls.key + ``` -```yaml ---- -apiVersion: v1 -kind: PersistentVolumeClaim + You can also make use of user-provided configuration files (_e.g. `user-patches.sh`, `postfix-accounts.cf`, etc_), to customize DMS to your needs. -metadata: - name: data + ??? example "Providing config files" -spec: - storageClassName: local-path - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 25Gi -``` + Here is a minimal example that supplies a `postfix-accounts.cf` file inline with two users: -### Service + ```yaml + --- + apiVersion: v1 + kind: ConfigMap -A `Service` is required for getting the traffic to the pod itself. The service is somewhat crucial. Its configuration determines whether the original IP from the sender will be kept. [More about this further down below](#exposing-your-mail-server-to-the-outside-world). + metadata: + name: mailserver.files -The configuration you're seeing does keep the original IP, but you will not be able to scale this way. We have chosen to go this route in this case because we think most Kubernetes users will only want to have one instance. + data: + postfix-accounts.cf: | + test@example.com|{SHA512-CRYPT}$6$someHashValueHere + other@example.com|{SHA512-CRYPT}$6$someOtherHashValueHere + ``` -```yaml ---- -apiVersion: v1 -kind: Service - -metadata: - name: mailserver - labels: - app: mailserver - -spec: - type: LoadBalancer - - selector: - app: mailserver - - ports: - # Transfer - - name: transfer - port: 25 - targetPort: transfer - protocol: TCP - # ESMTP with implicit TLS - - name: esmtp-implicit - port: 465 - targetPort: esmtp-implicit - protocol: TCP - # ESMTP with explicit TLS (STARTTLS) - - name: esmtp-explicit - port: 587 - targetPort: esmtp-explicit - protocol: TCP - # IMAPS with implicit TLS - - name: imap-implicit - port: 993 - targetPort: imap-implicit - protocol: TCP - -``` - -### Deployments - -Last but not least, the `Deployment` becomes the most complex component. It instructs Kubernetes how to run the `docker-mailserver` container and how to apply your `ConfigMaps`, persisted storage, etc. Additionally, we can set options to enforce runtime security here. - -```yaml ---- -apiVersion: apps/v1 -kind: Deployment - -metadata: - name: mailserver - - annotations: - ignore-check.kube-linter.io/run-as-non-root: >- - 'mailserver' needs to run as root - ignore-check.kube-linter.io/privileged-ports: >- - 'mailserver' needs privilegdes ports - ignore-check.kube-linter.io/no-read-only-root-fs: >- - There are too many files written to make The - root FS read-only - -spec: - replicas: 1 - selector: - matchLabels: - app: mailserver - - template: - metadata: - labels: - app: mailserver - - annotations: - container.apparmor.security.beta.kubernetes.io/mailserver: runtime/default - - spec: - hostname: mail - containers: - - name: mailserver - image: docker.io/mailserver/docker-mailserver:latest - imagePullPolicy: IfNotPresent - - securityContext: - allowPrivilegeEscalation: false - readOnlyRootFilesystem: false - runAsUser: 0 - runAsGroup: 0 - runAsNonRoot: false - privileged: false - capabilities: - add: - # file permission capabilities - - CHOWN - - FOWNER - - MKNOD - - SETGID - - SETUID - - DAC_OVERRIDE - # network capabilities - - NET_ADMIN # needed for F2B - - NET_RAW # needed for F2B - - NET_BIND_SERVICE - # miscellaneous capabilities - - SYS_CHROOT - - KILL - drop: [ALL] - seccompProfile: - type: RuntimeDefault - - # You want to tune this to your needs. If you disable ClamAV, - # you can use less RAM and CPU. This becomes important in - # case you're low on resources and Kubernetes refuses to - # schedule new pods. - resources: - limits: - memory: 4Gi - cpu: 1500m - requests: - memory: 2Gi - cpu: 600m - - volumeMounts: - - name: files - subPath: postfix-accounts.cf - mountPath: /tmp/docker-mailserver/postfix-accounts.cf - readOnly: true - - # PVCs - - name: data - mountPath: /var/mail - subPath: data - readOnly: false - - name: data - mountPath: /var/mail-state - subPath: state - readOnly: false - - name: data - mountPath: /var/log/mail - subPath: log - readOnly: false - - # certificates - - name: certificates-rsa - mountPath: /secrets/ssl/rsa/ - readOnly: true - - # other - - name: tmp-files - mountPath: /tmp - readOnly: false - - ports: - - name: transfer - containerPort: 25 - protocol: TCP - - name: esmtp-implicit - containerPort: 465 - protocol: TCP - - name: esmtp-explicit - containerPort: 587 - - name: imap-implicit - containerPort: 993 - protocol: TCP - - envFrom: - - configMapRef: - name: mailserver.environment - - restartPolicy: Always - - volumes: - # configuration files - - name: files - configMap: - name: mailserver.files - - # PVCs - - name: data - persistentVolumeClaim: - claimName: data - - # certificates - - name: certificates-rsa - secret: - secretName: mail-tls-certificate-rsa - items: - - key: tls.key - path: tls.key - - key: tls.crt - path: tls.crt - - # other - - name: tmp-files - emptyDir: {} -``` - -### Certificates - An Example - -In this example, we use [`cert-manager`][cert-manager] to supply RSA certificates. You can also supply RSA certificates as fallback certificates, which `docker-mailserver` supports out of the box with `SSL_ALT_CERT_PATH` and `SSL_ALT_KEY_PATH`, and provide ECDSA as the proper certificates. - -```yaml ---- -apiVersion: cert-manager.io/v1 -kind: Certificate + !!! warning "Static Configuration" -metadata: - name: mail-tls-certificate-rsa + The inline `postfix-accounts.cf` config example above provides file content that is static. It is mounted as read-only at runtime, thus cannot support modifications. -spec: - secretName: mail-tls-certificate-rsa - isCA: false - privateKey: - algorithm: RSA - encoding: PKCS1 - size: 2048 - dnsNames: [mail.example.com] - issuerRef: - name: mail-issuer - kind: Issuer -``` + For production deployments, use persistent volumes instead (via `PersistentVolumeClaim`). That will enable files like `postfix-account.cf` to add and remove accounts, while also persisting those changes externally from the container. -!!! attention + !!! tip "Modularize your `ConfigMap`" - You will need to have [`cert-manager`][cert-manager] configured. Especially the issue will need to be configured. Since we do not know how you want or need your certificates to be supplied, we do not provide more configuration here. The documentation for [`cert-manager`][cert-manager] is excellent. + [Kustomize][kustomize] can be a useful tool as it supports creating a `ConfigMap` from multiple files. -### Sensitive Data + === "`PersistentVolumeClaim`" -!!! attention "Sensitive Data" + To persist data externally from the DMS container, configure a `PersistentVolumeClaim` (PVC). - For storing OpenDKIM keys, TLS certificates or any sort of sensitive data, you should be using `Secret`s. You can mount secrets like `ConfigMap`s and use them the same way. + Make sure you have a storage system (like Longhorn, Rook, etc.) and that you choose the correct `storageClassName` (according to your storage system). -The [TLS docs page][docs-tls] provides guidance when it comes to certificates and transport layer security. Always provide sensitive information vai `Secrets`. + !!! example -## Exposing your Mail-Server to the Outside World + ```yaml + --- + apiVersion: v1 + kind: PersistentVolumeClaim -The more difficult part with Kubernetes is to expose a deployed `docker-mailserver` to the outside world. Kubernetes provides multiple ways for doing that; each has downsides and complexity. The major problem with exposing `docker-mailserver` to outside world in Kubernetes is to [preserve the real client IP][Kubernetes-service-source-ip]. The real client IP is required by `docker-mailserver` for performing IP-based SPF checks and spam checks. If you do not require SPF checks for incoming mails, you may disable them in your [Postfix configuration][docs-postfix] by dropping the line that states: `check_policy_service unix:private/policyd-spf`. + metadata: + name: data -The easiest approach was covered above, using `#!yaml externalTrafficPolicy: Local`, which disables the service proxy, but makes the service local as well (which does not scale). This approach only works when you are given the correct (that is, a public and routable) IP address by a load balancer (like MetalLB). In this sense, the approach above is similar to the next example below. We want to provide you with a few alternatives too. **But** we also want to communicate the idea of another simple method: you could use a load-balancer without an external IP and DNAT the network traffic to the mail-server. After all, this does not interfere with SPF checks because it keeps the origin IP address. If no dedicated external IP address is available, you could try the latter approach, if one is available, use the former. + spec: + storageClassName: local-path + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 25Gi + ``` -### External IPs Service + === "`Service`" -The simplest way is to expose `docker-mailserver` as a [Service][Kubernetes-network-service] with [external IPs][Kubernetes-network-external-ip]. This is very similar to the approach taken above. Here, an external IP is given to the service directly by you. With the approach above, you tell your load-balancer to do this. + A [`Service`][k8s-docs::config::service] is required for getting the traffic to the pod itself. It configures a load balancer with the ports you'll need. -```yaml ---- -apiVersion: v1 -kind: Service + The configuration for a `Service` affects if the original IP from a connecting client is preserved (_this is important_). [More about this further down below](#exposing-your-mail-server-to-the-outside-world). -metadata: - name: mailserver - labels: - app: mailserver + !!! example -spec: - selector: - app: mailserver - ports: - - name: smtp - port: 25 - targetPort: smtp - # ... + ```yaml + --- + apiVersion: v1 + kind: Service - externalIPs: - - 80.11.12.10 -``` + metadata: + name: mailserver + labels: + app: mailserver -This approach + spec: + # `Local` is most likely required, otherwise every incoming request would be identified by the external IP, + # which will get banned by Fail2Ban when monitored services are not configured for PROXY protocol + externalTrafficPolicy: Local + type: LoadBalancer + + selector: + app: mailserver -- does not preserve the real client IP, so SPF check of incoming mail will fail. -- requires you to specify the exposed IPs explicitly. + ports: + # smtp + - name: smtp + port: 25 + targetPort: smtp + protocol: TCP + # submissions (ESMTP with implicit TLS) + - name: submissions + port: 465 + targetPort: submissions + protocol: TCP + # submission (ESMTP with explicit TLS) + - name: submission + port: 587 + targetPort: submission + protocol: TCP + # imaps (implicit TLS) + - name: imaps + port: 993 + targetPort: imaps + protocol: TCP + ``` + + === "`Certificate`" + + !!! example "Using [`cert-manager`][cert-manager] to supply TLS certificates" + + ```yaml + --- + apiVersion: cert-manager.io/v1 + kind: Certificate + + metadata: + name: mail-tls-certificate-rsa + + spec: + secretName: mail-tls-certificate-rsa + isCA: false + privateKey: + algorithm: RSA + encoding: PKCS1 + size: 2048 + dnsNames: [mail.example.com] + issuerRef: + name: mail-issuer + kind: Issuer + ``` + + The [TLS docs page][docs-tls] provides guidance when it comes to certificates and transport layer security. + + !!! tip "ECDSA + RSA (fallback)" + + You could supply RSA certificates as fallback certificates instead, with ECDSA as the primary. DMS supports dual certificates via the ENV `SSL_ALT_CERT_PATH` and `SSL_ALT_KEY_PATH`. + + !!! warning "Always provide sensitive information via a `Secret`" + + For storing OpenDKIM keys, TLS certificates, or any sort of sensitive data - you should be using `Secret`s. + + A `Secret` is similar to `ConfigMap`, it can be used and mounted as a volume as demonstrated in the [`Deployment` manifest][docs::k8s::config-deployment] tab. + + === "`Deployment`" + + The [`Deployment`][k8s-docs::config::deployment] config is the most complex component. + + - It instructs Kubernetes how to run the DMS container and how to apply your `ConfigMap`s, persisted storage, etc. + - Additional options can be set to enforce runtime security. + + ???+ example + + ```yaml + --- + apiVersion: apps/v1 + kind: Deployment + + metadata: + name: mailserver + + annotations: + ignore-check.kube-linter.io/run-as-non-root: >- + 'mailserver' needs to run as root + ignore-check.kube-linter.io/privileged-ports: >- + 'mailserver' needs privileged ports + ignore-check.kube-linter.io/no-read-only-root-fs: >- + There are too many files written to make the root FS read-only + + spec: + replicas: 1 + selector: + matchLabels: + app: mailserver + + template: + metadata: + labels: + app: mailserver + + annotations: + container.apparmor.security.beta.kubernetes.io/mailserver: runtime/default + + spec: + hostname: mail + containers: + - name: mailserver + image: ghcr.io/docker-mailserver/docker-mailserver:latest + imagePullPolicy: IfNotPresent + + securityContext: + # `allowPrivilegeEscalation: true` is required to support SGID via the `postdrop` + # executable in `/var/mail-state` for Postfix (maildrop + public dirs): + # https://github.com/docker-mailserver/docker-mailserver/pull/3625 + allowPrivilegeEscalation: true + readOnlyRootFilesystem: false + runAsUser: 0 + runAsGroup: 0 + runAsNonRoot: false + privileged: false + capabilities: + add: + # file permission + - CHOWN + - FOWNER + - MKNOD + - SETGID + - SETUID + - DAC_OVERRIDE + # network + - NET_ADMIN # needed for F2B + - NET_RAW # needed for F2B + - NET_BIND_SERVICE + # miscellaneous + - SYS_CHROOT + - MAC_OVERRIDE + - KILL + drop: [ALL] + seccompProfile: + type: RuntimeDefault + + # Tune this to your needs. + # If you disable ClamAV, you can use less RAM and CPU. + # This becomes important in case you're low on resources + # and Kubernetes refuses to schedule new pods. + resources: + limits: + memory: 4Gi + cpu: 1500m + requests: + memory: 2Gi + cpu: 600m + + volumeMounts: + - name: files + subPath: postfix-accounts.cf + mountPath: /tmp/docker-mailserver/postfix-accounts.cf + readOnly: true + + # PVCs + - name: data + mountPath: /var/mail + subPath: data + readOnly: false + - name: data + mountPath: /var/mail-state + subPath: state + readOnly: false + - name: data + mountPath: /var/log/mail + subPath: log + readOnly: false + + # certificates + - name: certificates-rsa + mountPath: /secrets/ssl/rsa/ + readOnly: true + + ports: + - name: smtp + containerPort: 25 + protocol: TCP + - name: submissions + containerPort: 465 + protocol: TCP + - name: submission + containerPort: 587 + - name: imaps + containerPort: 993 + protocol: TCP + + envFrom: + - configMapRef: + name: mailserver.environment + + restartPolicy: Always + + volumes: + # configuration files + - name: files + configMap: + name: mailserver.files + + # PVCs + - name: data + persistentVolumeClaim: + claimName: data + + # certificates + - name: certificates-rsa + secret: + secretName: mail-tls-certificate-rsa + items: + - key: tls.key + path: tls.key + - key: tls.crt + path: tls.crt + ``` + +## Exposing your Mail Server to the Outside World + +The more difficult part with Kubernetes is to expose a deployed DMS instance to the outside world. + +The major problem with exposing DMS to the outside world in Kubernetes is to [preserve the real client IP][k8s-docs::service-source-ip]. The real client IP is required by DMS for performing IP-based DNS and spam checks. + +Kubernetes provides multiple ways to address this; each has its upsides and downsides. + + +!!! quote "" + + === "Configure IP Manually" + + ???+ abstract "Advantages / Disadvantages" + + - [x] Simple + - [ ] Requires the node to have a dedicated, publicly routable IP address + - [ ] Limited to a single node (_associated to the dedicated IP address_) + - [ ] Your deployment requires an explicit IP in your configuration (_or an entire Load Balancer_). + + !!! info "Requirements" + + 1. You can dedicate a **publicly routable IP** address for the DMS configured `Service`. + 2. A dedicated IP is required to allow your mail server to have matching `A` and `PTR` records (_which other mail servers will use to verify trust when they receive mail sent from your DMS instance_). + + !!! example + + Assign the DMS `Service` an external IP directly, or delegate an LB to assign the IP on your behalf. + + === "External-IP Service" + + The DMS `Service` is configured with an "[external IP][k8s-docs::network-external-ip]" manually. Append your externally reachable IP address to `spec.externalIPs`. + + ```yaml + --- + apiVersion: v1 + kind: Service + + metadata: + name: mailserver + labels: + app: mailserver + + spec: + selector: + app: mailserver + ports: + - name: smtp + port: 25 + targetPort: smtp + # ... + + externalIPs: + - 10.20.30.40 + ``` + + === "Load-Balancer" + + The config differs depending on your choice of load balancer. This example uses [MetalLB][metallb-web]. + + ```yaml + --- + apiVersion: v1 + kind: Service + + metadata: + name: mailserver + labels: + app: mailserver + annotations: + metallb.universe.tf/address-pool: mailserver -### Proxy port to Service + # ... -The [proxy pod][Kubernetes-proxy-service] helps to avoid the necessity of specifying external IPs explicitly. This comes at the cost of complexity; you must deploy a proxy pod on each [Node][Kubernetes-nodes] you want to expose `docker-mailserver` on. + --- + apiVersion: metallb.io/v1beta1 + kind: IPAddressPool -This approach + metadata: + name: mail + namespace: metallb-system -- does not preserve the real client IP, so SPF check of incoming mail will fail. + spec: + addresses: [ ] + autoAssign: true -### Bind to concrete Node and use host network + --- + apiVersion: metallb.io/v1beta1 + kind: L2Advertisement -One way to preserve the real client IP is to use `hostPort` and `hostNetwork: true`. This comes at the cost of availability; you can reach `docker-mailserver` from the outside world only via IPs of [Node][Kubernetes-nodes] where `docker-mailserver` is deployed. + metadata: + name: mail + namespace: metallb-system -```yaml ---- -apiVersion: extensions/v1beta1 -kind: Deployment - -metadata: - name: mailserver - -# ... - spec: - hostNetwork: true - - # ... - containers: - # ... - ports: - - name: smtp - containerPort: 25 - hostPort: 25 - - name: smtp-auth - containerPort: 587 - hostPort: 587 - - name: imap-secure - containerPort: 993 - hostPort: 993 - # ... -``` - -With this approach, - -- it is not possible to access `docker-mailserver` via other cluster Nodes, only via the Node `docker-mailserver` was deployed at. -- every Port within the Container is exposed on the Host side. - -### Proxy Port to Service via PROXY Protocol - -This way is ideologically the same as [using a proxy pod](#proxy-port-to-service), but instead of a separate proxy pod, you configure your ingress to proxy TCP traffic to the `docker-mailserver` pod using the PROXY protocol, which preserves the real client IP. - -#### Configure your Ingress - -With an [NGINX ingress controller][Kubernetes-nginx], set `externalTrafficPolicy: Local` for its service, and add the following to the TCP services config map (as described [here][Kubernetes-nginx-expose]): - -```yaml -25: "mailserver/mailserver:25::PROXY" -465: "mailserver/mailserver:465::PROXY" -587: "mailserver/mailserver:587::PROXY" -993: "mailserver/mailserver:993::PROXY" -``` - -!!! help "HAProxy" - With [HAProxy][dockerhub-haproxy], the configuration should look similar to the above. If you know what it actually looks like, add an example here. :smiley: - -#### Configure the Mailserver - -Then, configure both [Postfix][docs-postfix] and [Dovecot][docs-dovecot] to expect the PROXY protocol: - -??? example "HAProxy Example" - - ```yaml - kind: ConfigMap - apiVersion: v1 - metadata: - name: mailserver.config - labels: - app: mailserver - data: - postfix-main.cf: | - postscreen_upstream_proxy_protocol = haproxy - postfix-master.cf: | - smtp/inet/postscreen_upstream_proxy_protocol=haproxy - submission/inet/smtpd_upstream_proxy_protocol=haproxy - smtps/inet/smtpd_upstream_proxy_protocol=haproxy - dovecot.cf: | - # Assuming your ingress controller is bound to 10.0.0.0/8 - haproxy_trusted_networks = 10.0.0.0/8, 127.0.0.0/8 - service imap-login { - inet_listener imap { - haproxy = yes - } - inet_listener imaps { - haproxy = yes - } - } - # ... - --- - - kind: Deployment - apiVersion: extensions/v1beta1 - metadata: - name: mailserver - spec: - template: - spec: - containers: - - name: docker-mailserver - volumeMounts: - - name: config - subPath: postfix-main.cf - mountPath: /tmp/docker-mailserver/postfix-main.cf - readOnly: true - - name: config - subPath: postfix-master.cf - mountPath: /tmp/docker-mailserver/postfix-master.cf - readOnly: true - - name: config - subPath: dovecot.cf - mountPath: /tmp/docker-mailserver/dovecot.cf - readOnly: true - ``` - -With this approach, - -- it is not possible to access `docker-mailserver` via cluster-DNS, as the PROXY protocol is required for incoming connections. - -[Helm chart]: https://github.com/docker-mailserver/docker-mailserver-helm -[kustomize]: https://kustomize.io/ -[cert-manager]: https://cert-manager.io/docs/ + spec: + ipAddressPools: [ mailserver ] + ``` + + === "Host network" + + ???+ abstract "Advantages / Disadvantages" + + - [x] Simple + - [ ] Requires the node to have a dedicated, publicly routable IP address + - [ ] Limited to a single node (_associated to the dedicated IP address_) + - [ ] It is not possible to access DMS via other cluster nodes, only via the node that DMS was deployed on + - [ ] Every port within the container is exposed on the host side + + !!! example + + Using `hostPort` and `hostNetwork: true` is a similar approach to [`network_mode: host` with Docker Compose][docker-docs::compose::network_mode]. + + ```yaml + --- + apiVersion: apps/v1 + kind: Deployment + + metadata: + name: mailserver + + # ... + spec: + hostNetwork: true + # ... + containers: + # ... + ports: + - name: smtp + containerPort: 25 + hostPort: 25 + - name: submissions + containerPort: 465 + hostPort: 465 + - name: submission + containerPort: 587 + hostPort: 587 + - name: imaps + containerPort: 993 + hostPort: 993 + ``` + + === "Using the PROXY Protocol" + + ???+ abstract "Advantages / Disadvantages" + + - [x] Preserves the origin IP address of clients (_which is crucial for DNS related checks_) + - [x] Aligns with a best practice for Kubernetes by using a dedicated ingress, routing external traffic to the k8s cluster (_with the benefits of flexible routing rules_) + - [x] Avoids the restraint of a single [node][k8s-docs::nodes] (_as a workaround to preserve the original client IP_) + - [ ] Introduces complexity by requiring: + - A reverse-proxy / ingress controller (_potentially extra setup_) + - Kubernetes manifest changes for the DMS configured `Service` + - DMS configuration changes for Postfix and Dovecot + - [ ] To keep support for direct connections to DMS services internally within cluster, service ports must be "duplicated" to offer an alternative port for connections using PROXY protocol + - [ ] Custom Fail2Ban required: Because the traffic to DMS is now coming from the proxy, banning the origin IP address will have no effect; you'll need to implement a [custom solution for your setup][github-web::docker-mailserver::proxy-protocol-fail2ban]. + + ??? question "What is the PROXY protocol?" + + PROXY protocol is a network protocol for preserving a client’s IP address when the client’s TCP connection passes through a proxy. + + It is a common feature supported among reverse-proxy services (_NGINX, HAProxy, Traefik_), which you may already have handling ingress traffic for your cluster. + + ```mermaid + flowchart LR + A(External Mail Server) -->|Incoming connection| B + subgraph cluster + B("Ingress Acting as a Proxy") -->|PROXY protocol connection| C(DMS) + end + ``` + + For more information on the PROXY protocol, refer to [our dedicated docs page][docs-mailserver-behind-proxy] on the topic. + + ???+ example "Configure the Ingress Controller" + + === "Traefik" + + On Traefik's side, the configuration is very simple. + + - Create an entrypoint for each port that you want to expose (_probably 25, 465, 587 and 993_). + - Each entrypoint should configure an [`IngressRouteTCP`][traefik-docs::k8s::ingress-route-tcp] that routes to the equivalent internal DMS `Service` port which supports PROXY protocol connections. + + The below snippet demonstrates an example for two entrypoints, `submissions` (port 465) and `imaps` (port 993). + + ```yaml + --- + apiVersion: v1 + kind: Service + + metadata: + name: mailserver + + spec: + # This an optimization to get rid of additional routing steps. + # Previously "type: LoadBalancer" + type: ClusterIP + + --- + apiVersion: traefik.io/v1alpha1 + kind: IngressRouteTCP + + metadata: + name: smtp + + spec: + entryPoints: [ submissions ] + routes: + - match: HostSNI(`*`) + services: + - name: mailserver + namespace: mail + port: subs-proxy # note the 15 character limit here + proxyProtocol: + version: 2 + + --- + apiVersion: traefik.io/v1alpha1 + kind: IngressRouteTCP + + metadata: + name: imaps + + spec: + entryPoints: [ imaps ] + routes: + - match: HostSNI(`*`) + services: + - name: mailserver + namespace: mail + port: imaps-proxy + proxyProtocol: + version: 2 + ``` + + !!! info "`*-proxy` port name suffix" + + The `IngressRouteTCP` example configs above reference ports with a `*-proxy` suffix. + + - These port variants will be defined in the [`Deployment` manifest][docs::k8s::config-deployment], and are scoped to the `mailserver` service (via `spec.routes.services.name`). + - The suffix is used to distinguish that these ports are only compatible with connections using the PROXY protocol, which is what your ingress controller should be managing for you by adding the correct PROXY protocol headers to TCP connections it routes to DMS. + + === "NGINX" + + With an [NGINX ingress controller][k8s-docs::nginx], add the following to the TCP services config map (_as described [here][k8s-docs::nginx-expose]_): + + ```yaml + 25: "mailserver/mailserver:25::PROXY" + 465: "mailserver/mailserver:465::PROXY" + 587: "mailserver/mailserver:587::PROXY" + 993: "mailserver/mailserver:993::PROXY" + ``` + + ???+ example "Adjust DMS config for Dovecot + Postfix" + + ??? warning "Only ingress should connect to DMS with PROXY protocol" + + While Dovecot will restrict connections via PROXY protocol to only clients trusted configured via `haproxy_trusted_networks`, Postfix does not have an equivalent setting. Public clients should always route through ingress to establish a PROXY protocol connection. + + You are responsible for properly managing traffic inside your cluster and to **ensure that only trustworthy entities** can connect to the designated PROXY protocol ports. + + With Kubernetes, this is usually the task of the CNI (_container network interface_). + + !!! tip "Advised approach" + + The _"Separate PROXY protocol ports"_ tab below introduces a little more complexity, but provides better compatibility for internal connections to DMS. + + === "Only accept connections with PROXY protocol" + + !!! warning "Connections to DMS within the internal cluster will be rejected" + + The services for these ports can only enable PROXY protocol support by mandating the protocol on all connections for these ports. + + This can be problematic when you also need to support internal cluster traffic directly to DMS (_instead of routing indirectly through the ingress controller_). + + Here is an example configuration for [Postfix][docs-postfix], [Dovecot][docs-dovecot], and the required adjustments for the [`Deployment` manifest][docs::k8s::config-deployment]. The port names are adjusted here only to convey the additional context described earlier. + + ```yaml + kind: ConfigMap + apiVersion: v1 + metadata: + name: mailserver-extra-config + labels: + app: mailserver + data: + postfix-main.cf: | + postscreen_upstream_proxy_protocol = haproxy + postfix-master.cf: | + smtp/inet/postscreen_upstream_proxy_protocol=haproxy + submission/inet/smtpd_upstream_proxy_protocol=haproxy + submissions/inet/smtpd_upstream_proxy_protocol=haproxy + dovecot.cf: | + haproxy_trusted_networks = + service imap-login { + inet_listener imap { + haproxy = yes + } + inet_listener imaps { + haproxy = yes + } + } + # ... + + --- + kind: Deployment + apiVersion: apps/v1 + metadata: + name: mailserver + spec: + template: + spec: + containers: + - name: docker-mailserver + # ... + ports: + - name: smtp-proxy + containerPort: 25 + protocol: TCP + - name: imap-proxy + containerPort: 143 + protocol: TCP + - name: subs-proxy + containerPort: 465 + protocol: TCP + - name: sub-proxy + containerPort: 587 + protocol: TCP + - name: imaps-proxy + containerPort: 993 + protocol: TCP + # ... + volumeMounts: + - name: config + subPath: postfix-main.cf + mountPath: /tmp/docker-mailserver/postfix-main.cf + readOnly: true + - name: config + subPath: postfix-master.cf + mountPath: /tmp/docker-mailserver/postfix-master.cf + readOnly: true + - name: config + subPath: dovecot.cf + mountPath: /tmp/docker-mailserver/dovecot.cf + readOnly: true + ``` + + === "Separate PROXY protocol ports for ingress" + + !!! info + + Supporting internal cluster connections to DMS without using PROXY protocol requires both Postfix and Dovecot to be configured with alternative ports for each service port (_which only differ by enforcing PROXY protocol connections_). + + - The ingress controller will route public connections to the internal alternative ports for DMS (`*-proxy` variants). + - Internal cluster connections will instead use the original ports configured for the DMS container directly (_which are private to the cluster network_). + + In this example we'll create a copy of the original service ports with PROXY protocol enabled, and increment the port number assigned by `10000`. + + Create a `user-patches.sh` file to apply these config changes during container startup: + + ```bash + #!/bin/bash + + # Duplicate the config for the submission(s) service ports (587 / 465) with adjustments for the PROXY ports (10587 / 10465) and `syslog_name` setting: + postconf -Mf submission/inet | sed -e s/^submission/10587/ -e 's/submission/submission-proxyprotocol/' >> /etc/postfix/master.cf + postconf -Mf submissions/inet | sed -e s/^submissions/10465/ -e 's/submissions/submissions-proxyprotocol/' >> /etc/postfix/master.cf + # Enable PROXY Protocol support for these new service variants: + postconf -P 10587/inet/smtpd_upstream_proxy_protocol=haproxy + postconf -P 10465/inet/smtpd_upstream_proxy_protocol=haproxy + + # Create a variant for port 25 too (NOTE: Port 10025 is already assigned in DMS to Amavis): + postconf -Mf smtp/inet | sed -e s/^smtp/12525/ >> /etc/postfix/master.cf + # Enable PROXY Protocol support (different setting as port 25 is handled via postscreen), optionally configure a `syslog_name` to distinguish in logs: + postconf -P 12525/inet/postscreen_upstream_proxy_protocol=haproxy 12525/inet/syslog_name=smtp-proxyprotocol + ``` + + For Dovecot, you can configure [`dovecot.cf`][docs-dovecot] to look like this: + + ```cf + haproxy_trusted_networks = + + service imap-login { + inet_listener imap-proxied { + haproxy = yes + port = 10143 + } + + inet_listener imaps-proxied { + haproxy = yes + port = 10993 + ssl = yes + } + } + ``` + + Update the [`Deployment` manifest][docs::k8s::config-deployment] `ports` section by appending these new ports: + + ```yaml + - name: smtp-proxy + # not 10025 in this example due to a possible clash with Amavis + containerPort: 12525 + protocol: TCP + - name: imap-proxy + containerPort: 10143 + protocol: TCP + - name: subs-proxy + containerPort: 10465 + protocol: TCP + - name: sub-proxy + containerPort: 10587 + protocol: TCP + - name: imaps-proxy + containerPort: 10993 + protocol: TCP + ``` + + !!! note + + If you use other Dovecot ports (110, 995, 4190), you may want to configure those similar to above. The `dovecot.cf` config for these ports is [documented here][docs-mailserver-behind-proxy] (_in the equivalent section of that page_). + +[docs::k8s::config-deployment]: #deployment [docs-tls]: ../security/ssl.md [docs-dovecot]: ./override-defaults/dovecot.md [docs-postfix]: ./override-defaults/postfix.md -[dockerhub-haproxy]: https://hub.docker.com/_/haproxy -[Kubernetes-nginx]: https://kubernetes.github.io/ingress-nginx -[Kubernetes-nginx-expose]: https://kubernetes.github.io/ingress-nginx/user-guide/exposing-tcp-udp-services -[Kubernetes-network-service]: https://kubernetes.io/docs/concepts/services-networking/service -[Kubernetes-network-external-ip]: https://kubernetes.io/docs/concepts/services-networking/service/#external-ips -[Kubernetes-nodes]: https://kubernetes.io/docs/concepts/architecture/nodes -[Kubernetes-proxy-service]: https://github.com/kubernetes/contrib/tree/master/for-demos/proxy-to-service -[Kubernetes-service-source-ip]: https://kubernetes.io/docs/tutorials/services/source-ip +[docs-mailserver-behind-proxy]: ../../examples/tutorials/mailserver-behind-proxy.md + +[github-web::docker-mailserver-helm]: https://github.com/docker-mailserver/docker-mailserver-helm +[docker-docs::compose::network_mode]: https://docs.docker.com/compose/compose-file/compose-file-v3/#network_mode +[kustomize]: https://kustomize.io/ +[cert-manager]: https://cert-manager.io/docs/ +[metallb-web]: https://metallb.universe.tf/ + +[k8s-docs::config::service]: https://kubernetes.io/docs/concepts/services-networking/service +[k8s-docs::config::deployment]: https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#creating-a-deployment +[k8s-docs::nodes]: https://kubernetes.io/docs/concepts/architecture/nodes +[k8s-docs::nginx]: https://kubernetes.github.io/ingress-nginx +[k8s-docs::nginx-expose]: https://kubernetes.github.io/ingress-nginx/user-guide/exposing-tcp-udp-services +[k8s-docs::service-source-ip]: https://kubernetes.io/docs/tutorials/services/source-ip +[k8s-docs::network-external-ip]: https://kubernetes.io/docs/concepts/services-networking/service/#external-ips + +[traefik-docs::k8s::ingress-route-tcp]: https://doc.traefik.io/traefik/routing/providers/kubernetes-crd/#kind-ingressroutetcp +[github-web::docker-mailserver::proxy-protocol-fail2ban]: https://github.com/docker-mailserver/docker-mailserver/issues/1761#issuecomment-2016879319 diff --git a/docs/content/config/advanced/mail-fetchmail.md b/docs/content/config/advanced/mail-fetchmail.md index eeb8c9f49de..936d5188c9b 100644 --- a/docs/content/config/advanced/mail-fetchmail.md +++ b/docs/content/config/advanced/mail-fetchmail.md @@ -2,7 +2,7 @@ title: 'Advanced | Email Gathering with Fetchmail' --- -To enable the [fetchmail][fetchmail-website] service to retrieve e-mails set the environment variable `ENABLE_FETCHMAIL` to `1`. Your `docker-compose.yml` file should look like following snippet: +To enable the [fetchmail][fetchmail-website] service to retrieve e-mails, set the environment variable `ENABLE_FETCHMAIL` to `1`. Your `compose.yaml` file should look like following snippet: ```yaml environment: @@ -10,7 +10,7 @@ environment: - FETCHMAIL_POLL=300 ``` -Generate a file called `fetchmail.cf` and place it in the `docker-data/dms/config/` folder. Your `docker-mailserver` folder should look like this example: +Generate a file called `fetchmail.cf` and place it in the `docker-data/dms/config/` folder. Your DMS folder should look like this example: ```txt ├── docker-data/dms/config @@ -18,108 +18,135 @@ Generate a file called `fetchmail.cf` and place it in the `docker-data/dms/confi │   ├── fetchmail.cf │   ├── postfix-accounts.cf │   └── postfix-virtual.cf -├── docker-compose.yml -└── README.md +└── compose.yaml ``` ## Configuration -A detailed description of the configuration options can be found in the [online version of the manual page][fetchmail-docs]. +Configuration options for `fetchmail.cf` are covered at the [official fetchmail docs][fetchmail-docs-config] (_see the section "The run control file" and the table with "keyword" column for all settings_). -### IMAP Configuration +!!! example "Basic `fetchmail.cf` configuration" -!!! example + Retrieve mail from `remote-user@somewhere.com` and deliver it to `dms-user@example.com`: ```fetchmailrc - poll 'imap.gmail.com' proto imap - user 'username' - pass 'secret' - is 'user1@example.com' - ssl + poll 'mail.somewhere.com' + proto imap + user 'remote-user' + pass 'secret' + is 'dms-user@example.com' ``` -### POP3 Configuration + - `poll` sets the remote mail server to connect to retrieve mail from. + - `proto` lets you connect via IMAP or POP3. + - `user` and `pass` provide the login credentials for the remote mail service account to access. + - `is` configures where the fetched mail will be sent to (_eg: your local DMS account in `docker-data/dms/config/postfix-accounts.cf`_). -!!! example + --- - ```fetchmailrc - poll 'pop3.gmail.com' proto pop3 - user 'username' - pass 'secret' - is 'user2@example.com' - ssl - ``` + ??? warning "`proto imap` will still delete remote mail once fetched" -!!! caution + This is due to a separate default setting `no keep`. Adding the setting `keep` to your config on a new line will prevent deleting the remote copy. - Don’t forget the last line! (_eg: `is 'user1@example.com'`_). After `is`, you have to specify an email address from the configuration file: `docker-data/dms/config/postfix-accounts.cf`. +??? example "Multiple users or remote servers" -More details how to configure fetchmail can be found in the [fetchmail man page in the chapter “The run control file”][fetchmail-docs-run]. + The official docs [config examples][fetchmail-config-examples] show a common convention to indent settings on subsequent lines for visually grouping per server. -### Polling Interval + === "Minimal syntax" -By default the fetchmail service searches every 5 minutes for new mails on your external mail accounts. You can override this default value by changing the ENV variable `FETCHMAIL_POLL`: + ```fetchmailrc + poll 'mail.somewhere.com' proto imap + user 'john.doe' pass 'secret' is 'johnny@example.com' + user 'jane.doe' pass 'secret' is 'jane@example.com' -```yaml -environment: - - FETCHMAIL_POLL=60 -``` + poll 'mail.somewhere-else.com' proto pop3 + user 'john.doe@somewhere-else.com' pass 'secret' is 'johnny@example.com' + ``` + + === "With optional syntax" + + - `#` for adding comments. + - The config file may include "noise" keywords to improve readability. + + ```fetchmailrc + # Retrieve mail for users `john.doe` and `jane.doe` via IMAP at this remote mail server: + poll 'mail.somewhere.com' with proto imap wants: + user 'john.doe' with pass 'secret', is 'johnny@example.com' here + user 'jane.doe' with pass 'secret', is 'jane@example.com' here -You must specify a numeric argument which is a polling interval in seconds. The example above polls every minute for new mails. + # Also retrieve mail from this mail server (but via POP3). + # NOTE: This could also be all on a single line, or with each key + value as a separate line. + # Notice how the remote username includes a full email address, + # Some mail servers like DMS use the full email address as the username: + poll 'mail.somewhere-else.com' with proto pop3 wants: + user 'john.doe@somewhere-else.com' with pass 'secret', is 'johnny@example.com' here + ``` + +!!! tip "`FETCHMAIL_POLL` ENV: Override default polling interval" + + By default the fetchmail service will check every 5 minutes for new mail at the configured mail accounts. + + ```yaml + environment: + # The fetchmail polling interval in seconds: + FETCHMAIL_POLL: 60 + ``` ## Debugging -To debug your `fetchmail.cf` configuration run this command: +To debug your `fetchmail.cf` configuration run this `setup debug` command: ```sh -./setup.sh debug fetchmail +docker exec -it dms-container-name setup debug fetchmail ``` -For more informations about the configuration script `setup.sh` [read the corresponding docs][docs-setup]. - -Here a sample output of `./setup.sh debug fetchmail`: - -```log -fetchmail: 6.3.26 querying outlook.office365.com (protocol POP3) at Mon Aug 29 22:11:09 2016: poll started -Trying to connect to 132.245.48.18/995...connected. -fetchmail: Server certificate: -fetchmail: Issuer Organization: Microsoft Corporation -fetchmail: Issuer CommonName: Microsoft IT SSL SHA2 -fetchmail: Subject CommonName: outlook.com -fetchmail: Subject Alternative Name: outlook.com -fetchmail: Subject Alternative Name: *.outlook.com -fetchmail: Subject Alternative Name: office365.com -fetchmail: Subject Alternative Name: *.office365.com -fetchmail: Subject Alternative Name: *.live.com -fetchmail: Subject Alternative Name: *.internal.outlook.com -fetchmail: Subject Alternative Name: *.outlook.office365.com -fetchmail: Subject Alternative Name: outlook.office.com -fetchmail: Subject Alternative Name: attachment.outlook.office.net -fetchmail: Subject Alternative Name: attachment.outlook.officeppe.net -fetchmail: Subject Alternative Name: *.office.com -fetchmail: outlook.office365.com key fingerprint: 3A:A4:58:42:56:CD:BD:11:19:5B:CF:1E:85:16:8E:4D -fetchmail: POP3< +OK The Microsoft Exchange POP3 service is ready. [SABFADEAUABSADAAMQBDAEEAMAAwADAANwAuAGUAdQByAHAAcgBkADAAMQAuAHAAcgBvAGQALgBlAHgAYwBoAGEAbgBnAGUAbABhAGIAcwAuAGMAbwBtAA==] -fetchmail: POP3> CAPA -fetchmail: POP3< +OK -fetchmail: POP3< TOP -fetchmail: POP3< UIDL -fetchmail: POP3< SASL PLAIN -fetchmail: POP3< USER -fetchmail: POP3< . -fetchmail: POP3> USER user1@outlook.com -fetchmail: POP3< +OK -fetchmail: POP3> PASS * -fetchmail: POP3< +OK User successfully logged on. -fetchmail: POP3> STAT -fetchmail: POP3< +OK 0 0 -fetchmail: No mail for user1@outlook.com at outlook.office365.com -fetchmail: POP3> QUIT -fetchmail: POP3< +OK Microsoft Exchange Server 2016 POP3 server signing off. -fetchmail: 6.3.26 querying outlook.office365.com (protocol POP3) at Mon Aug 29 22:11:11 2016: poll completed -fetchmail: normal termination, status 1 -``` +??? example "Sample output of `setup debug fetchmail`" + + ```log + fetchmail: 6.3.26 querying outlook.office365.com (protocol POP3) at Mon Aug 29 22:11:09 2016: poll started + Trying to connect to 132.245.48.18/995...connected. + fetchmail: Server certificate: + fetchmail: Issuer Organization: Microsoft Corporation + fetchmail: Issuer CommonName: Microsoft IT SSL SHA2 + fetchmail: Subject CommonName: outlook.com + fetchmail: Subject Alternative Name: outlook.com + fetchmail: Subject Alternative Name: *.outlook.com + fetchmail: Subject Alternative Name: office365.com + fetchmail: Subject Alternative Name: *.office365.com + fetchmail: Subject Alternative Name: *.live.com + fetchmail: Subject Alternative Name: *.internal.outlook.com + fetchmail: Subject Alternative Name: *.outlook.office365.com + fetchmail: Subject Alternative Name: outlook.office.com + fetchmail: Subject Alternative Name: attachment.outlook.office.net + fetchmail: Subject Alternative Name: attachment.outlook.officeppe.net + fetchmail: Subject Alternative Name: *.office.com + fetchmail: outlook.office365.com key fingerprint: 3A:A4:58:42:56:CD:BD:11:19:5B:CF:1E:85:16:8E:4D + fetchmail: POP3< +OK The Microsoft Exchange POP3 service is ready. [SABFADEAUABSADAAMQBDAEEAMAAwADAANwAuAGUAdQByAHAAcgBkADAAMQAuAHAAcgBvAGQALgBlAHgAYwBoAGEAbgBnAGUAbABhAGIAcwAuAGMAbwBtAA==] + fetchmail: POP3> CAPA + fetchmail: POP3< +OK + fetchmail: POP3< TOP + fetchmail: POP3< UIDL + fetchmail: POP3< SASL PLAIN + fetchmail: POP3< USER + fetchmail: POP3< . + fetchmail: POP3> USER user1@outlook.com + fetchmail: POP3< +OK + fetchmail: POP3> PASS * + fetchmail: POP3< +OK User successfully logged on. + fetchmail: POP3> STAT + fetchmail: POP3< +OK 0 0 + fetchmail: No mail for user1@outlook.com at outlook.office365.com + fetchmail: POP3> QUIT + fetchmail: POP3< +OK Microsoft Exchange Server 2016 POP3 server signing off. + fetchmail: 6.3.26 querying outlook.office365.com (protocol POP3) at Mon Aug 29 22:11:11 2016: poll completed + fetchmail: normal termination, status 1 + ``` + +!!! tip "Troubleshoot with this reference `compose.yaml`" + + [A minimal `compose.yaml` example][fetchmail-compose-example] demonstrates how to run two instances of DMS locally, with one instance configured with `fetchmail.cf` and the other to simulate a remote mail server to fetch from. -[docs-setup]: ../../config/setup.sh.md [fetchmail-website]: https://www.fetchmail.info -[fetchmail-docs]: https://www.fetchmail.info/fetchmail-man.html -[fetchmail-docs-run]: https://www.fetchmail.info/fetchmail-man.html#31 +[fetchmail-docs-config]: https://www.fetchmail.info/fetchmail-man.html#the-run-control-file +[fetchmail-config-examples]: https://www.fetchmail.info/fetchmail-man.html#configuration-examples +[fetchmail-compose-example]: https://github.com/orgs/docker-mailserver/discussions/3994#discussioncomment-9290570 diff --git a/docs/content/config/advanced/mail-forwarding/aws-ses.md b/docs/content/config/advanced/mail-forwarding/aws-ses.md index 891295b450b..00ca2a13908 100644 --- a/docs/content/config/advanced/mail-forwarding/aws-ses.md +++ b/docs/content/config/advanced/mail-forwarding/aws-ses.md @@ -2,29 +2,46 @@ title: 'Mail Forwarding | AWS SES' --- -[Amazon SES (Simple Email Service)](https://aws.amazon.com/ses/) is intended to provide a simple way for cloud based applications to send email and receive email. For the purposes of this project only sending email via SES is supported. Older versions of docker-mailserver used `AWS_SES_HOST` and `AWS_SES_USERPASS` to configure sending, this has changed and the setup is mananged through [Configure Relay Hosts][docs-relay]. +[Amazon SES (Simple Email Service)][aws-ses] provides a simple way for cloud based applications to send and receive email. -You will need to create some [Amazon SES SMTP credentials](https://docs.aws.amazon.com/ses/latest/DeveloperGuide/smtp-credentials.html). The SMTP credentials you create will be used to populate the `RELAY_USER` and `RELAY_PASSWORD` environment variables. +!!! example "Configuration via ENV" -The `RELAY_HOST` should match your [AWS SES region](https://docs.aws.amazon.com/general/latest/gr/ses.html), the `RELAY_PORT` will be 587. + [Configure a relay host in DMS][docs::relay] to forward all your mail through AWS SES: -If all of your email is being forwarded through AWS SES, `DEFAULT_RELAY_HOST` should be set accordingly. + - `RELAY_HOST` should match your [AWS SES region][aws-ses::region]. + - `RELAY_PORT` should be set to [one of the supported AWS SES SMTP ports][aws-ses::smtp-ports] (_eg: 587 for STARTTLS_). + - `RELAY_USER` and `RELAY_PASSWORD` should be set to your [Amazon SES SMTP credentials][aws-ses::credentials]. -Example: -``` -DEFAULT_RELAY_HOST=[email-smtp.us-west-2.amazonaws.com]:587 -``` + ```env + RELAY_HOST=email-smtp.us-west-2.amazonaws.com + RELAY_PORT=587 + # Alternative to RELAY_HOST + RELAY_PORT which is compatible with LDAP: + DEFAULT_RELAY_HOST=[email-smtp.us-west-2.amazonaws.com]:587 -!!! note - If you set up [AWS Easy DKIM](https://docs.aws.amazon.com/ses/latest/DeveloperGuide/send-email-authentication-dkim-easy.html) you can safely skip setting up DKIM as the AWS SES will take care of signing your outgoing email. + RELAY_USER=aws-user + RELAY_PASSWORD=secret + ``` -To verify proper operation, send an email to some external account of yours and inspect the mail headers. You will also see the connection to SES in the mail logs. For example: +!!! tip -```log -May 23 07:09:36 mail postfix/smtp[692]: Trusted TLS connection established to email-smtp.us-east-1.amazonaws.com[107.20.142.169]:25: -TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits) -May 23 07:09:36 mail postfix/smtp[692]: 8C82A7E7: to=, relay=email-smtp.us-east-1.amazonaws.com[107.20.142.169]:25, -delay=0.35, delays=0/0.02/0.13/0.2, dsn=2.0.0, status=sent (250 Ok 01000154dc729264-93fdd7ea-f039-43d6-91ed-653e8547867c-000000) -``` + If you have set up [AWS Easy DKIM][aws-ses::easy-dkim], you can safely skip setting up DKIM as AWS SES will take care of signing your outbound mail. -[docs-relay]: ./relay-hosts.md +!!! note "Verify the relay host is configured correctly" + + To verify proper operation, send an email to some external account of yours and inspect the mail headers. + + You will also see the connection to SES in the mail logs: + + ```log + postfix/smtp[692]: Trusted TLS connection established to email-smtp.us-west-1.amazonaws.com[107.20.142.169]:25: + TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits) + postfix/smtp[692]: 8C82A7E7: to=, relay=email-smtp.us-west-1.amazonaws.com[107.20.142.169]:25, + delay=0.35, delays=0/0.02/0.13/0.2, dsn=2.0.0, status=sent (250 Ok 01000154dc729264-93fdd7ea-f039-43d6-91ed-653e8547867c-000000) + ``` + +[docs::relay]: ./relay-hosts.md +[aws-ses]: https://aws.amazon.com/ses/ +[aws-ses::credentials]: https://docs.aws.amazon.com/ses/latest/dg/smtp-credentials.html +[aws-ses::smtp-ports]: https://docs.aws.amazon.com/ses/latest/dg/smtp-connect.html +[aws-ses::region]: https://docs.aws.amazon.com/general/latest/gr/ses.html +[aws-ses::easy-dkim]: https://docs.aws.amazon.com/ses/latest/DeveloperGuide/send-email-authentication-dkim-easy.html diff --git a/docs/content/config/advanced/mail-forwarding/gmail-smtp.md b/docs/content/config/advanced/mail-forwarding/gmail-smtp.md new file mode 100644 index 00000000000..a581acd17ed --- /dev/null +++ b/docs/content/config/advanced/mail-forwarding/gmail-smtp.md @@ -0,0 +1,50 @@ +--- +title: 'Mail Forwarding | Configure Gmail as a relay host' +--- + +This page provides a guide for configuring DMS to use [GMAIL as an SMTP relay host][gmail-smtp]. + +!!! example "Configuration via ENV" + + [Configure a relay host in DMS][docs::relay]. This example shows how the related ENV settings map to the Gmail service config: + + - `RELAY_HOST` should be configured as [advised by Gmail][gmail-smtp::relay-host], there are two SMTP endpoints to choose: + - `smtp.gmail.com` (_for a personal Gmail account_) + - `smtp-relay.gmail.com` (_when using Google Workspace_) + - `RELAY_PORT` should be set to [one of the supported Gmail SMTP ports][gmail-smtp::relay-port] (_eg: 587 for STARTTLS_). + - `RELAY_USER` should be your gmail address (`user@gmail.com`). + - `RELAY_PASSWORD` should be your [App Password][gmail-smtp::app-password], **not** your personal gmail account password. + + ```env + RELAY_HOST=smtp.gmail.com + RELAY_PORT=587 + # Alternative to RELAY_HOST + RELAY_PORT which is compatible with LDAP: + DEFAULT_RELAY_HOST=[smtp.gmail.com]:587 + + RELAY_USER=username@gmail.com + RELAY_PASSWORD=secret + ``` + +!!! tip + + - As per our main [relay host docs page][docs::relay], you may prefer to configure your credentials via `setup relay add-auth` instead of the `RELAY_USER` + `RELAY_PASSWORD` ENV. + - If you configure for `smtp-relay.gmail.com`, the `DEFAULT_RELAY_HOST` ENV should be all you need as shown in the above example. Credentials can be optional when using Google Workspace (`smtp-relay.gmail.com`), which supports restricting connections to trusted IP addresses. + +!!! note "Verify the relay host is configured correctly" + + To verify proper operation, send an email to an external account of yours and inspect the mail headers. + + You will also see the connection to the Gmail relay host (`smtp.gmail.com`) in the mail logs: + + ```log + postfix/smtp[910]: Trusted TLS connection established to smtp.gmail.com[64.233.188.109]:587: + TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) + postfix/smtp[910]: 4BCB547D9D: to=, relay=smtp.gmail.com[64.233.188.109]:587, + delay=2.9, delays=0.01/0.02/1.7/1.2, dsn=2.0.0, status=sent (250 2.0.0 OK 17... - gsmtp) + ``` + +[docs::relay]: ./relay-hosts.md +[gmail-smtp]: https://support.google.com/a/answer/2956491 +[gmail-smtp::relay-host]: https://support.google.com/a/answer/176600 +[gmail-smtp::relay-port]: https://support.google.com/a/answer/2956491 +[gmail-smtp::app-password]: https://support.google.com/accounts/answer/185833 diff --git a/docs/content/config/advanced/mail-forwarding/relay-hosts.md b/docs/content/config/advanced/mail-forwarding/relay-hosts.md index db2528e5ba3..3d13e93b81b 100644 --- a/docs/content/config/advanced/mail-forwarding/relay-hosts.md +++ b/docs/content/config/advanced/mail-forwarding/relay-hosts.md @@ -2,82 +2,155 @@ title: 'Mail Forwarding | Relay Hosts' --- -## Introduction +## What is a Relay Host? -Rather than having Postfix deliver mail directly, you can configure Postfix to send mail via another mail relay (smarthost). Examples include [Mailgun](https://www.mailgun.com/), [Sendgrid](https://sendgrid.com/) and [AWS SES](https://aws.amazon.com/ses/). +An SMTP relay service (_aka relay host / [smarthost][wikipedia::smarthost]_) is an MTA that relays (_forwards_) mail on behalf of third-parties (_it does not manage the mail domains_). -Depending on the domain of the sender, you may want to send via a different relay, or authenticate in a different way. +- Instead of DMS handling SMTP delivery directly itself (_via Postfix_), it can be configured to delegate delivery by sending all outbound mail through a relay service. +- Examples of popular mail relay services: [AWS SES][smarthost::aws-ses], [Mailgun][smarthost::mailgun], [Mailjet][smarthost::mailjet], [SendGrid][smarthost::sendgrid] -## Basic Configuration +!!! info "When can a relay service can be helpful?" -Basic configuration is done via environment variables: + - Your network provider has blocked outbound connections on port 25 (_required for direct delivery_). + - To improve delivery success via better established reputation (trust) of a relay service. -- `RELAY_HOST`: _default host to relay mail through, `empty` (aka '', or no ENV set) will disable this feature_ -- `RELAY_PORT`: _port on default relay, defaults to port 25_ -- `RELAY_USER`: _username for the default relay_ -- `RELAY_PASSWORD`: _password for the default user_ +## Configuration -Setting these environment variables will cause mail for all sender domains to be routed via the specified host, authenticating with the user/password combination. +All mail sent outbound from DMS (_where the sender address is a DMS account or a virtual alias_) will be relayed through the configured relay host. -!!! warning - For users of the previous `AWS_SES_*` variables: please update your configuration to use these new variables, no other configuration is required. +!!! info "Configuration via ENV" -## Advanced Configuration + Configure the default relayhost with either of these ENV: -### Sender-dependent Authentication + - Preferable (_LDAP compatible_): `DEFAULT_RELAY_HOST` (eg: `[mail.relay-service.com]:25`) + - `RELAY_HOST` (eg: `mail.relay-service.com`) + `RELAY_PORT` (default: 25) -Sender dependent authentication is done in `docker-data/dms/config/postfix-sasl-password.cf`. You can create this file manually, or use: + Most relay services also require authentication configured: -```sh -setup.sh relay add-auth [] -``` + - `RELAY_USER` + `RELAY_PASSWORD` provides credentials for authenticating with the default relayhost. -An example configuration file looks like this: + !!! warning "Providing secrets via ENV" -```txt -@domain1.com relay_user_1:password_1 -@domain2.com relay_user_2:password_2 -``` + While ENV is convenient, the risk of exposing secrets is higher. -If there is no other configuration, this will cause Postfix to deliver email through the relay specified in `RELAY_HOST` env variable, authenticating as `relay_user_1` when sent from `domain1.com` and authenticating as `relay_user_2` when sending from `domain2.com`. + `setup relay add-auth` is a better alternative, which manages the credentials via a config file. -!!! note - To activate the configuration you must either restart the container, or you can also trigger an update by modifying a mail account. +??? tip "Excluding specific sender domains from relay" -### Sender-dependent Relay Host + You can opt-out with: `setup relay exclude-domain ` -Sender dependent relay hosts are configured in `docker-data/dms/config/postfix-relaymap.cf`. You can create this file manually, or use: + Outbound mail from senders of that domain will be sent normally (_instead of through the configured `RELAY_HOST`_). -```sh -setup.sh relay add-domain [] -``` + !!! warning "When any relay host credentials are configured" -An example configuration file looks like this: + It will still be expected that mail is sent over a secure connection with credentials provided. -```txt -@domain1.com [relay1.org]:587 -@domain2.com [relay2.org]:2525 -``` + Thus this opt-out feature is rarely practical. -Combined with the previous configuration in `docker-data/dms/config/postfix-sasl-password.cf`, this will cause Postfix to deliver mail sent from `domain1.com` via `relay1.org:587`, authenticating as `relay_user_1`, and mail sent from `domain2.com` via `relay2.org:2525` authenticating as `relay_user_2`. +### Advanced Configuration -!!! note - You still have to define `RELAY_HOST` to activate the feature +When mail is sent, there is support to change the relay service or the credentials configured based on the sender address domain used. -### Excluding Sender Domains +We provide this support via two config files: -If you want mail sent from some domains to be delivered directly, you can exclude them from being delivered via the default relay by adding them to `docker-data/dms/config/postfix-relaymap.cf` with no destination. You can also do this via: +- Sender-dependent Relay Host: `docker-data/dms/config/postfix-relaymap.cf` +- Sender-dependent Authentication: `docker-data/dms/config/postfix-sasl-password.cf` -```sh -setup.sh relay exclude-domain -``` +!!! tip "Configure with our `setup relay` commands" -Extending the configuration file from above: + While you can edit those configs directly, DMS provides these helpful config management commands: -```txt -@domain1.com [relay1.org]:587 -@domain2.com [relay2.org]:2525 -@domain3.com -``` + ```cli-syntax + # Configure a sender domain to use a specific relay host: + setup relay add-domain [] -This will cause email sent from `domain3.com` to be delivered directly. + # Configure relay host credentials for a sender domain to use: + setup relay add-auth [] + + # Optionally avoid relaying from senders of this domain: + # NOTE: Only supported when configured with the `RELAY_HOST` ENV! + setup relay exclude-domain + ``` + +!!! example "Config file: `postfix-sasl-password.cf`" + + ```cf-extra title="docker-data/dms/config/postfix-sasl-password.cf" + @domain1.com mailgun-user:secret + @domain2.com sendgrid-user:secret + + # NOTE: This must have an exact match with the relay host in `postfix-relaymap.cf`, + # `/etc/postfix/relayhost_map`, or the `DEFAULT_RELAY_HOST` ENV. + # NOTE: Not supported via our setup CLI, but valid config for Postfix. + [email-smtp.us-west-2.amazonaws.com]:2587 aws-user:secret + ``` + + When Postfix needs to lookup credentials for mail sent outbound, the above config will: + + - Authenticate as `mailgun-user` for mail sent with a sender belonging to `@domain1.com` + - Authenticate as `sendgrid-user` for mail sent with a sender belonging to `@domain2.com` + - Authenticate as `aws-user` for mail sent through a configured AWS SES relay host (any sender domain). + +!!! example "Config file: `postfix-relaymap.cf`" + + ```cf-extra title="docker-data/dms/config/postfix-relaymap.cf" + @domain1.com [smtp.mailgun.org]:587 + @domain2.com [smtp.sendgrid.net]:2525 + + # Opt-out of relaying: + @domain3.com + ``` + + When Postfix sends mail outbound from these sender domains, the above config will: + + - Relay mail through `[smtp.mailgun.org]:587` when mail is sent from a sender of `@domain1.com` + - Relay mail through `[smtp.sendgrid.net]:2525` when mail is sent from a sender of `@domain1.com` + - Mail with a sender from `@domain3.com` is not sent through a relay (_**Only applicable** when using `RELAY_HOST`_) + +### Technical Details + +- Both the supported ENV and config files for this feature have additional details covered in our ENV docs [Relay Host section][docs::env-relay]. +- For troubleshooting, a [minimal `compose.yaml` config with several DMS instances][dms-gh::relay-example] demonstrates this feature for local testing. +- [Subscribe to this tracking issue][dms-gh::pr-3607] for future improvements intended for this feature. + +!!! abstract "Postfix Settings" + + Internally this feature is implemented in DMS by [`relay.sh`][dms-repo::helpers-relay]. + + The `relay.sh` script manages configuring these Postfix settings: + + ```cf-extra + # Send all outbound mail through this relay service: + relayhost = [smtp.relay-service.com]:587 + + # Credentials to use: + smtp_sasl_password_maps = texthash:/etc/postfix/sasl_passwd + # Alternative table type examples which do not require a separate file: + #smtp_sasl_password_maps = static:john.doe@relay-service.com:secret + #smtp_sasl_password_maps = inline:{ [smtp.relay-service.com]:587=john.doe@relay-service.com:secret } + + ## Authentication support: + # Required to provide credentials to the relay service: + smtp_sasl_auth_enable = yes + # Enforces requiring credentials when sending mail outbound: + smtp_sasl_security_options = noanonymous + # Enforces a secure connection (TLS required) to the relay service: + smtp_tls_security_level = encrypt + + ## Support for advanced requirements: + # Relay service(s) to use instead of direct delivery for specific sender domains: + sender_dependent_relayhost_maps = texthash:/etc/postfix/relayhost_map + # Support credentials to a relay service(s) that vary by relay host used or sender domain: + smtp_sender_dependent_authentication = yes + ``` + + +[smarthost::mailgun]: https://www.mailgun.com/ +[smarthost::mailjet]: https://www.mailjet.com +[smarthost::sendgrid]: https://sendgrid.com/ +[smarthost::aws-ses]: https://aws.amazon.com/ses/ +[wikipedia::smarthost]: https://en.wikipedia.org/wiki/Smart_host + +[docs::env-relay]: ../../environment.md#relay-host +[dms-repo::helpers-relay]: https://github.com/docker-mailserver/docker-mailserver/blob/v15.0.0/target/scripts/helpers/relay.sh +[dms-gh::pr-3607]: https://github.com/docker-mailserver/docker-mailserver/issues/3607 +[dms-gh::relay-example]: https://github.com/docker-mailserver/docker-mailserver/issues/3842#issuecomment-1913380639 diff --git a/docs/content/config/advanced/mail-getmail.md b/docs/content/config/advanced/mail-getmail.md new file mode 100644 index 00000000000..90deebe43e3 --- /dev/null +++ b/docs/content/config/advanced/mail-getmail.md @@ -0,0 +1,118 @@ +--- +title: 'Advanced | Email Gathering with Getmail' +--- + +To enable the [getmail][getmail-website] service to retrieve e-mails set the environment variable `ENABLE_GETMAIL` to `1`. Your `compose.yaml` file should include the following: + +```yaml +environment: + - ENABLE_GETMAIL=1 + - GETMAIL_POLL=5 +``` + +In your DMS config volume (eg: `docker-data/dms/config/`), add a subdirectory `getmail/` for including your getmail config files (eg: `imap-example.cf`) for each remote account that you want to retrieve mail from and deliver to the mailbox of a DMS account. + +The content of these config files is documented in the next section with an IMAP and POP3 example to reference. + +The directory structure should look similar to this: + +```txt +├── docker-data/dms/config +│   ├── dovecot.cf +│ ├── getmail +│   │ ├── getmailrc_general.cf +│   │ ├── remote-account1.cf +│   │ ├── remote-account2.cf +│   ├── postfix-accounts.cf +│   └── postfix-virtual.cf +├── docker-compose.yml +└── README.md +``` + +## Configuration + +A detailed description of the configuration options can be found in the [online version of the manual page][getmail-docs]. + +### Common Options + +The default options added to each `getmail` config are: + +```getmailrc +[options] +verbose = 0 +read_all = false +delete = false +max_messages_per_session = 500 +received = false +delivered_to = false +``` + +The DMS integration for Getmail generates a `getmailrc` config that prepends the common options of the base config to each remote account config file (`*.cf`) found in the DMS Config Volume `getmail/` directory. + +!!! tip "Change the base options" + + Add your own base config as `getmail/getmailrc_general.cf` into the DMS Config Volume. It will replace the DMS defaults shown above. + +??? example "IMAP Configuration" + + This example will: + + 1. Connect to the remote IMAP server from Gmail. + 2. Retrieve mail from the gmail account `alice` with password `notsecure`. + 3. Store any mail retrieved from the remote mail-server into DMS for the `user1@example.com` account that DMS manages. + + ```getmailrc + [retriever] + type = SimpleIMAPSSLRetriever + server = imap.gmail.com + username = alice + password = notsecure + [destination] + type = MDA_external + path = /usr/lib/dovecot/deliver + allow_root_commands = true + arguments =("-d","user1@example.com") + ``` + +??? example "POP3 Configuration" + + Just like the IMAP example above, but instead via POP3 protocol if you prefer that over IMAP. + + ```getmailrc + [retriever] + type = SimplePOP3SSLRetriever + server = pop3.gmail.com + username = alice + password = notsecure + [destination] + type = MDA_external + path = /usr/lib/dovecot/deliver + allow_root_commands = true + arguments =("-d","user1@example.com") + ``` + +### Polling Interval + +By default the `getmail` service checks external mail accounts for new mail every 5 minutes. That polling interval is configurable via the `GETMAIL_POLL` ENV variable, with a value in minutes (_default: 5, min: 1_): + +```yaml +environment: + - GETMAIL_POLL=1 +``` + +### XOAUTH2 Authentication + +It is possible to utilize the `getmail-gmail-xoauth-tokens` helper to provide authentication using `xoauth2` for [gmail (example 12)][getmail-docs-xoauth-12] or [Microsoft Office 365 (example 13)][getmail-docs-xoauth-13] + +[getmail-website]: https://www.getmail6.org +[getmail-docs]: https://getmail6.org/configuration.html +[getmail-docs-xoauth-12]: https://github.com/getmail6/getmail6/blob/v6.19.10/docs/getmailrc-examples#L286 +[getmail-docs-xoauth-13]: https://github.com/getmail6/getmail6/blob/v6.19.10/docs/getmailrc-examples#L351 + +## Debugging + +To debug your `getmail` configurations, run this `setup debug` command: + +```sh +docker exec -it dms-container-name setup debug getmail +``` diff --git a/docs/content/config/advanced/mail-sieve.md b/docs/content/config/advanced/mail-sieve.md index f6577329e6f..11197a6f6ac 100644 --- a/docs/content/config/advanced/mail-sieve.md +++ b/docs/content/config/advanced/mail-sieve.md @@ -4,16 +4,24 @@ title: 'Advanced | Email Filtering with Sieve' ## User-Defined Sieve Filters -[Sieve](http://sieve.info/) allows to specify filtering rules for incoming emails that allow for example sorting mails into different folders depending on the title of an email. -There are global and user specific filters which are filtering the incoming emails in the following order: +!!! warning "Advice may be outdated" -- Global-before -> User specific -> Global-after + This section was contributed by the community some time ago and some configuration examples may be outdated. + +[Sieve][sieve-info] allows to specify filtering rules for incoming emails that allow for example sorting mails into different folders depending on the title of an email. + +!!! info "Global vs User order" + + There are global and user specific filters which are filtering the incoming emails in the following order: + + Global-before -> User specific -> Global-after Global filters are applied to EVERY incoming mail for EVERY email address. -To specify a global Sieve filter provide a `docker-data/dms/config/before.dovecot.sieve` or a `docker-data/dms/config/after.dovecot.sieve` file with your filter rules. -If any filter in this filtering chain discards an incoming mail, the delivery process will stop as well and the mail will not reach any following filters(e.g. global-before stops an incoming spam mail: The mail will get discarded and a user-specific filter won't get applied.) -To specify a user-defined Sieve filter place a `.dovecot.sieve` file into a virtual user's mail folder e.g. `/var/mail/example.com/user1/.dovecot.sieve`. If this file exists dovecot will apply the filtering rules. +- To specify a global Sieve filter provide a `docker-data/dms/config/before.dovecot.sieve` or a `docker-data/dms/config/after.dovecot.sieve` file with your filter rules. +- If any filter in this filtering chain discards an incoming mail, the delivery process will stop as well and the mail will not reach any following filters (e.g. global-before stops an incoming spam mail: The mail will get discarded and a user-specific filter won't get applied.) + +To specify a user-defined Sieve filter place a `.dovecot.sieve` file into a virtual user's mail folder (e.g. `/var/mail/example.com/user1/home/.dovecot.sieve`). If this file exists dovecot will apply the filtering rules. It's even possible to install a user provided Sieve filter at startup during users setup: simply include a Sieve file in the `docker-data/dms/config/` path for each user login that needs a filter. The file name provided should be in the form `.dovecot.sieve`, so for example for `user1@example.com` you should provide a Sieve file named `docker-data/dms/config/user1@example.com.dovecot.sieve`. @@ -32,6 +40,7 @@ An example of a sieve filter that moves mails to a folder `INBOX/spam` depending ``` !!! warning + That folders have to exist beforehand if sieve should move them. Another example of a sieve filter that forward mails to a different address: @@ -52,31 +61,96 @@ Just forward all incoming emails and do not save them locally: redirect "user2@not-example.com"; ``` -You can also use external programs to filter or pipe (process) messages by adding executable scripts in `docker-data/dms/config/sieve-pipe` or `docker-data/dms/config/sieve-filter`. This can be used in lieu of a local alias file, for instance to forward an email to a webservice. These programs can then be referenced by filename, by all users. Note that the process running the scripts run as a privileged user. For further information see [Dovecot's wiki](https://wiki.dovecot.org/Pigeonhole/Sieve/Plugins/Pipe). +You can also use external programs to filter or pipe (process) messages by adding executable scripts in `docker-data/dms/config/sieve-pipe` or `docker-data/dms/config/sieve-filter`. + +This can be used in lieu of a local alias file, for instance to forward an email to a webservice. + +- These programs can then be referenced by filename, by all users. +- Note that the process running the scripts run as a privileged user. +- For further information see [Dovecot's docs][dovecot-docs::sieve-pipe]. ```sieve require ["vnd.dovecot.pipe"]; pipe "external-program"; ``` -For more examples or a detailed description of the Sieve language have a look at [the official site](http://sieve.info/examplescripts). Other resources are available on the internet where you can find several [examples](https://support.tigertech.net/sieve#sieve-example-rules-jmp). +For more examples or a detailed description of the Sieve language have a look at [the official site][sieve-info::examples]. Other resources are available on the internet where you can find several [examples][third-party::sieve-examples]. -## Automatic Sorting Based on Subaddresses +[dovecot-docs::sieve-pipe]: https://doc.dovecot.org/configuration_manual/sieve/plugins/extprograms/#pigeonhole-plugin-extprograms +[sieve-info]: http://sieve.info/ +[sieve-info::examples]: http://sieve.info/examplescripts +[third-party::sieve-examples]: https://support.tigertech.net/sieve#sieve-example-rules-jmp -It is possible to sort subaddresses such as `user+mailing-lists@example.com` into a corresponding folder (here: `INBOX/Mailing-lists`) automatically. +## Automatic Sorting Based on Sub-addresses { #subaddress-mailbox-routing } -```sieve -require ["envelope", "fileinto", "mailbox", "subaddress", "variables"]; - -if envelope :detail :matches "to" "*" { - set :lower :upperfirst "tag" "${1}"; - if mailboxexists "INBOX.${1}" { - fileinto "INBOX.${1}"; - } else { - fileinto :create "INBOX.${tag}"; - } -} -``` +When mail is delivered to your account, it is possible to organize storing mail into folders by the [subaddress (tag)][docs::accounts-subaddressing] used. + +!!! example "Example: `user+@example.com` to `INBOX/`" + + This example sorts mail into inbox folders by their tag: + + ```sieve title="docker-data/dms/config/user@example.com.dovecot.sieve" + require ["envelope", "fileinto", "mailbox", "subaddress", "variables"]; + + # Check if the mail recipient address has a tag (:detail) + if envelope :detail :matches "to" "*" { + # Create a variable `tag`, with the captured `to` value normalized (SoCIAL => Social) + set :lower :upperfirst "tag" "${1}"; + + # Store the mail into a folder with the tag name, nested under your inbox folder: + if mailboxexists "INBOX.${tag}" { + fileinto "INBOX.${tag}"; + } else { + fileinto :create "INBOX.${tag}"; + } + } + ``` + + When receiving mail for `user+social@example.com` it would be delivered into the `INBOX/Social` folder. + +??? tip "Only redirect mail for specific tags" + + If you want to only handle specific tags, you could replace the envelope condition and tag assignment from the prior example with: + + ```sieve title="docker-data/dms/config/user@example.com.dovecot.sieve" + # Instead of `:matches`, use the default comparator `:is` (exact match) + if envelope :detail "to" "social" { + set "tag" "Social"; + ``` + + ```sieve title="docker-data/dms/config/user@example.com.dovecot.sieve" + # Alternatively you can also provide a list of values to match: + if envelope :detail "to" ["azure", "aws"] { + set "tag" "Cloud"; + ``` + + ```sieve title="docker-data/dms/config/user@example.com.dovecot.sieve" + # Similar to `:matches`, except `:regex` provides enhanced pattern matching. + # NOTE: This example needs you to `require` the "regex" extension + if envelope :detail :regex "to" "^cloud-(azure|aws)$" { + # Normalize the captured azure/aws tag as the resolved value is no longer fixed: + set :lower :upperfirst "vendor" "${1}"; + # If a `.` exists in the tag, it will create nested folders: + set "tag" "Cloud.${vendor}"; + ``` + + **NOTE:** There is no need to lowercase the tag in the conditional as the [`to` value is a case-insensitive check][sieve-docs::envelope]. + +??? abstract "Technical Details" + + - Dovecot supports this feature via the _Sieve subaddress extension_ ([RFC 5233][rfc::5233::sieve-subaddress]). + - Only a single tag per subaddress is supported. Any additional tag delimiters are part of the tag value itself. + - The Dovecot setting [`recipient_delimiter`][dovecot-docs::config::recipient_delimiter] (default: `+`) configures the tag delimiter. This is where the `local-part` of the recipient address will split at, providing the `:detail` (tag) value for Sieve. + + --- + + `INBOX` is the [default namespace configured by Dovecot][dovecot-docs::namespace]. + + - If you omit the `INBOX.` prefix from the sieve script above, the mailbox (folder) for that tag is created at the top-level alongside your Trash and Junk folders. + - The `.` between `INBOX` and `${tag}` is important as a [separator to distinguish mailbox names][dovecot-docs::mailbox-names]. This can vary by mailbox format or configuration. DMS uses [`Maildir`][dovecot-docs::mailbox-formats::maildir] by default, which uses `.` as the separator. + - [`lmtp_save_to_detail_mailbox = yes`][dovecot-docs::config::lmtp_save_to_detail_mailbox] can be set in `/etc/dovecot/conf.d/20-lmtp.conf`: + - This implements the feature globally, except for the tag normalization and `INBOX.` prefix parts of the example script. + - However, if the sieve script is also present, the script has precedence and will handle this task instead when the condition is successful, otherwise falling back to the global feature. ## Manage Sieve @@ -84,20 +158,31 @@ The [Manage Sieve](https://doc.dovecot.org/admin_manual/pigeonhole_managesieve_s !!! example - ```yaml - # docker-compose.yml + ```yaml title="compose.yaml" ports: - "4190:4190" environment: - ENABLE_MANAGESIEVE=1 ``` -All user defined sieve scripts that are managed by ManageSieve are stored in the user's home folder in `/var/mail/example.com/user1/sieve`. Just one sieve script might be active for a user and is sym-linked to `/var/mail/example.com/user1/.dovecot.sieve` automatically. +All user defined sieve scripts that are managed by ManageSieve are stored in the user's home folder in `/var/mail/example.com/user1/home/sieve`. Just one Sieve script might be active for a user and is sym-linked to `/var/mail/example.com/user1/home/.dovecot.sieve` automatically. !!! note - ManageSieve makes sure to not overwrite an existing `.dovecot.sieve` file. If a user activates a new sieve script the old one is backuped and moved to the `sieve` folder. + + ManageSieve makes sure to not overwrite an existing `.dovecot.sieve` file. If a user activates a new sieve script the old one is backed up and moved to the `sieve` folder. The extension is known to work with the following ManageSieve clients: - **[Sieve Editor](https://github.com/thsmi/sieve)** a portable standalone application based on the former Thunderbird plugin. - **[Kmail](https://kontact.kde.org/components/kmail/)** the mail client of [KDE](https://kde.org/)'s Kontact Suite. + +[docs::accounts-subaddressing]: ../account-management/overview.md#sub-addressing + +[dovecot-docs::namespace]: https://doc.dovecot.org/configuration_manual/namespace/ +[dovecot-docs::mailbox-names]: https://doc.dovecot.org/configuration_manual/sieve/usage/#mailbox-names +[dovecot-docs::mailbox-formats::maildir]: https://doc.dovecot.org/admin_manual/mailbox_formats/maildir/#maildir-mbox-format +[dovecot-docs::config::lmtp_save_to_detail_mailbox]: https://doc.dovecot.org/settings/core/#core_setting-lmtp_save_to_detail_mailbox +[dovecot-docs::config::recipient_delimiter]: https://doc.dovecot.org/settings/core/#core_setting-recipient_delimiter + +[rfc::5233::sieve-subaddress]: https://datatracker.ietf.org/doc/html/rfc5233 +[sieve-docs::envelope]: https://thsmi.github.io/sieve-reference/en/test/core/envelope.html diff --git a/docs/content/config/advanced/maintenance/update-and-cleanup.md b/docs/content/config/advanced/maintenance/update-and-cleanup.md index dcf53c934c7..87c7eb9576c 100644 --- a/docs/content/config/advanced/maintenance/update-and-cleanup.md +++ b/docs/content/config/advanced/maintenance/update-and-cleanup.md @@ -2,40 +2,70 @@ title: 'Maintenance | Update and Cleanup' --- -## Automatic Update +[`ghcr.io/nickfedor/watchtower`][watchtower::registry] is a service that monitors Docker images for updates on the same tag used, automatically updating and restarting running containers. This is useful for images like DMS that support semver tags. -Docker images are handy but it can become a hassle to keep them updated. Also when a repository is automated you want to get these images when they get out. +!!! example "Automatic image updates + cleanup" -One could setup a complex action/hook-based workflow using probes, but there is a nice, easy to use docker image that solves this issue and could prove useful: [`watchtower`](https://hub.docker.com/r/containrrr/watchtower). + Run a `watchtower` container with access to `docker.sock`, enabling the service to manage Docker: -A docker-compose example: + ```yaml title="compose.yaml" + services: + watchtower: + image: ghcr.io/nickfedor/watchtower:latest + # Automatic cleanup: + environment: + - WATCHTOWER_CLEANUP=true + volumes: + - /var/run/docker.sock:/var/run/docker.sock + ``` -```yaml -services: - watchtower: - restart: always - image: containrrr/watchtower:latest - volumes: - - /var/run/docker.sock:/var/run/docker.sock -``` + The `watchtower` container can use the [`WATCHTOWER_CLEANUP=true` ENV (CLI option: `--cleanup`)][watchtower-docs::cleanup] to enable automatic cleanup (removal) of the previous image used for container it updates. Removal occurs after the container is restarted with the new image pulled. -For more details, see the [manual](https://containrrr.github.io/watchtower/) + !!! info "`containrrr/watchtower` is unmaintained" -## Automatic Cleanup + The [original project (`containrrr/watchtower`)][watchtower::original] has not received maintenance over recent years and was [archived in Dec 2025][watchtower::archived]. -When you are pulling new images in automatically, it would be nice to have them cleaned up as well. There is also a docker image for this: [`spotify/docker-gc`](https://hub.docker.com/r/spotify/docker-gc/). + A [community fork (`nicholas-fedor/watchtower`)][watchtower::community-fork] has since established itself as a maintained successor. -A docker-compose example: +!!! tip "The image tag used for a container is monitored for updates (eg: `:latest`, `:edge`, `:16`)" -```yaml -services: - docker-gc: - restart: always - image: spotify/docker-gc:latest - volumes: - - /var/run/docker.sock:/var/run/docker.sock -``` + The automatic update support is **only for updates to that specific image tag**. -For more details, see the [manual](https://github.com/spotify/docker-gc/blob/master/README.md) + --- -Or you can just use the [`--cleanup`](https://containrrr.github.io/watchtower/arguments/#cleanup) option provided by `containrrr/watchtower`. + The tag for an image is never modified by `watchtower`, instead `watchtower` monitors the image digest associated to that image tag (_which will change to a new image digest if a new image release reassigns the tag_), when the digest for the tag changes this triggers a pull of the new image. + + - Your container will not update to a new major release version (_unless using `:latest`_). + - Omit the minor or patch portion of a semver tag to receive updates for the omitted portion (_eg: `:16` will represent the latest minor + patch release, whereas `:16.0` would only receive patch updates instead of minor releases like `16.1`_). + +!!! tip "Updating only specific containers" + + By default the `watchtower` service will check every 24 hours for new image updates to pull, based on currently running containers (_**not restricted** to only those running within your `compose.yaml`_). + + Images eligible for updates can configured with a [custom `command`][docker-docs::compose-command] that provides a list of container names, alternatively via [container labels to monitor only specific containers][watchtower-docs::monitor-labels] (_or instead exclude specific containers from monitoring_). + +!!! info "Manual cleanup" + + `watchtower` supports running on-demand with `docker run` or `compose.yaml` via the [`WATCHTOWER_RUN_ONCE=true` ENV (CLI option: `--run-once`)][watchtower-docs::run-once]. You can either use this for manual or scheduled update + cleanup, instead of running as a background service. + + --- + + Without `watchtower` handling image cleanup, you can alternatively invoke cleanup of Docker storage directly with: + + - [`docker image prune --all`][docker-docs::prune-image] + - [`docker system prune --all`][docker-docs::prune-system] (_also removes unused containers, networks, build cache_). + + If you omit the `--all` option, this will instead only remove ["dangling" content][docker::prune-dangling] (_eg: Orphaned images_). + +[watchtower::registry]: https://github.com/nicholas-fedor/watchtower/pkgs/container/watchtower +[watchtower::original]: https://github.com/containrrr/watchtower +[watchtower::archived]: https://github.com/containrrr/watchtower/discussions/2135 +[watchtower::community-fork]: https://github.com/nicholas-fedor/watchtower +[watchtower-docs::cleanup]: https://watchtower.nickfedor.com/v1.13.1/configuration/arguments/#cleanup_old_images +[watchtower-docs::run-once]: https://watchtower.nickfedor.com/v1.13.1/configuration/arguments/#run_once +[watchtower-docs::monitor-labels]: https://watchtower.nickfedor.com/v1.13.1/configuration/container-selection + +[docker-docs::compose-command]: https://docs.docker.com/compose/compose-file/05-services/#command +[docker-docs::prune-image]: https://docs.docker.com/engine/reference/commandline/image_prune/ +[docker-docs::prune-system]: https://docs.docker.com/engine/reference/commandline/system_prune/ +[docker::prune-dangling]: https://stackoverflow.com/questions/45142528/what-is-a-dangling-image-and-what-is-an-unused-image/60756668#60756668 diff --git a/docs/content/config/advanced/optional-config.md b/docs/content/config/advanced/optional-config.md index 2e5c29e070e..57fa68dee14 100644 --- a/docs/content/config/advanced/optional-config.md +++ b/docs/content/config/advanced/optional-config.md @@ -4,16 +4,69 @@ hide: - toc # Hide Table of Contents for this page --- -This is a list of all configuration files and directories which are optional or automatically generated in your `docker-data/dms/config/` directory. +## Volumes -## Directories +DMS has several locations in the container which may be worth persisting externally via [Docker Volumes][docker-docs::volumes]. + +- Often you will want to prefer [bind mount volumes][docker-docs::volumes::bind-mount] for easy access to files at a local location on your filesystem. +- As a convention for our docs and example configs, the local location has the common prefix `docker-data/dms/` for grouping these related volumes. + +!!! info "Reference - Volmes for DMS" + + Our docs may refer to these DMS specific volumes only by name, or the host/container path for brevity. + + - [Config](#volumes-config): `docker-data/dms/config/` => `/tmp/docker-mailserver/` + - [Mail Storage](#volumes-mail): `docker-data/dms/mail-data/` => `/var/mail/` + - [State](#volumes-state): `docker-data/dms/mail-state/` => `/var/mail-state/` + - [Logs](#volumes-log): `docker-data/dms/mail-logs/` => `/var/log/mail/` + +### Mail Storage Volume { #volumes-mail } + +This is the location where mail is delivered to your mailboxes. + +### State Volume { #volumes-state } + +Run-time specific state lives here, but so does some data you may want to keep if a failure event occurs (_crash, power loss_). + +!!! example "Examples of relevant data" + + - The Postfix queue (eg: mail pending delivery attempt) + - Fail2Ban blocks. + - ClamAV signature updates. + - Redis storage for Rspamd. + +!!! info "When a volume is mounted to `/var/mail-state/`" + + - Service run-time data is [consolidated into the `/var/mail-state/` directory][mail-state-folders]. Otherwise the original locations vary and would need to be mounted individually. + - The original locations are updated with symlinks to redirect to their new path in `/var/mail-state/` (_eg: `/var/lib/redis` => `/var/mail-state/lib-redis/`_). + + Supported services: Postfix, Dovecot, Fail2Ban, Amavis, PostGrey, ClamAV, SpamAssassin, Rspamd & Redis, Fetchmail, Getmail, LogRotate, PostSRSd, MTA-STS. + +!!! tip + + Sometimes it is helpful to disable this volume when troubleshooting to verify if the data stored here is in a bad state (_eg: caused by a failure event_). + +[mail-state-folders]: https://github.com/docker-mailserver/docker-mailserver/blob/v13.3.1/target/scripts/startup/setup.d/mail_state.sh#L13-L33 + +### Logs Volume { #volumes-log } + +This can be a useful volume to persist for troubleshooting needs for the full set of log files. + +### Config Volume { #volumes-config } + +Most configuration files for Postfix, Dovecot, etc. are persisted here. + +This is a list of all configuration files and directories which are optional, automatically generated / updated by our `setup` CLI, or other internal scripts. + +#### Directories - **sieve-filter:** directory for sieve filter scripts. (Docs: [Sieve][docs-sieve]) - **sieve-pipe:** directory for sieve pipe scripts. (Docs: [Sieve][docs-sieve]) - **opendkim:** DKIM directory. Auto-configurable via [`setup.sh config dkim`][docs-setupsh]. (Docs: [DKIM][docs-dkim]) - **ssl:** SSL Certificate directory if `SSL_TYPE` is set to `self-signed` or `custom`. (Docs: [SSL][docs-ssl]) +- **rspamd:** Override directory for custom settings when using Rspamd (Docs: [Rspamd][docs-rspamd-override-d]) -## Files +#### Files - **{user_email_address}.dovecot.sieve:** User specific Sieve filter file. (Docs: [Sieve][docs-sieve]) - **before.dovecot.sieve:** Global Sieve filter file, applied prior to the `${login}.dovecot.sieve` filter. (Docs: [Sieve][docs-sieve]) @@ -24,35 +77,38 @@ This is a list of all configuration files and directories which are optional or - **postfix-send-access.cf:** List of users denied sending. Modify via [`setup.sh email restrict`][docs-setupsh]. - **postfix-receive-access.cf:** List of users denied receiving. Modify via [`setup.sh email restrict`][docs-setupsh]. - **postfix-virtual.cf:** Alias configuration file. Modify via [`setup.sh alias`][docs-setupsh]. -- **postfix-sasl-password.cf:** listing of relayed domains with their respective `:`. Modify via `setup.sh relay add-auth []`. (Docs: [Relay-Hosts Auth][docs-relayhosts-senderauth]) -- **postfix-relaymap.cf:** domain-specific relays and exclusions. Modify via `setup.sh relay add-domain` and `setup.sh relay exclude-domain`. (Docs: [Relay-Hosts Senders][docs-relayhosts-senderhost]) +- **postfix-sasl-password.cf:** listing of relayed domains with their respective `:`. Modify via `setup.sh relay add-auth []`. (Docs: [Relay-Hosts Auth][docs::relay-hosts::advanced]) +- **postfix-relaymap.cf:** domain-specific relays and exclusions. Modify via `setup.sh relay add-domain` and `setup.sh relay exclude-domain`. (Docs: [Relay-Hosts Senders][docs::relay-hosts::advanced]) - **postfix-regexp.cf:** Regular expression alias file. (Docs: [Aliases][docs-aliases-regex]) - **ldap-users.cf:** Configuration for the virtual user mapping `virtual_mailbox_maps`. See the [`setup-stack.sh`][github-commit-setup-stack.sh-L411] script. - **ldap-groups.cf:** Configuration for the virtual alias mapping `virtual_alias_maps`. See the [`setup-stack.sh`][github-commit-setup-stack.sh-L411] script. - **ldap-aliases.cf:** Configuration for the virtual alias mapping `virtual_alias_maps`. See the [`setup-stack.sh`][github-commit-setup-stack.sh-L411] script. - **ldap-domains.cf:** Configuration for the virtual domain mapping `virtual_mailbox_domains`. See the [`setup-stack.sh`][github-commit-setup-stack.sh-L411] script. - **whitelist_clients.local:** Whitelisted domains, not considered by postgrey. Enter one host or domain per line. -- **spamassassin-rules.cf:** Antispam rules for Spamassassin. (Docs: [FAQ - SpamAssassin Rules][docs-faq-spamrules]) +- **spamassassin-rules.cf:** Anti-spam rules for Spamassassin. (Docs: [FAQ - SpamAssassin Rules][docs-faq-spamrules]) - **fail2ban-fail2ban.cf:** Additional config options for `fail2ban.cf`. (Docs: [Fail2Ban][docs-fail2ban]) -- **fail2ban-jail.cf:** Additional config options for fail2ban's jail behaviour. (Docs: [Fail2Ban][docs-fail2ban]) +- **fail2ban-jail.cf:** Additional config options for fail2ban's jail behavior. (Docs: [Fail2Ban][docs-fail2ban]) - **amavis.cf:** replaces the `/etc/amavis/conf.d/50-user` file - **dovecot.cf:** replaces `/etc/dovecot/local.conf`. (Docs: [Override Dovecot Defaults][docs-override-dovecot]) - **dovecot-quotas.cf:** list of custom quotas per mailbox. (Docs: [Accounts][docs-accounts-quota]) - **user-patches.sh:** this file will be run after all configuration files are set up, but before the postfix, amavis and other daemons are started. (Docs: [FAQ - How to adjust settings with the `user-patches.sh` script][docs-faq-userpatches]) -- **rspamd-commands:** list of simple commands to adjust Rspamd modules in an easy way (Docs: [Rspamd][docs-rspamd-commands]) +- **rspamd/custom-commands.conf:** list of simple commands to adjust Rspamd modules in an easy way (Docs: [Rspamd][docs-rspamd-commands]) + +[docker-docs::volumes]: https://docs.docker.com/storage/volumes/ +[docker-docs::volumes::bind-mount]: https://docs.docker.com/storage/bind-mounts/ -[docs-accounts-quota]: ../../config/user-management/accounts.md#notes -[docs-aliases-regex]: ../../config/user-management/aliases.md#configuring-regexp-aliases -[docs-dkim]: ../../config/best-practices/dkim.md +[docs-accounts-quota]: ../../config/account-management/provisioner/file.md#quotas +[docs-aliases-regex]: ../../config/account-management/provisioner/file.md#configuring-regex-aliases +[docs-dkim]: ../../config/best-practices/dkim_dmarc_spf.md#dkim [docs-fail2ban]: ../../config/security/fail2ban.md [docs-faq-spamrules]: ../../faq.md#how-can-i-manage-my-custom-spamassassin-rules [docs-faq-userpatches]: ../../faq.md#how-to-adjust-settings-with-the-user-patchessh-script [docs-override-postfix]: ./override-defaults/postfix.md [docs-override-dovecot]: ./override-defaults/dovecot.md -[docs-relayhosts-senderauth]: ./mail-forwarding/relay-hosts.md#sender-dependent-authentication -[docs-relayhosts-senderhost]: ./mail-forwarding/relay-hosts.md#sender-dependent-relay-host +[docs::relay-hosts::advanced]: ./mail-forwarding/relay-hosts.md#advanced-configuration [docs-sieve]: ./mail-sieve.md [docs-setupsh]: ../../config/setup.sh.md [docs-ssl]: ../../config/security/ssl.md +[docs-rspamd-override-d]: ../security/rspamd.md#manually [docs-rspamd-commands]: ../security/rspamd.md#with-the-help-of-a-custom-file [github-commit-setup-stack.sh-L411]: https://github.com/docker-mailserver/docker-mailserver/blob/941e7acdaebe271eaf3d296b36d4d81df4c54b90/target/scripts/startup/setup-stack.sh#L411 diff --git a/docs/content/config/advanced/override-defaults/dovecot.md b/docs/content/config/advanced/override-defaults/dovecot.md index 87b51ca129f..84d13a04ea2 100644 --- a/docs/content/config/advanced/override-defaults/dovecot.md +++ b/docs/content/config/advanced/override-defaults/dovecot.md @@ -7,14 +7,14 @@ title: 'Override the Default Configs | Dovecot' The Dovecot default configuration can easily be extended providing a `docker-data/dms/config/dovecot.cf` file. [Dovecot documentation](https://doc.dovecot.org/configuration_manual/) remains the best place to find configuration options. -Your `docker-mailserver` folder should look like this example: +Your DMS folder structure should look like this example: ```txt ├── docker-data/dms/config │ ├── dovecot.cf │ ├── postfix-accounts.cf │ └── postfix-virtual.cf -├── docker-compose.yml +├── compose.yaml └── README.md ``` @@ -26,7 +26,7 @@ mail_max_userip_connections = 100 Another important option is the `default_process_limit` (defaults to `100`). If high-security mode is enabled you'll need to make sure this count is higher than the maximum number of users that can be logged in simultaneously. -This limit is quickly reached if users connect to the `docker-mailserver` with multiple end devices. +This limit is quickly reached if users connect to DMS with multiple end devices. ## Override Configuration @@ -55,7 +55,7 @@ To debug your dovecot configuration you can use: - Or: `docker exec -it mailserver doveconf | grep ` !!! note - [`setup.sh`][github-file-setupsh] is included in the `docker-mailserver` repository. Make sure to use the one matching your image version release. + [`setup.sh`][github-file-setupsh] is included in the DMS repository. Make sure to use the one matching your image version release. The file `docker-data/dms/config/dovecot.cf` is copied internally to `/etc/dovecot/local.conf`. To verify the file content, run: diff --git a/docs/content/config/advanced/override-defaults/user-patches.md b/docs/content/config/advanced/override-defaults/user-patches.md index b10173368c1..597ed6aa52b 100644 --- a/docs/content/config/advanced/override-defaults/user-patches.md +++ b/docs/content/config/advanced/override-defaults/user-patches.md @@ -2,9 +2,9 @@ title: 'Custom User Changes & Patches | Scripting' --- -If you'd like to change, patch or alter files or behavior of `docker-mailserver`, you can use a script. +If you'd like to change, patch or alter files or behavior of DMS, you can use a script. -In case you cloned this repository, you can copy the file [`user-patches.sh.dist` (_under `config/`_)][gh-file-userpatches] with `#!sh cp config/user-patches.sh.dist docker-data/dms/config/user-patches.sh` in order to create the `user-patches.sh` script. +In case you cloned this repository, you can copy the file [`user-patches.sh.dist` (_under `config/`_)][github-file-userpatches] with `#!sh cp config/user-patches.sh.dist docker-data/dms/config/user-patches.sh` in order to create the `user-patches.sh` script. If you are managing your directory structure yourself, create a `docker-data/dms/config/` directory and add the `user-patches.sh` file yourself. @@ -40,4 +40,4 @@ And you're done. The user patches script runs right before starting daemons. Tha !!! note Many "patches" can already be done with the Docker Compose-/Stack-file. Adding hostnames to `/etc/hosts` is done with the `#!yaml extra_hosts:` section, `sysctl` commands can be managed with the `#!yaml sysctls:` section, etc. -[gh-file-userpatches]: https://github.com/docker-mailserver/docker-mailserver/blob/master/config-examples/user-patches.sh +[github-file-userpatches]: https://github.com/docker-mailserver/docker-mailserver/blob/master/config-examples/user-patches.sh diff --git a/docs/content/config/advanced/podman.md b/docs/content/config/advanced/podman.md index 599132ccc9b..714c6834100 100644 --- a/docs/content/config/advanced/podman.md +++ b/docs/content/config/advanced/podman.md @@ -8,7 +8,7 @@ Podman is a daemonless container engine for developing, managing, and running OC !!! warning "About Support for Podman" - Please note that Podman **is not** officially supported as `docker-mailserver` is built and verified on top of the _Docker Engine_. This content is entirely community supported. If you find errors, please open an issue and provide a PR. + Please note that Podman **is not** officially supported as DMS is built and verified on top of the _Docker Engine_. This content is entirely community supported. If you find errors, please open an issue and provide a PR. !!! warning "About this Guide" @@ -19,11 +19,11 @@ Podman is a daemonless container engine for developing, managing, and running OC Running podman in rootless mode requires additional modifications in order to keep your mailserver secure. Make sure to read the related documentation. -## Installation in Rootfull Mode +## Installation in Rootful Mode While using Podman, you can just manage docker-mailserver as what you did with Docker. Your best friend `setup.sh` includes the minimum code in order to support Podman since it's 100% compatible with the Docker CLI. -The installation is basically the same. Podman v3.2 introduced a RESTful API that is 100% compatible with the Docker API, so you can use docker-compose with Podman easily. Install Podman and docker-compose with your package manager first. +The installation is basically the same. Podman v3.2 introduced a RESTful API that is 100% compatible with the Docker API, so you can use Docker Compose with Podman easily. Install Podman and Docker Compose with your package manager first. ```bash sudo dnf install podman docker-compose @@ -39,25 +39,15 @@ This will create a unix socket locate under `/run/podman/podman.sock`, which is ```bash export DOCKER_HOST="unix:///run/podman/podman.sock" -docker-compose up -d mailserver -docker-compose ps +docker compose up -d mailserver +docker compose ps ``` You should see that docker-mailserver is running now. -### Self-start in Rootfull Mode - -Podman is daemonless, that means if you want docker-mailserver self-start while boot up the system, you have to generate a systemd file with Podman CLI. - -```bash -podman generate systemd mailserver > /etc/systemd/system/mailserver.service -systemctl daemon-reload -systemctl enable --now mailserver.service -``` - ## Installation in Rootless Mode -Running rootless containers is one of Podman's major features. But due to some restrictions, deploying docker-mailserver in rootless mode is not as easy compared to rootfull mode. +Running [rootless containers][podman-docs::rootless-mode] is one of Podman's major features. But due to some restrictions, deploying docker-mailserver in rootless mode is not as easy compared to rootful mode. - a rootless container is running in a user namespace so you cannot bind ports lower than 1024 - a rootless container's systemd file can only be placed in folder under `~/.config` @@ -67,7 +57,7 @@ Also notice that Podman's rootless mode is not about running as a non-root user !!! warning - In order to make rootless `docker-mailserver` work we must modify some settings in the Linux system, it requires some basic linux server knowledge so don't follow this guide if you not sure what this guide is talking about. Podman rootfull mode and Docker are still good and security enough for normal daily usage. + In order to make rootless DMS work we must modify some settings in the Linux system, it requires some basic linux server knowledge so don't follow this guide if you not sure what this guide is talking about. Podman rootful mode and Docker are still good and security enough for normal daily usage. First, enable `podman.socket` in systemd's userspace with a non-root user. @@ -75,7 +65,7 @@ First, enable `podman.socket` in systemd's userspace with a non-root user. systemctl enable --now --user podman.socket ``` -The socket file should be located at `/var/run/user/$(id -u)/podman/podman.sock`. Then, modify `docker-compose.yml` to make sure all ports are bindings are on non-privileged ports. +The socket file should be located at `/var/run/user/$(id -u)/podman/podman.sock`. Then, modify `compose.yaml` to make sure all ports are bindings are on non-privileged ports. ```yaml services: @@ -88,12 +78,221 @@ services: - "10993:993" # IMAP4 (implicit TLS) ``` -Then, setup your `mailserver.env` file follow the documentation and use docker-compose to start the container. +Then, setup your `mailserver.env` file follow the documentation and use Docker Compose to start the container. ```bash export DOCKER_HOST="unix:///var/run/user/$(id -u)/podman/podman.sock" -docker-compose up -d mailserver -docker-compose ps +docker compose up -d mailserver +docker compose ps +``` + +### Rootless Quadlet + +!!! info "What is a Quadlet?" + + A [Quadlet][podman::quadlet::introduction] file uses the [systemd config format][systemd-docs::config-syntax] which is similar to the INI format. + + [Quadlets define your podman configuration][podman-docs::quadlet::example-configs] (_pods, volumes, networks, images, etc_) which are [adapted into the equivalent systemd service config files][podman::quadlet::generated-output-example] at [boot or when reloading the systemd daemon][podman-docs::config::quadlet-generation] (`systemctl daemon-reload` / `systemctl --user daemon-reload`). + +!!! tip "Rootless compatibility" + + Quadlets can [support rootless with a few differences][podman::rootless-differences]: + + - `Network=pasta` configures [`pasta`][network-driver::pasta] as a rootless compatible network driver (_a popular alternative to `slirp4netns`. `pasta` is the default for rootless since Podman v5_). + - `Restart=always` will auto-start your Quadlet at login. Rootless support requires to enable [lingering][systemd-docs::loginctl::linger] for your user: + + ```bash + loginctl enable-linger user + ``` + - [Config locations between rootful vs rootless][podman-docs::quadlet::config-search-path]. + +#### Example Quadlet file + +???+ example + + 1. Create your DMS Quadlet at `~/.config/containers/systemd/dms.container` with the example content shown below. + - Adjust settings like `HostName` as needed. You may prefer a different convention for your `Volume` host paths. + - Some syntax like systemd specifiers and Podman's `UIDMap` value are explained in detail after this example. + 2. Run [`systemctl --user daemon-reload`][systemd-docs::systemctl::daemon-reload], which will trigger the Quadlet service generator. This command is required whenever you adjust config in `dms.container`. + 3. You should now be able to start the service with `systemctl --user start dms`. + + ```ini title="dms.container" + [Unit] + Description="Docker Mail Server" + Documentation=https://docker-mailserver.github.io/docker-mailserver/latest + + [Service] + Restart=always + # Optional - This will run before the container starts: + # - It ensures all the DMS volumes have the host directories created for you. + # - For `mkdir` command to leverage the shell brace expansion syntax, you need to run it via bash. + ExecStartPre=/usr/bin/bash -c 'mkdir -p %h/volumes/%N/{mail-data,mail-state,mail-logs,config}' + + # This section enables the service at generation, avoids requiring `systemctl --user enable dms`: + # - `multi-user.target` => root + # - `default.target` => rootless + [Install] + WantedBy=default.target + + [Container] + ContainerName=%N + HostName=mail.example.com + Image=docker.io/mailserver/docker-mailserver:latest + + PublishPort=25:25 + PublishPort=143:143 + PublishPort=587:587 + PublishPort=993:993 + + # The container UID for root will be mapped to the host UID running this Quadlet service. + # All other UIDs in the container are mapped via the sub-id range for that user from host configs `/etc/subuid` + `/etc/subgid`. + UIDMap=+0:@%U + + # Volumes (Base location example: `%h/volumes/%N` => `~/volumes/dms`) + # NOTE: If your host has SELinux enabled, avoid permission errors by appending the mount option `:Z`. + Volume=%h/volumes/%N/mail-data:/var/mail + Volume=%h/volumes/%N/mail-state:/var/mail-state + Volume=%h/volumes/%N/mail-logs:/var/log/mail + Volume=%h/volumes/%N/config:/tmp/docker-mailserver + # Optional - Additional mounts: + # NOTE: For SELinux, when using the `z` or `Z` mount options: + # Take caution if choosing a host location not belonging to your user. Consider using `SecurityLabelDisable=true` instead. + # https://docs.podman.io/en/latest/markdown/podman-run.1.html#volume-v-source-volume-host-dir-container-dir-options + Volume=%h/volumes/certbot/certs:/etc/letsencrypt:ro + + # Podman can create a timer (defaults to daily at midnight) to check the `registry` or `local` storage for detecting if the + # image tag points to a new digest, if so it updates the image and restarts the service (similar to `containrrr/watchtower`): + # https://docs.podman.io/en/latest/markdown/podman-auto-update.1.html + AutoUpdate=registry + + # Podman Quadlet has a better alternative instead of a volume directly bind mounting `/etc/localtime` to match the host TZ: + # https://docs.podman.io/en/latest/markdown/podman-run.1.html#tz-timezone + # NOTE: Should the host modify the system TZ, neither approach will sync the change to the `/etc/localtime` inside the running container. + Timezone=local + + Environment=SSL_TYPE=letsencrypt + # NOTE: You may need to adjust the default `NETWORK_INTERFACE`: + # https://docker-mailserver.github.io/docker-mailserver/latest/config/environment/#network_interface + #Environment=NETWORK_INTERFACE=enp1s0 + #Environment=NETWORK_INTERFACE=tap0 + ``` + +??? info "Systemd specifiers" + + Systemd has a [variety of specifiers][systemd-docs::config-specifiers] (_prefixed with `%`_) that help manage configs. + + Here are the ones used in the Quadlet config example: + + - **`%h`:** Location of the users home directory. Use this instead of `~` (_which would only work in a shell, not this config_). + - **`%N`:** Represents the unit service name, which is taken from the filename excluding the extension (_thus `dms.container` => `dms`_). + - **`%U`:** The UID of the user running this service. The next section details the relevance with `UIDMap`. + + --- + + If you prefer the conventional XDG locations, you may prefer `%D` + `%E` + `%S` as part of your `Volume` host paths. + +Stopping the service with systemd will result in the container being removed. Restarting will use the existing container, which is however not recommended. You do not need to enable services with Quadlet. + +Start container: + +`systemctl --user start dockermailserver` + +Stop container: + +`systemctl --user stop dockermailserver` + +Using root with machinectl (used for some Ansible versions): + +`machinectl -q shell yourrootlessuser@ /bin/systemctl --user start dockermailserver` + +#### Mapping ownership between container and host users + +Podman supports a few different approaches for this functionality. For rootless Quadlets you will likely want to use `UIDMap` (_`GIDMap` will use this same mapping by default_). + +- `UIDMap` + `GIDMap` works by mapping user and group IDs from a container, to IDs associated for a user on the host [configured in `/etc/subuid` + `/etc/subgid`][podman-docs::rootless-mode] (_this isn't necessary for rootful Podman_). +- Each mapping must be unique, thus only a single container UID can map to your rootless UID on the host. Every other container UID mapped must be within the configured range from `/etc/subuid`. +- Rootless containers have one additional level of mapping involved. This is an offset from their `/etc/subuid` entry starting from `0`, but can be inferred when the intended UID on the host is prefixed with `@` + +??? tip "Why should I prefer `UIDMap=+0:@%U`? How does the `@` syntax work?" + + The most common case is to map the containers root user (UID `0`) to your host user ID. + + For a rootless user with the UID `1000` on the host, any of the following `UIDMap` values are equivalent: + + - **`UIDMap=+0:0`:** The 1st `0` is the container root ID and the 2nd `0` refers to host mapping ID. For rootless the mapping ID is an indirect offset to their user entry in `/etc/subuid` where `0` maps to their host user ID, while `1` or higher maps to the users subuid range. + - **`UIDMap=+0:@1000`:** A rootless Quadlet can also use `@` as a prefix which Podman will then instead lookup as the host ID in `/etc/subuid` to get the offset value. If the host user ID was `1000`, then `@1000` would resolve that to `0`. + - **`UIDMap=+0:@%U`:** Instead of providing the explicit rootless UID, a better approach is to leverage `%U` (_a [systemd specifier][systemd-docs::config-specifiers]_) which will resolve to the UID of your rootless user that starts the Quadlet service. + +??? tip "What is the `+` syntax used with `UIDMap`?" + + Prefixing the container ID with `+` is a a podman feature similar to `@`, which ensures `/etc/subuid` is mapped fully. + + For example `UIDMap=+5000:@%U` is the short-hand equivalent to: + + ```ini + UIDMap=5000:0:1 + UIDMap=0:1:5000 + UIDMap=5001:5001:60536 + ``` + + The third value is the amount of IDs to map from the `container:host` pair as an offset/range. It defaults to `1`. + + In addition to our explicit `5000:0` mapping, the `+` ensures: + + - That we have a mapping of all container ID prior to `5000` to IDs from our rootless user entry in `/etc/subuid` on the host. + - It also adds a mapping after this value for the remainder of the range configured in `/etc/subuid` which covers the `nobody` user in the container. + + Within the container you can view these mappings via `cat /proc/self/uid_map`. + +??? warning "Impact on disk usage of images with Rootless" + + **NOTE:** This should not usually be a concern, but is documented here to explain the impact of creating new user namespaces (_such as by running a container with settings like `UIDMap` that differ between runs_). + + --- + + Rootless containers [perform a copy of the image with `chown`][caveat::podman::rootless::image-chown] during the first pull/run of the image. + + - The larger the image to copy, the longer the initial delay on first use. + - This process will be repeated if the `UIDMap` / `GIDMap` settings are changed to a value that has not been used previously (_accumulating more disk usage with additional image layer copies_). + - Only when the original image is removed will any of these associated `chown` image copies be purged from storage. + + When you specify a `UIDMap` like demonstrated in the earlier tip for the `+` syntax with `UIDMap=+0:5000`, if the `/proc/self/uid_map` shows a row with the first two columns as equivalent then no excess `chown` should be applied. + + - `UIDMap=+0:@%U` is equivalent from ID 2 onwards. + - `UIDMap=+5000:@%U` is equivalent from ID 5001 onwards. This is relevant with DMS as the container UID 200 is assigned to ClamAV, the offset introduced will now incur a `chown` copy of 230MB. + +## Start DMS container at boot + +Unlike Docker, Podman is daemonless thus containers do not start at boot. You can create your own systemd service to schedule this or use the Podman CLI. + +!!! warning "`podman generate systemd` is deprecated" + + The [`podman generate systemd`][podman-docs::cli::generate-systemd] command has been deprecated [since Podman v4.7][gh::podman::release-4.7] (Sep 2023) in favor of Quadlets (_available [since Podman v4.4][gh::podman::release-4.4]_). + +!!! example "Create a systemd service" + + Use the Podman CLI to generate a systemd service at the rootful or rootless location. + + === "Rootful" + + ```bash + podman generate systemd mailserver > /etc/systemd/system/mailserver.service + systemctl daemon-reload + systemctl enable --now mailserver.service + ``` + + === "Rootless" + + ```bash + podman generate systemd mailserver > ~/.config/systemd/user/mailserver.service + systemctl --user daemon-reload + systemctl enable --user --now mailserver.service + ``` + +A systemd user service will only start when that specific user logs in and stops when after log out. To instead allow user services to run when that user has no active session running run: + +```bash +loginctl enable-linger ``` ### Security in Rootless Mode @@ -104,14 +303,26 @@ In rootless mode, podman resolves all incoming IPs as localhost, which results i The `PERMIT_DOCKER` variable in the `mailserver.env` file allows to specify trusted networks that do not need to authenticate. If the variable is left empty, only requests from localhost and the container IP are allowed, but in the case of rootless podman any IP will be resolved as localhost. Setting `PERMIT_DOCKER=none` enforces authentication also from localhost, which prevents sending unauthenticated emails. -#### Use the slip4netns network driver +#### Use the `pasta` network driver + +Since [Podman 5.0][gh::podman::release-5.0] the default rootless network driver is now `pasta` instead of `slirp4netns`. These two drivers [have some differences][rhel-docs::podman::slirp4netns-vs-pasta]: + +> Notable differences of `pasta` network mode compared to `slirp4netns` include: +> +> - `pasta` supports IPv6 port forwarding. +> - `pasta` is more efficient than `slirp4netns`. +> - `pasta` copies IP addresses from the host, while `slirp4netns` uses a predefined IPv4 address. +> - `pasta` uses an interface name from the host, while `slirp4netns` uses `tap0` as an interface name. +> - `pasta` uses the gateway address from the host, while `slirp4netns` defines its own gateway address and uses NAT. -The second workaround is slightly more complicated because the `docker-compose.yml` has to be modified. -As shown in the [fail2ban section](https://docker-mailserver.github.io/docker-mailserver/edge/config/security/fail2ban/#podman-with-slirp4netns-port-driver) the `slirp4netns` network driver has to be enabled. +#### Use the `slip4netns` network driver + +The second workaround is slightly more complicated because the `compose.yaml` has to be modified. +As shown in the [fail2ban section][docs::fail2ban::rootless] the `slirp4netns` network driver has to be enabled. This network driver enables podman to correctly resolve IP addresses but it is not compatible with user defined networks which might be a problem depending on your setup. -[Rootless Podman][rootless::podman] requires adding the value `slirp4netns:port_handler=slirp4netns` to the `--network` CLI option, or `network_mode` setting in your `docker-compose.yml`. +[Rootless Podman][rootless::podman] requires adding the value `slirp4netns:port_handler=slirp4netns` to the `--network` CLI option, or `network_mode` setting in your `compose.yaml`. You must also add the ENV `NETWORK_INTERFACE=tap0`, because Podman uses a [hard-coded interface name][rootless::podman::interface] for `slirp4netns`. @@ -130,27 +341,9 @@ You must also add the ENV `NETWORK_INTERFACE=tap0`, because Podman uses a [hard- `podman-compose` is not compatible with this configuration. -### Self-start in Rootless Mode - -Generate a systemd file with the Podman CLI. - -```bash -podman generate systemd mailserver > ~/.config/systemd/user/mailserver.service -systemctl --user daemon-reload -systemctl enable --user --now mailserver.service -``` - -Systemd's user space service is only started when a specific user logs in and stops when you log out. In order to make it to start with the system, we need to enable linger with `loginctl` - -```bash -loginctl enable-linger -``` - -Remember to run this command as root user. - ### Port Forwarding -When it comes to forwarding ports using `firewalld`, see for more information. +When it comes to forwarding ports using `firewalld`, see [these port forwarding docs][firewalld-port-forwarding] for more information. ```bash firewall-cmd --permanent --add-forward-port=port=<25|143|465|587|993>:proto=:toport=<10025|10143|10465|10587|10993> @@ -169,7 +362,32 @@ firewall-cmd --permanent --direct --add-rule nat OUTPUT 0 -p -``` - -For LDAP systems that do not have any directly created user account you can run the following command (since `8.0.0`) to generate the signature by additionally providing the desired domain name (if you have multiple domains use the command multiple times or provide a comma-separated list of domains): - -```sh -./setup.sh config dkim keysize domain [,] -``` - -Now the keys are generated, you can configure your DNS server with DKIM signature, simply by adding a TXT record. If you have direct access to your DNS zone file, then it's only a matter of pasting the content of `docker-data/dms/config/opendkim/keys/example.com/mail.txt` in your `example.com.hosts` zone. - -```console -$ dig mail._domainkey.example.com TXT ---- -;; ANSWER SECTION -mail._domainkey. 300 IN TXT "v=DKIM1; k=rsa; p=AZERTYUIOPQSDFGHJKLMWXCVBN/AZERTYUIOPQSDFGHJKLMWXCVBN/AZERTYUIOPQSDFGHJKLMWXCVBN/AZERTYUIOPQSDFGHJKLMWXCVBN/AZERTYUIOPQSDFGHJKLMWXCVBN/AZERTYUIOPQSDFGHJKLMWXCVBN/AZERTYUIOPQSDFGHJKLMWXCVBN/AZERTYUIOPQSDFGHJKLMWXCVBN" -``` - -## Configuration using a Web Interface - -1. Generate a new record of the type `TXT`. -2. Paste `mail._domainkey` the `Name` txt field. -3. In the `Target` or `Value` field fill in `v=DKIM1; k=rsa; p=AZERTYUGHJKLMWX...`. -4. In `TTL` (time to live): Time span in seconds. How long the DNS server should cache the `TXT` record. -5. Save. - -!!! note - Sometimes the key in `docker-data/dms/config/opendkim/keys/example.com/mail.txt` can be on multiple lines. If so then you need to concatenate the values in the TXT record: - -```console -$ dig mail._domainkey.example.com TXT ---- -;; ANSWER SECTION -mail._domainkey. 300 IN TXT "v=DKIM1; k=rsa; " - "p=AZERTYUIOPQSDF..." - "asdfQWERTYUIOPQSDF..." -``` - -The target (or value) field must then have all the parts together: `v=DKIM1; k=rsa; p=AZERTYUIOPQSDF...asdfQWERTYUIOPQSDF...` - -## Verify-Only - -If you want DKIM to only _verify_ incoming emails, the following version of `/etc/opendkim.conf` may be useful (right now there is no easy mechanism for installing it other than forking the repo): - -```conf -# This is a simple config file verifying messages only - -#LogWhy yes -Syslog yes -SyslogSuccess yes - -Socket inet:12301@localhost -PidFile /var/run/opendkim/opendkim.pid - -ReportAddress postmaster@example.com -SendReports yes - -Mode v -``` - -## Switch Off DKIM - -Simply remove the DKIM key by recreating (not just relaunching) the `docker-mailserver` container. - -## Debugging - -- [DKIM-verifer](https://addons.mozilla.org/en-US/thunderbird/addon/dkim-verifier): A add-on for the mail client Thunderbird. -- You can debug your TXT records with the `dig` tool. - -```console -$ dig TXT mail._domainkey.example.com ---- -; <<>> DiG 9.10.3-P4-Debian <<>> TXT mail._domainkey.example.com -;; global options: +cmd -;; Got answer: -;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 39669 -;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1 - -;; OPT PSEUDOSECTION: -; EDNS: version: 0, flags:; udp: 512 -;; QUESTION SECTION: -;mail._domainkey.example.com. IN TXT - -;; ANSWER SECTION: -mail._domainkey.example.com. 3600 IN TXT "v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCxBSjG6RnWAdU3oOlqsdf2WC0FOUmU8uHVrzxPLW2R3yRBPGLrGO1++yy3tv6kMieWZwEBHVOdefM6uQOQsZ4brahu9lhG8sFLPX4MaKYN/NR6RK4gdjrZu+MYSdfk3THgSbNwIDAQAB" - -;; Query time: 50 msec -;; SERVER: 127.0.1.1#53(127.0.1.1) -;; WHEN: Wed Sep 07 18:22:57 CEST 2016 -;; MSG SIZE rcvd: 310 -``` - ---- - -!!! warning "Key sizes >=4096-bit" - - Keys of 4096 bits could de denied by some mail-servers. According to https://tools.ietf.org/html/rfc6376 keys are preferably between 512 and 2048 bits. See issue [#1854][github-issue-1854]. - -[github-issue-1854]: https://github.com/docker-mailserver/docker-mailserver/issues/1854 diff --git a/docs/content/config/best-practices/dkim_dmarc_spf.md b/docs/content/config/best-practices/dkim_dmarc_spf.md new file mode 100644 index 00000000000..08e73955132 --- /dev/null +++ b/docs/content/config/best-practices/dkim_dmarc_spf.md @@ -0,0 +1,382 @@ +# DKIM, DMARC & SPF + +Cloudflare has written an [article about DKIM, DMARC and SPF][cloudflare-dkim-dmarc-spf] that we highly recommend you to read to get acquainted with the topic. + +!!! note "Rspamd vs Individual validators" + + With v12.0.0, Rspamd was integrated into DMS. It can perform validations for DKIM, DMARC and SPF as part of the `spam-score-calculation` for an email. DMS provides individual alternatives for each validation that can be used instead of deferring to Rspamd: + + - DKIM: `opendkim` is used as a milter (like Rspamd) + - DMARC: `opendmarc` is used as a milter (like Rspamd) + - SPF: `policyd-spf` is used in Postfix's `smtpd_recipient_restrictions` + + In a future release Rspamd will become the default for these validations, with a deprecation notice issued prior to the removal of the above alternatives. + + We encourage everyone to prefer Rspamd via `ENABLE_RSPAMD=1`. + +!!! warning "DNS Caches & Propagation" + + While modern DNS providers are quick, it may take minutes or even hours for new DNS records to become available / propagate. + +## DKIM + +!!! quote "What is DKIM" + + DomainKeys Identified Mail (DKIM) is an email authentication method designed to detect forged sender addresses in email (email spoofing), a technique often used in phishing and email spam. + + [Source][wikipedia-dkim] + +When DKIM is enabled: + +1. Inbound mail will verify any included DKIM signatures +2. Outbound mail is signed (_when your sending domain has a configured DKIM key_) + +DKIM requires a public/private key pair to enable **signing (_via private key_)** your outgoing mail, while the receiving end must query DNS to **verify (_via public key_)** that the signature is trustworthy. + +??? info "Verification expiry" + + Unlike your TLS certificate, your DKIM keypair does not have a fixed expiry associated to it. + + + Instead, an expiry may be included in your DKIM signature for each mail sent, where a receiver will [refuse to validate the signature for an email after that expiry date][dkim-verification-expiry-refusal]. This is an added precaution to mitigate malicious activity like "DKIM replay attacks", where an already delivered email from a third-party with a trustworthy DKIM signature is leveraged by a spammer when sending mail to an MTA which verifies the DKIM signature successfully, enabling the spammer to bypass spam protections. + + Unlike a TLS handshake where you are authenticating trust with future communications, with DKIM once the mail has been received and trust of the signature has been verified, the value of verifying the signature again at a later date is less meaningful since the signature was to ensure no tampering had occurred during delivery through the network. + +??? tip "DKIM key rotation" + + You can rotate your DKIM keypair by switching to a new DKIM selector (_and DNS updates_), while the previous key and selector remains valid for verification until the last mail signed with that key reaches it's expiry. + + DMS does not provide any automation or support for key rotation, [nor is it likely to provide a notable security benefit][gh-discussion::dkim-key-rotation-expiry] to the typical small scale DMS deployment. + +### Generating Keys + +You'll need to repeat this process if you add any new domains. + +You should have: + +- At least one [email account setup][docs-accounts] +- Attached a [volume for config][docs-volumes-config] to persist the generated files to local storage + +!!! example "Creating DKIM Keys" + + DKIM keys can be generated with good defaults by running: + + ```bash + docker exec -it setup config dkim + ``` + + If you need to generate your keys with different settings, check the `help` output for supported config options and examples: + + ```bash + docker exec -it setup config dkim help + ``` + + As described by the help output, you may need to use the `domain` option explicitly when you're using LDAP or Rspamd. + +??? info "Changing the key size" + + The keypair generated for using with DKIM presently defaults to RSA-2048. This is a good size but you can lower the security to `1024-bit`, or increase it to `4096-bit` (_discouraged as that is excessive_). + + To generate a key with different size (_for RSA 1024-bit_) run: + + ```sh + setup config dkim keysize 1024 + ``` + + !!! warning "RSA Key Sizes >= 4096 Bit" + + According to [RFC 8301][rfc-8301], keys are preferably between 1024 and 2048 bits. Keys of size 4096-bit or larger may not be compatible to all systems your mail is intended for. + + You [should not need a key length beyond 2048-bit][gh-issue::dkim-length]. If 2048-bit does not meet your security needs, you may want to instead consider adopting key rotation or switching from RSA to ECC keys for DKIM. + +??? note "You may need to specify mail domains explicitly" + + Required when using LDAP and Rspamd. + + `setup config dkim` will generate DKIM keys for what is assumed as the primary mail domain (_derived from the FQDN assigned to DMS, minus any subdomain_). + + When the DMS FQDN is `mail.example.com` or `example.com`, by default this command will generate DKIM keys for `example.com` as the primary domain for your users mail accounts (eg: `hello@example.com`). + + The DKIM generation does not have support to query LDAP for additional mail domains it should know about. If the primary mail domain is not sufficient, then you must explicitly specify any extra domains via the `domain` option: + + ```sh + # ENABLE_OPENDKIM=1 (default): + setup config dkim domain 'example.com,another-example.com' + + # ENABLE_RSPAMD=1 + ENABLE_OPENDKIM=0: + setup config dkim domain example.com + setup config dkim domain another-example.com + ``` + + !!! info "OpenDKIM with `ACCOUNT_PROVISIONER=FILE`" + + When DMS uses this configuration, it will by default also detect mail domains (_from accounts added via `setup email add`_), generating additional DKIM keys. + +DKIM is currently supported by either OpenDKIM or Rspamd: + +=== "OpenDKIM" + + OpenDKIM is currently [enabled by default][docs-env-opendkim]. + + After running `setup config dkim`, your new DKIM key files (_and OpenDKIM config_) have been added to `/tmp/docker-mailserver/opendkim/`. + + !!! info "Restart required" + + After restarting DMS, outgoing mail will now be signed with your new DKIM key(s) :tada: + +=== "Rspamd" + + Requires opt-in via [`ENABLE_RSPAMD=1`][docs-env-rspamd] (_and disable the default OpenDKIM: `ENABLE_OPENDKIM=0`_). + + Rspamd provides DKIM support through two separate modules: + + 1. [Verifying DKIM signatures from inbound mail][rspamd-docs-dkim-checks] is enabled by default. + 2. [Signing outbound mail with your DKIM key][rspamd-docs-dkim-signing] needs additional setup (key + dns + config). + + ??? warning "Using Multiple Domains" + + If you have multiple domains, you need to: + + - Create a key with `docker exec -it setup config dkim domain ` for each domain DMS should sign outgoing mail for. + - Provide a custom `dkim_signing.conf` (for which an example is shown below), as the default config only supports one domain. + + !!! info "About the Helper Script" + + The script will persist the keys in `/tmp/docker-mailserver/rspamd/dkim/`. Hence, if you are already using the default volume mounts, the keys are persisted in a volume. The script also restarts Rspamd directly, so changes take effect without restarting DMS. + + The script provides you with log messages along the way of creating keys. In case you want to read the complete log, use `-v` (verbose) or `-vv` (very verbose). + + --- + + In case you have not already provided a default DKIM signing configuration, the script will create one and write it to `/tmp/docker-mailserver/rspamd/override.d/dkim_signing.conf`. If this file already exists, it will not be overwritten. + + An example of what a default configuration file for DKIM signing looks like can be found by expanding the example below. + + ??? example "DKIM Signing Module Configuration Examples" + + A simple configuration could look like this: + + ```cf + # documentation: https://rspamd.com/doc/modules/dkim_signing.html + + enabled = true; + + sign_authenticated = true; + sign_local = true; + + use_domain = "header"; + use_redis = false; # don't change unless Redis also provides the DKIM keys + use_esld = true; + check_pubkey = true; # you want to use this in the beginning + + selector = "mail"; + # The path location is searched for a DKIM key with these variables: + # - `$domain` is sourced from the MIME mail message `From` header + # - `$selector` is configured for `mail` (as a default fallback) + path = "/tmp/docker-mailserver/dkim/keys/$domain/$selector.private"; + + # domain specific configurations can be provided below: + domain { + example.com { + path = "/tmp/docker-mailserver/rspamd/dkim/mail.private"; + selector = "mail"; + } + } + ``` + + As shown next: + + - You can add more domains into the `domain { ... }` section (in the following example: `example.com` and `example.org`). + - A domain can also be configured with multiple selectors and keys within a `selectors [ ... ]` array (in the following example, this is done for `example.org`). + + ```cf + # ... + + domain { + example.com { + path = /tmp/docker-mailserver/rspamd/example.com/ed25519.private"; + selector = "dkim-ed25519"; + } + example.org { + selectors [ + { + path = "/tmp/docker-mailserver/rspamd/dkim/example.org/rsa.private"; + selector = "dkim-rsa"; + }, + { + path = "/tmp/docker-mailserver/rspamd/dkim/example.org/ed25519.private"; + selector = "dkim-ed25519"; + } + ] + } + } + ``` + + ??? warning "Support for DKIM Keys using ED25519" + + This modern elliptic curve is supported by Rspamd, but support by third-parties for [verifying Ed25519 DKIM signatures is unreliable][dkim-ed25519-support]. + + If you sign your mail with this key type, you should include RSA as a fallback, like shown in the above example. + + ??? tip "Let Rspamd Check Your Keys" + + When `check_pubkey = true;` is set, Rspamd will query the DNS record for each DKIM selector, verifying each public key matches the private key configured. + + If there is a mismatch, a warning will be emitted to the Rspamd log `/var/log/mail/rspamd.log`. + +### DNS Record { #dkim-dns } + +When mail signed with your DKIM key is sent from your mail server, the receiver needs to check a DNS `TXT` record to verify the DKIM signature is trustworthy. + +!!! example "Configuring DNS - DKIM record" + + When you generated your key in the previous step, the DNS data was saved into a file `.txt` (default: `mail.txt`). Use this content to update your [DNS via Web Interface][dns::example-webui] or directly edit your [DNS Zone file][dns::wikipedia-zonefile]: + + === "Web Interface" + + Create a new record: + + | Field | Value | + | ----- | ------------------------------------------------------------------------------ | + | Type | `TXT` | + | Name | `._domainkey` (_default: `mail._domainkey`_) | + | TTL | Use the default (_otherwise [3600 seconds is appropriate][dns::digicert-ttl]_) | + | Data | File content within `( ... )` (_formatted as advised below_) | + + When using Rspamd, the helper script has already provided you with the contents (the "Data" field) of the DNS record you need to create - you can just copy-paste this text. + + === "DNS Zone file" + + `.txt` is already formatted as a snippet for adding to your [DNS Zone file][dns::wikipedia-zonefile]. + + Just copy/paste the file contents into your existing DNS zone. The `TXT` value has been split into separate strings every 255 characters for compatibility. + +??? info "`.txt` - Formatting the `TXT` record value correctly" + + This file was generated for use within a [DNS zone file][dns::wikipedia-zonefile]. The file name uses the DKIM selector it was generated with (default DKIM selector is `mail`, which creates `mail.txt`_). + + For your DNS setup, DKIM support needs to create a `TXT` record to store the public key for mail clients to use. `TXT` records with values that are longer than 255 characters need to be split into multiple parts. This is why the generated `.txt` file (_containing your public key for use with DKIM_) has multiple value parts wrapped within double-quotes between `(` and `)`. + + A DNS web-interface may handle this separation internally instead, and [could expect the value provided all as a single line][dns::webui-dkim] instead of split. When that is required, you'll need to manually format the value as described below. + + Your generated DNS record file (`.txt`) should look similar to this: + + ```txt + mail._domainkey IN TXT ( "v=DKIM1; k=rsa; " + "p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqQMMqhb1S52Rg7VFS3EC6JQIMxNDdiBmOKZvY5fiVtD3Z+yd9ZV+V8e4IARVoMXWcJWSR6xkloitzfrRtJRwOYvmrcgugOalkmM0V4Gy/2aXeamuiBuUc4esDQEI3egmtAsHcVY1XCoYfs+9VqoHEq3vdr3UQ8zP/l+FP5UfcaJFCK/ZllqcO2P1GjIDVSHLdPpRHbMP/tU1a9mNZ" + "5QMZBJ/JuJK/s+2bp8gpxKn8rh1akSQjlynlV9NI+7J3CC7CUf3bGvoXIrb37C/lpJehS39KNtcGdaRufKauSfqx/7SxA0zyZC+r13f7ASbMaQFzm+/RRusTqozY/p/MsWx8QIDAQAB" + ) ; + ``` + + Take the content between `( ... )`, and combine all the quote wrapped content and remove the double-quotes including the white-space between them. That is your `TXT` record value, the above example would become this: + + ```txt + v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqQMMqhb1S52Rg7VFS3EC6JQIMxNDdiBmOKZvY5fiVtD3Z+yd9ZV+V8e4IARVoMXWcJWSR6xkloitzfrRtJRwOYvmrcgugOalkmM0V4Gy/2aXeamuiBuUc4esDQEI3egmtAsHcVY1XCoYfs+9VqoHEq3vdr3UQ8zP/l+FP5UfcaJFCK/ZllqcO2P1GjIDVSHLdPpRHbMP/tU1a9mNZ5QMZBJ/JuJK/s+2bp8gpxKn8rh1akSQjlynlV9NI+7J3CC7CUf3bGvoXIrb37C/lpJehS39KNtcGdaRufKauSfqx/7SxA0zyZC+r13f7ASbMaQFzm+/RRusTqozY/p/MsWx8QIDAQAB + ``` + + To test that your new DKIM record is correct, query it with the `dig` command. The `TXT` value response should be a single line split into multiple parts wrapped in double-quotes: + + ```console + $ dig +short TXT mail._domainkey.example.com + "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqQMMqhb1S52Rg7VFS3EC6JQIMxNDdiBmOKZvY5fiVtD3Z+yd9ZV+V8e4IARVoMXWcJWSR6xkloitzfrRtJRwOYvmrcgugOalkmM0V4Gy/2aXeamuiBuUc4esDQEI3egmtAsHcVY1XCoYfs+9VqoHEq3vdr3UQ8zP/l+FP5UfcaJFCK/ZllqcO2P1GjIDVSHLdPpRHbMP/tU1a9mNZ5QMZBJ/JuJK/s+2bp8gpxKn8rh1akSQjlynlV9NI+7J3CC7CUf3bGvoXIrb37C/lpJehS39" "KNtcGdaRufKauSfqx/7SxA0zyZC+r13f7ASbMaQFzm+/RRusTqozY/p/MsWx8QIDAQAB" + ``` + +### Troubleshooting { #dkim-debug } + +[MxToolbox has a DKIM Verifier][mxtoolbox-dkim-verifier] that you can use to check your DKIM DNS record(s). + +When using Rspamd, we recommend you turn on `check_pubkey = true;` in `dkim_signing.conf`. Rspamd will then check whether your private key matches your public key, and you can check possible mismatches by looking at `/var/log/mail/rspamd.log`. + +## DMARC + +With DMS, DMARC is pre-configured out of the box. You may disable extra and excessive DMARC checks when using Rspamd via `ENABLE_OPENDMARC=0`. + +The only thing you need to do in order to enable DMARC on a "DNS-level" is to add new `TXT`. In contrast to [DKIM](#dkim), DMARC DNS entries do not require any keys, but merely setting the [configuration values][dmarc-howto-configtags]. You can either handcraft the entry by yourself or use one of available generators (like [this one][dmarc-tool-gca]). + +Typically something like this should be good to start with: + +```txt +_dmarc.example.com. IN TXT "v=DMARC1; p=none; sp=none; fo=0; adkim=r; aspf=r; pct=100; rf=afrf; ri=86400; rua=mailto:dmarc.report@example.com; ruf=mailto:dmarc.report@example.com" +``` + +Or a bit more strict policies (_mind `p=quarantine` and `sp=quarantine`_): + +```txt +_dmarc.example.com. IN TXT "v=DMARC1; p=quarantine; sp=quarantine; fo=0; adkim=r; aspf=r; pct=100; rf=afrf; ri=86400; rua=mailto:dmarc.report@example.com; ruf=mailto:dmarc.report@example.com" +``` + +The DMARC status may not be displayed instantly due to delays in DNS (caches). Dmarcian has [a few tools][dmarcian-tools] you can use to verify your DNS records. + +## SPF + +!!! quote "What is SPF" + + Sender Policy Framework (SPF) is a simple email-validation system designed to detect email spoofing by providing a mechanism to allow receiving mail exchangers to check that incoming mail from a domain comes from a host authorized by that domain's administrators. + + [Source][wikipedia-spf] + +!!! tip "Disabling the default SPF service `policy-spf`" + + Set [`ENABLE_POLICYD_SPF=0`][docs-env-spf-policyd] to opt-out of the default SPF service. Advised when Rspamd is configured to handle SPF instead. + +### Adding an SPF Record + +To add a SPF record in your DNS, insert the following line in your DNS zone: + +```txt +example.com. IN TXT "v=spf1 mx ~all" +``` + +This enables the _Softfail_ mode for SPF. You could first add this SPF record with a very low TTL. _SoftFail_ is a good setting for getting started and testing, as it lets all email through, with spams tagged as such in the mailbox. + +After verification, you _might_ want to change your SPF record to `v=spf1 mx -all` so as to enforce the _HardFail_ policy. See for more details about SPF policies. + +In any case, increment the SPF record's TTL to its final value. + +### Backup MX & Secondary MX for `policyd-spf` + +For whitelisting an IP Address from the SPF test, you can create a config file (see [`policyd-spf.conf`](https://www.linuxcertif.com/man/5/policyd-spf.conf)) and mount that file into `/etc/postfix-policyd-spf-python/policyd-spf.conf`. + +**Example:** Create and edit a `policyd-spf.conf` file at `docker-data/dms/config/postfix-policyd-spf.conf`: + +```conf +debugLevel = 1 +#0(only errors)-4(complete data received) + +skip_addresses = 127.0.0.0/8,::ffff:127.0.0.0/104,::1 + +# Preferably use IP-Addresses for whitelist lookups: +Whitelist = 192.168.0.0/31,192.168.1.0/30 +# Domain_Whitelist = mx1.not-example.com,mx2.not-example.com +``` + +Then add this line to `compose.yaml`: + +```yaml +volumes: + - ./docker-data/dms/config/postfix-policyd-spf.conf:/etc/postfix-policyd-spf-python/policyd-spf.conf +``` + +[docs-accounts]: ../account-management/overview.md#accounts +[docs-volumes-config]: ../advanced/optional-config.md#volumes-config +[docs-env-opendkim]: ../environment.md#enable_opendkim +[docs-env-rspamd]: ../environment.md#enable_rspamd +[docs-env-spf-policyd]: ../environment.md#enable_policyd_spf +[cloudflare-dkim-dmarc-spf]: https://www.cloudflare.com/learning/email-security/dmarc-dkim-spf/ +[rfc-8301]: https://datatracker.ietf.org/doc/html/rfc8301#section-3.2 +[gh-discussion::dkim-key-rotation-expiry]: https://github.com/orgs/docker-mailserver/discussions/4068#discussioncomment-9784263 +[gh-issue::dkim-length]: https://github.com/docker-mailserver/docker-mailserver/issues/1854#issuecomment-806280929 +[rspamd-docs-dkim-checks]: https://www.rspamd.com/doc/modules/dkim.html +[rspamd-docs-dkim-signing]: https://www.rspamd.com/doc/modules/dkim_signing.html +[dns::example-webui]: https://www.vultr.com/docs/introduction-to-vultr-dns/ +[dns::digicert-ttl]: https://www.digicert.com/faq/dns/what-is-ttl +[dns::wikipedia-zonefile]: https://en.wikipedia.org/wiki/Zone_file +[dns::webui-dkim]: https://serverfault.com/questions/763815/route-53-doesnt-allow-adding-dkim-keys-because-length-is-too-long +[dkim-ed25519-support]: https://serverfault.com/questions/1023674/is-ed25519-well-supported-for-the-dkim-validation/1074545#1074545 +[dkim-verification-expiry-refusal]: https://mxtoolbox.com/problem/dkim/dkim-signature-expiration +[mxtoolbox-dkim-verifier]: https://mxtoolbox.com/dkim.aspx +[dmarc-howto-configtags]: https://github.com/internetstandards/toolbox-wiki/blob/master/DMARC-how-to.md#overview-of-dmarc-configuration-tags +[dmarc-tool-gca]: https://dmarcguide.globalcyberalliance.org +[dmarcian-tools]: https://dmarcian.com/dmarc-tools/ +[wikipedia-dkim]: https://en.wikipedia.org/wiki/DomainKeys_Identified_Mail +[wikipedia-spf]: https://en.wikipedia.org/wiki/Sender_Policy_Framework diff --git a/docs/content/config/best-practices/dmarc.md b/docs/content/config/best-practices/dmarc.md deleted file mode 100644 index d8a2710b98e..00000000000 --- a/docs/content/config/best-practices/dmarc.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -title: 'Best Practices | DMARC' -hide: - - toc # Hide Table of Contents for this page ---- - -More information at [DMARC Guide][dmarc-howto]. - -## Enabling DMARC - -In `docker-mailserver`, DMARC is pre-configured out of the box. The only thing you need to do in order to enable it, is to add new `TXT` entry to your DNS. - -In contrast with [DKIM][docs-dkim], the DMARC DNS entry does not require any keys, but merely setting the [configuration values][dmarc-howto::configtags]. You can either handcraft the entry by yourself or use one of available generators (like [this one][dmarc-tool::gca]). - -Typically something like this should be good to start with (_don't forget to replace `@example.com` to your actual domain_): - -``` -_dmarc.example.com. IN TXT "v=DMARC1; p=none; rua=mailto:dmarc.report@example.com; ruf=mailto:dmarc.report@example.com; sp=none; ri=86400" -``` - -Or a bit more strict policies (_mind `p=quarantine` and `sp=quarantine`_): - -``` -_dmarc IN TXT "v=DMARC1; p=quarantine; rua=mailto:dmarc.report@example.com; ruf=mailto:dmarc.report@example.com; fo=0; adkim=r; aspf=r; pct=100; rf=afrf; ri=86400; sp=quarantine" -``` - -DMARC status is not being displayed instantly in Gmail for instance. If you want to check it directly after DNS entries, you can use some services around the Internet such as from [Global Cyber Alliance][dmarc-tool::gca] or [RedSift][dmarc-tool::redsift]. In other cases, email clients will show "DMARC: PASS" in ~1 day or so. - -Reference: [#1511][github-issue-1511] - -[docs-dkim]: ./dkim.md -[github-issue-1511]: https://github.com/docker-mailserver/docker-mailserver/issues/1511 -[dmarc-howto]: https://github.com/internetstandards/toolbox-wiki/blob/master/DMARC-how-to.md -[dmarc-howto::configtags]: https://github.com/internetstandards/toolbox-wiki/blob/master/DMARC-how-to.md#overview-of-dmarc-configuration-tags -[dmarc-tool::gca]: https://dmarcguide.globalcyberalliance.org -[dmarc-tool::redsift]: https://ondmarc.redsift.com diff --git a/docs/content/config/best-practices/mta-sts.md b/docs/content/config/best-practices/mta-sts.md new file mode 100644 index 00000000000..1aebbdc59b0 --- /dev/null +++ b/docs/content/config/best-practices/mta-sts.md @@ -0,0 +1,30 @@ +--- +title: 'Best practices | MTA-STS' +hide: + - toc # Hide Table of Contents for this page +--- + +MTA-STS is an optional mechanism for a domain to signal support for STARTTLS. + +- It can be used to prevent man-in-the-middle-attacks from hiding STARTTLS support that would force DMS to send outbound mail through an insecure connection. +- MTA-STS is an alternative to DANE without the need of DNSSEC. +- MTA-STS is supported by some of the biggest mail providers like Google Mail and Outlook. + +## Supporting MTA-STS for outbound mail + +Enable this feature via the ENV setting [`ENABLE_MTA_STS=1`](../environment.md#enable_mta_sts). + +!!! warning "If you have configured DANE" + + Enabling MTA-STS will by default override DANE if both are configured for a domain. + + This can be partially addressed by configuring a dane-only policy resolver before the MTA-STS entry in `smtp_tls_policy_maps`. See the [`postfix-mta-sts-resolver` documentation][postfix-mta-sts-resolver::dane] for further details. + +[postfix-mta-sts-resolver::dane]: https://github.com/Snawoot/postfix-mta-sts-resolver#warning-mta-sts-policy-overrides-dane-tls-authentication + +## Supporting MTA-STS for inbound mail + +While this feature in DMS supports ensuring STARTTLS is used when mail is sent to another mail server, you may setup similar for mail servers sending mail to DMS. + +This requires configuring your DNS and hosting the MTA-STS policy file via a webserver. A good introduction can be found on [dmarcian.com](https://dmarcian.com/mta-sts/). + diff --git a/docs/content/config/best-practices/spf.md b/docs/content/config/best-practices/spf.md deleted file mode 100644 index 308c3fe0888..00000000000 --- a/docs/content/config/best-practices/spf.md +++ /dev/null @@ -1,59 +0,0 @@ ---- -title: 'Best Practices | SPF' -hide: - - toc # Hide Table of Contents for this page ---- - -From [Wikipedia](https://en.wikipedia.org/wiki/Sender_Policy_Framework): - -!!! quote - Sender Policy Framework (SPF) is a simple email-validation system designed to detect email spoofing by providing a mechanism to allow receiving mail exchangers to check that incoming mail from a domain comes from a host authorized by that domain's administrators. The list of authorized sending hosts for a domain is published in the Domain Name System (DNS) records for that domain in the form of a specially formatted TXT record. Email spam and phishing often use forged "from" addresses, so publishing and checking SPF records can be considered anti-spam techniques. - -!!! note - For a more technical review: https://github.com/internetstandards/toolbox-wiki/blob/master/SPF-how-to.md - -## Add a SPF Record - -To add a SPF record in your DNS, insert the following line in your DNS zone: - -```txt -; MX record must be declared for SPF to work -example.com. IN MX 1 mail.example.com. - -; SPF record -example.com. IN TXT "v=spf1 mx ~all" -``` - -This enables the _Softfail_ mode for SPF. You could first add this SPF record with a very low TTL. - -_SoftFail_ is a good setting for getting started and testing, as it lets all email through, with spams tagged as such in the mailbox. - -After verification, you _might_ want to change your SPF record to `v=spf1 mx -all` so as to enforce the _HardFail_ policy. See http://www.open-spf.org/SPF_Record_Syntax for more details about SPF policies. - -In any case, increment the SPF record's TTL to its final value. - -## Backup MX, Secondary MX - -For whitelisting a IP Address from the SPF test, you can create a config file (see [`policyd-spf.conf`](https://www.linuxcertif.com/man/5/policyd-spf.conf)) and mount that file into `/etc/postfix-policyd-spf-python/policyd-spf.conf`. - -**Example:** - -Create and edit a `policyd-spf.conf` file at `docker-data/dms/config/postfix-policyd-spf.conf`: - -```conf -debugLevel = 1 -#0(only errors)-4(complete data received) - -skip_addresses = 127.0.0.0/8,::ffff:127.0.0.0/104,::1 - -# Preferably use IP-Addresses for whitelist lookups: -Whitelist = 192.168.0.0/31,192.168.1.0/30 -# Domain_Whitelist = mx1.not-example.com,mx2.not-example.com -``` - -Then add this line to `docker-compose.yml`: - -```yaml -volumes: - - ./docker-data/dms/config/postfix-policyd-spf.conf:/etc/postfix-policyd-spf-python/policyd-spf.conf -``` diff --git a/docs/content/config/debugging.md b/docs/content/config/debugging.md new file mode 100644 index 00000000000..fca962f16bf --- /dev/null +++ b/docs/content/config/debugging.md @@ -0,0 +1,137 @@ +--- +title: 'Debugging' +hide: + - toc +--- + +This page contains valuable information when it comes to resolving issues you encounter. + +!!! info "Contributions Welcome!" + + Please consider contributing solutions to the [FAQ][docs-faq] :heart: + +## Preliminary Checks + +- Check that all published DMS ports are actually open and not blocked by your ISP / hosting provider. +- SSL errors are likely the result of a wrong setup on the user side and not caused by DMS itself. +- Ensure that you have correctly started DMS. Many problems related to configuration are due to this. + +!!! danger "Correctly starting DMS" + + Use the [`--force-recreate`][docker-docs::force-recreate] option to avoid configuration mishaps: `docker compose up --force-recreate` + + Alternatively, always use `docker compose down` to stop DMS. **Do not** rely on `CTRL + C`, `docker compose stop`, or `docker compose restart`. + + --- + + DMS setup scripts are run when a container starts, but may fail to work properly if you do the following: + + - Stopping a container with commands like: `docker stop` or `docker compose up` stopped via `CTRL + C` instead of `docker compose down`. + - Restarting a container. + + Volumes persist data across container instances, however the same container instance will keep internal changes not stored in a volume until the container is removed. + + Due to this, DMS setup scripts may modify configuration it has already modified in the past. + + - This is brittle as some changes are naive by assuming they are applied to the original configs from the image. + - Volumes in `compose.yaml` are expected to persist any important data. Thus it should be safe to throwaway the container created each time, avoiding this config problem. + +### Mail sent from DMS does not arrive at destination + +Some service providers block outbound traffic on port 25. Common hosting providers known to have this issue: + +- [Azure](https://docs.microsoft.com/en-us/azure/virtual-network/troubleshoot-outbound-smtp-connectivity) +- [AWS EC2](https://aws.amazon.com/premiumsupport/knowledge-center/ec2-port-25-throttle/) +- [Vultr](https://www.vultr.com/docs/what-ports-are-blocked/) + +These links may advise how the provider can unblock the port through additional services offered, or via a support ticket request. + +### Mail sent to DMS does not get delivered to user + +Common logs related to this are: + +- `warning: do not list domain domain.fr in BOTH mydestination and virtual_mailbox_domains` +- `Recipient address rejected: User unknown in local recipient table` + +If your logs look like this, you likely have [assigned the same FQDN to the DMS `hostname` and your mail accounts][gh-issues::dms-fqdn-misconfigured] which is not supported by default. You can either adjust your DMS `hostname` or follow [this FAQ advice][docs::faq-bare-domain] + +It is also possible that [DMS services are temporarily unavailable][gh-issues::dms-services-unavailable] when configuration changes are detected, producing the 2nd error. Certificate updates may be a less obvious trigger. + +## Steps for Debugging DMS + +1. **Increase log verbosity**: Very helpful for troubleshooting problems during container startup. Set the environment variable [`LOG_LEVEL`][docs-environment-log-level] to `debug` or `trace`. +2. **Use error logs as a search query**: Try [finding an _existing issue_][gh-issues] or _search engine result_ from any errors in your container log output. Often you'll find answers or more insights. If you still need to open an issue, sharing links from your search may help us assist you. The mail server log can be acquired by running `docker log ` (_or `docker logs -f ` if you want to follow the log_). +3. **Inspect the logs of the service that is failing**: We provide a dedicated paragraph on this topic [further down below](#logs). +4. **Understand the basics of mail servers**: Especially for beginners, make sure you read our [Introduction][docs-introduction] and [Usage][docs-usage] articles. +5. **Search the whole FAQ**: Our [FAQ][docs-faq] contains answers for common problems. Make sure you go through the list. +6. **Reduce the scope**: Ensure that you can run a basic setup of DMS first. Then incrementally restore parts of your original configuration until the problem is reproduced again. If you're new to DMS, it is common to find the cause is misunderstanding how to configure a minimal setup. + +### Debug a running container + +#### General + +To get a shell inside the container run: `docker exec -it bash`. To install additional software, run: + +1. `apt-get update` to update repository metadata. +2. `apt-get install ` to install a package, e.g., `apt-get install neovim` if you want to use NeoVim instead of `nano` (which is shipped by default). + +#### Logs + +If you need more flexibility than what the `docker logs` command offers, then the most useful locations to get relevant DMS logs within the container are: + +- `/var/log/mail/.log` +- `/var/log/supervisor/.log` + +You may use `nano` (a text editor) to edit files, while `less` (a file viewer) and `tail`/`cat` are useful tools to inspect the contents of logs. + +## Compatibility + +It's possible that the issue you're experiencing is due to a compatibility conflict. + +This could be from outdated software, or running a system that isn't able to provide you newer software and kernels. You may want to verify if you can reproduce the issue on a system that is not affected by these concerns. + +### Network + +- Misconfigured network connections can cause the client IP address to be proxied through a docker network gateway IP, or a [service that acts on behalf of connecting clients for logins][gh-discuss-roundcube-fail2ban] where the connections client IP appears to be only from that service (eg: Container IP) instead. This can relay the wrong information to other services (eg: monitoring like Fail2Ban, SPF verification) causing unexpected failures. +- **`userland-proxy`:** Prior to Docker `v23`, [changing the `userland-proxy` setting did not reliably remove NAT rules][network::docker-userlandproxy]. +- **UFW / firewalld:** Some users expect only their firewall frontend to manage the firewall rules, but these will be bypassed when Docker publishes a container port (_as there is no integration between the two_). +- **`iptables` / `nftables`:** + - Docker [only manages the NAT rules via `iptables`][network::docker-nftables], relying on compatibility shims for supporting the successor `nftables`. Internally DMS expects `nftables` support on the host kernel for services like Fail2Ban to function correctly. + - [Kernels older than 5.2 may affect management of NAT rules via `nftables`][network::kernel-nftables]. Other software outside of DMS may also manipulate these rules, such as firewall frontends. +- **IPv6:** + - Requires [additional configuration][docs-ipv6] to prevent or properly support IPv6 connections (eg: Preserving the Client IP). + - Support in 2023 is still considered experimental. You are advised to use at least Docker Engine `v23` (2023Q1). + - Various networking bug fixes have been addressed since the initial IPv6 support arrived in Docker Engine `v20.10.0` (2020Q4). + +### System + +- **macOS:** DMS has limited support for macOS. Often an issue encountered is due to permissions related to the `volumes` config in `compose.yaml`. You may have luck [trying `gRPC FUSE`][gh-macos-support] as the file sharing implementation; [`VirtioFS` is the successor][docker-macos-virtiofs] but presently appears incompatible with DMS. +- **Kernel:** Some systems provide [kernels with modifications (_replacing defaults and backporting patches_)][network::kernels-modified] to support running legacy software or kernels, complicating compatibility. This can be commonly experienced with products like NAS. +- **CGroups v2:** Hosts running older kernels (prior to 5.2) and systemd (prior to v244) are not likely to leverage cgroup v2, or have not defaulted to the cgroup v2 `unified` hierarchy. Not meeting this baseline may influence the behavior of your DMS container, even with the latest Docker Engine installed. +- **Container runtime:** Docker and Podman for example have subtle differences. DMS docs are primarily focused on Docker, but we try to document known issues where relevant. +- **Rootless containers:** Introduces additional differences in behavior or requirements: + - cgroup v2 is required for supporting rootless containers. + - Differences such as for container networking which may further affect support for IPv6 and preserving the client IP (Remote address). Example with Docker rootless are [binding a port to a specific interface][docker-rootless-interface] and the choice of [port forwarding driver][docs::fail2ban::rootless-portdriver]. + +[network::docker-userlandproxy]: https://github.com/moby/moby/issues/44721 +[network::docker-nftables]: https://github.com/moby/moby/issues/26824 +[network::kernels-modified]: https://github.com/docker-mailserver/docker-mailserver/pull/2662#issuecomment-1168435970 +[network::kernel-nftables]: https://unix.stackexchange.com/questions/596493/can-nftables-and-iptables-ip6tables-rules-be-applied-at-the-same-time-if-so-wh/596497#596497 + +[docs-environment-log-level]: ./environment.md#log_level +[docs-faq]: ../faq.md +[docs::faq-bare-domain]: ../faq.md#can-i-use-a-nakedbare-domain-ie-no-hostname +[docs-ipv6]: ./advanced/ipv6.md +[docs-introduction]: ../introduction.md +[docs::fail2ban::rootless-portdriver]: ./security/fail2ban.md#rootless-container +[docs-usage]: ../usage.md + +[gh-issues]: https://github.com/docker-mailserver/docker-mailserver/issues +[gh-issues::dms-fqdn-misconfigured]: https://github.com/docker-mailserver/docker-mailserver/issues/3679#issuecomment-1837609043 +[gh-issues::dms-services-unavailable]: https://github.com/docker-mailserver/docker-mailserver/issues/3679#issuecomment-1848083358 +[gh-macos-support]: https://github.com/docker-mailserver/docker-mailserver/issues/3648#issuecomment-1822774080 +[gh-discuss-roundcube-fail2ban]: https://github.com/orgs/docker-mailserver/discussions/3273#discussioncomment-5654603 + +[docker-rootless-interface]: https://github.com/moby/moby/issues/45742 +[docker-macos-virtiofs]: https://www.docker.com/blog/speed-boost-achievement-unlocked-on-docker-desktop-4-6-for-mac/ +[docker-docs::force-recreate]: https://docs.docker.com/compose/reference/up/ diff --git a/docs/content/config/environment.md b/docs/content/config/environment.md index 47cffe03311..3b9b73ca480 100644 --- a/docs/content/config/environment.md +++ b/docs/content/config/environment.md @@ -4,18 +4,32 @@ title: Environment Variables !!! info - Values in **bold** are the default values. If an option doesn't work as documented here, check if you are running the latest image. The current `master` branch corresponds to the image `mailserver/docker-mailserver:edge`. + Values in **bold** are the default values. If an option doesn't work as documented here, check if you are running the latest image. The current `master` branch corresponds to the image `ghcr.io/docker-mailserver/docker-mailserver:edge`. + +!!! tip + + If an environment variable `__FILE` is set with a valid file path as the value, the content of that file will become the value for `` (_provided `` has not already been set_). #### General ##### OVERRIDE_HOSTNAME -- **empty** => uses the `hostname` command to get canonical hostname for `docker-mailserver` to use. -- => Specify a fully-qualified domainname to serve mail for. This is used for many of the config features so if you can't set your hostname (_eg: you're in a container platform that doesn't let you_) specify it via this environment variable. It will take priority over `docker run` options: `--hostname` and `--domainname`, or `docker-compose.yml` config equivalents: `hostname:` and `domainname:`. +If you cannot set your DMS FQDN as your hostname (_eg: you're in a container runtime lacks the equivalent of Docker's `--hostname`_), specify it via this environment variable. + +- **empty** => Internally uses the `hostname --fqdn` command to get the canonical hostname assigned to the DMS container. +- => Specify an FQDN (fully-qualified domain name) to serve mail for. The hostname is required for DMS to function correctly. + +!!! info -##### DMS_DEBUG + `OVERRIDE_HOSTNAME` is checked early during DMS container setup. When set it will be preferred over querying the containers hostname via the `hostname --fqdn` command (_configured via `docker run --hostname` or the equivalent `hostname:` field in `compose.yaml`_). -**This environment variable was removed in `v11.0.0`!** Use `LOG_LEVEL` instead. +!!! warning "Compatibility may differ" + + `OVERRIDE_HOSTNAME` is not a complete replacement for adjusting the containers configured hostname. It is a best effort workaround for supporting deployment environments like Kubernetes or when using Docker with `--network=host`. + + Typically this feature is only useful when software supports configuring a specific hostname to use, instead of a default fallback that infers the hostname (such as retrieving the hostname via libc / NSS). [Fetchmail is known to be incompatible][gh--issue::hostname-compatibility] with this ENV, requiring manual workarounds. + + Compatibility differences are being [tracked here][gh-issue::dms-fqdn] as they become known. ##### LOG_LEVEL @@ -35,23 +49,32 @@ Here you can adjust the [log-level for Supervisor](http://supervisord.org/loggin The log-level will show everything in its class and above. -##### ONE_DIR +##### DMS_VMAIL_UID -- 0 => state in default directories. -- **1** => consolidate all states into a single directory (`/var/mail-state`) to allow persistence using docker volumes. See the [related FAQ entry][docs-faq-onedir] for more information. +Default: 5000 -##### ACCOUNT_PROVISIONER +The User ID assigned to the static vmail user for `/var/mail` (_Mail storage managed by Dovecot_). -Configures the provisioning source of user accounts (including aliases) for user queries and authentication by services managed by DMS (_Postfix and Dovecot_). +!!! warning "Incompatible UID values" -User provisioning via OIDC is planned for the future, see [this tracking issue](https://github.com/docker-mailserver/docker-mailserver/issues/2713). + - A value of [`0` (root) is not compatible][gh-issue::vmail-uid-cannot-be-root]. + - This feature will attempt to adjust the `uid` for the `docker` user (`/etc/passwd`), hence the error emitted to logs if the UID is already assigned to another user. + - The feature appears to work with other UID values that are already assigned in `/etc/passwd`, even though Dovecot by default has a setting for the minimum UID as `500`. -- **empty** => use FILE +##### DMS_VMAIL_GID + +Default: 5000 + +The Group ID assigned to the static vmail group for `/var/mail` (_Mail storage managed by Dovecot_). + +##### ACCOUNT_PROVISIONER + +Configures the [provisioning source of user accounts][docs::account-management::overview] (including aliases) for user queries and authentication by services managed by DMS (_Postfix and Dovecot_). + +- **FILE** => use local files - LDAP => use LDAP authentication -- OIDC => use OIDC authentication (**not yet implemented**) -- FILE => use local files (this is used as the default) -A second container for the ldap service is necessary (e.g. [docker-openldap](https://github.com/osixia/docker-openldap)) +LDAP requires an external service (e.g. [`bitnami/openldap`](https://hub.docker.com/r/bitnami/openldap/)). ##### PERMIT_DOCKER @@ -69,37 +92,6 @@ Note: you probably want to [set `POSTFIX_INET_PROTOCOLS=ipv4`](#postfix_inet_pro Set the timezone. If this variable is unset, the container runtime will try to detect the time using `/etc/localtime`, which you can alternatively mount into the container. The value of this variable must follow the pattern `AREA/ZONE`, i.e. of you want to use Germany's time zone, use `Europe/Berlin`. You can lookup all available timezones [here](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List). -##### ENABLE_RSPAMD - -Enable or disable Rspamd. - -!!! warning "Current State" - - Rspamd-support is under active development. Be aware that breaking changes can happen at any time. To get more information, see [the detailed documentation page for Rspamd][docs-rspamd]. - -- **0** => disabled -- 1 => enabled - -##### ENABLE_RSPAMD_REDIS - -Explicit control over running a Redis instance within the container. By default, this value will match what is set for [`ENABLE_RSPAMD`](#enable_rspamd). - -The purpose of this setting is to opt-out of starting an internal Redis instance when enabling Rspamd, replacing it with your own external instance. - -??? note "Configuring rspamd for an external Redis instance" - - You will need to [provide configuration][config-rspamd-redis] at `/etc/rspamd/local.d/redis.conf` similar to: - - ``` - servers = "redis.example.test:6379"; - expand_keys = true; - ``` - -[config-rspamd-redis]: https://rspamd.com/doc/configuration/redis.html - -- 0 => Disabled -- 1 => Enabled - ##### ENABLE_AMAVIS Amavis content filter (used for ClamAV & SpamAssassin) @@ -129,6 +121,15 @@ This enables DNS block lists in _Postscreen_. If you want to know which lists we - **0** => DNS block lists are disabled - 1 => DNS block lists are enabled +##### ENABLE_MTA_STS + +Enables MTA-STS support for outbound mail. + +- **0** => Disabled +- 1 => Enabled + +See [MTA-STS](best-practices/mta-sts.md) for further explanation. + ##### ENABLE_OPENDKIM Enables the OpenDKIM service. @@ -143,11 +144,23 @@ Enables the OpenDMARC service. - **1** => Enabled - 0 => Disabled +##### ENABLE_POLICYD_SPF + +Enabled `policyd-spf` in Postfix's configuration. You will likely want to set this to `0` in case you're using Rspamd ([`ENABLE_RSPAMD=1`](#enable_rspamd)). + +- 0 => Disabled +- **1** => Enabled + ##### ENABLE_POP3 -- **empty** => POP3 service disabled +- **0** => POP3 service disabled - 1 => Enables POP3 service +##### ENABLE_IMAP + +- 0 => Disabled +- **1** => Enabled + ##### ENABLE_CLAMAV - **0** => ClamAV is disabled @@ -158,7 +171,7 @@ Enables the OpenDMARC service. - **0** => fail2ban service disabled - 1 => Enables fail2ban service -If you enable Fail2Ban, don't forget to add the following lines to your `docker-compose.yml`: +If you enable Fail2Ban, don't forget to add the following lines to your `compose.yaml`: ``` BASH cap_add: @@ -197,19 +210,27 @@ Please read [the SSL page in the documentation][docs-tls] for more information. ##### TLS_LEVEL - **empty** => modern -- modern => Enables TLSv1.2 and modern ciphers only. (default) -- intermediate => Enables TLSv1, TLSv1.1 and TLSv1.2 and broad compatibility ciphers. +- `modern` => Limits the cipher suite to secure ciphers only. +- `intermediate` => Relaxes security by adding additional ciphers for broader compatibility. + +!!! info + + In both cases TLS v1.2 is the minimum protocol version supported. + +!!! note + + Prior to DMS v12.0, `TLS_LEVEL=intermediate` additionally supported TLS versions 1.0 and 1.1. If you still have legacy devices that can only use these versions of TLS, please follow [this workaround advice][gh-issue::tls-legacy-workaround]. ##### SPOOF_PROTECTION Configures the handling of creating mails with forged sender addresses. - **0** => (not recommended) Mail address spoofing allowed. Any logged in user may create email messages with a [forged sender address](https://en.wikipedia.org/wiki/Email_spoofing). -- 1 => Mail spoofing denied. Each user may only send with his own or his alias addresses. Addresses with [extension delimiters](http://www.postfix.org/postconf.5.html#recipient_delimiter) are not able to send messages. +- 1 => Mail spoofing denied. Each user may only send with their own or their alias addresses. Addresses with [extension delimiters](http://www.postfix.org/postconf.5.html#recipient_delimiter) are not able to send messages. ##### ENABLE_SRS -Enables the Sender Rewriting Scheme. SRS is needed if `docker-mailserver` acts as forwarder. See [postsrsd](https://github.com/roehling/postsrsd/blob/master/README.md#sender-rewriting-scheme-crash-course) for further explanation. +Enables the Sender Rewriting Scheme. SRS is needed if DMS acts as forwarder. See [postsrsd](https://github.com/roehling/postsrsd/blob/main/README.rst) for further explanation. - **0** => Disabled - 1 => Enabled @@ -237,9 +258,9 @@ Provide any valid URI. Examples: - `lmtps:inet::` (secure lmtp with starttls) - `lmtp::2003` (use kopano as mailstore) -##### POSTFIX\_MAILBOX\_SIZE\_LIMIT +##### POSTFIX_MAILBOX_SIZE_LIMIT -Set the mailbox size limit for all users. If set to zero, the size will be unlimited (default). +Set the mailbox size limit for all users. If set to zero, the size will be unlimited (default). Size is in bytes. - **empty** => 0 (no limit) @@ -248,11 +269,17 @@ Set the mailbox size limit for all users. If set to zero, the size will be unlim - **1** => Dovecot quota is enabled - 0 => Dovecot quota is disabled -See [mailbox quota][docs-accounts]. +See [mailbox quota][docs-accounts-quota]. + +!!! info "Compatibility" + + This feature is presently only compatible with `ACCOUNT_PROVISIONER=FILE`. + + When using a different provisioner (or `SMTP_ONLY=1`) this ENV will instead default to `0`. -##### POSTFIX\_MESSAGE\_SIZE\_LIMIT +##### POSTFIX_MESSAGE_SIZE_LIMIT -Set the message size limit for all users. If set to zero, the size will be unlimited (not recommended!) +Set the message size limit for all users. If set to zero, the size will be unlimited (not recommended!). Size is in bytes. - **empty** => 10240000 (~10 MB) @@ -298,7 +325,14 @@ Customize the update check interval. Number + Suffix. Suffix must be 's' for sec - sdbox => (experimental) uses Dovecot high-performance mailbox format, one file contains one message - mdbox ==> (experimental) uses Dovecot high-performance mailbox format, multiple messages per file and multiple files per box -This option has been added in November 2019. Using other format than Maildir is considered as experimental in docker-mailserver and should only be used for testing purpose. For more details, please refer to [Dovecot Documentation](https://wiki2.dovecot.org/MailboxFormat). +This option has been added in November 2019. Using other format than Maildir is considered as experimental in docker-mailserver and should only be used for testing purpose. For more details, please refer to [Dovecot Documentation](https://doc.dovecot.org/admin_manual/mailbox_formats/#mailbox-formats). + +##### POSTFIX_REJECT_UNKNOWN_CLIENT_HOSTNAME + +If enabled, employs `reject_unknown_client_hostname` to sender restrictions in Postfix's configuration. + +- **0** => Disabled +- 1 => Enabled ##### POSTFIX_INET_PROTOCOLS @@ -316,6 +350,160 @@ Note: More details at Note: More information at +##### MOVE_SPAM_TO_JUNK + +- 0 => Spam messages will be delivered to the inbox. +- **1** => Spam messages will be delivered to the Junk mailbox. + +Routes mail identified as spam into the recipient(s) Junk mailbox (_a specialized folder for junk associated to the [special-use flag `\Junk`][docs::dovecot::special-use-flag], handled via a Dovecot sieve script internally_). + +[docs::dovecot::special-use-flag]: ../examples/use-cases/imap-folders.md + +!!! info + + Mail is received as spam when it has been marked with either header: + + - `X-Spam: Yes` (_added by Rspamd_) + - `X-Spam-Flag: YES` (_added by SpamAssassin - requires [`SPAMASSASSIN_SPAM_TO_INBOX=1`](#spamassassin_spam_to_inbox)_) + +##### MARK_SPAM_AS_READ + +- **0** => disabled +- 1 => Spam messages will be marked as read + +Enable to treat received spam as "read" (_avoids notification to MUA client of new mail_). + +!!! info + + Mail is received as spam when it has been marked with either header: + + - `X-Spam: Yes` (_added by Rspamd_) + - `X-Spam-Flag: YES` (_added by SpamAssassin - requires [`SPAMASSASSIN_SPAM_TO_INBOX=1`](#spamassassin_spam_to_inbox)_) + +##### SPAM_SUBJECT + +This variable defines a prefix for e-mails tagged with the `X-Spam: Yes` (Rspamd) or `X-Spam-Flag: YES` (SpamAssassin/Amavis) header. + +Default: empty (no prefix will be added to e-mails) + +??? example "Including trailing white-space" + + Add trailing white-space by quote wrapping the value: `SPAM_SUBJECT='[SPAM] '` + +##### DMS_CONFIG_POLL + +Defines how often DMS polls [monitored config files][gh::monitored-configs] for changes in the DMS Config Volume. This also includes TLS certificates and is often relied on for applying changes managed via `setup` CLI commands. + +- **`2`** => How often (in seconds) [change detection][gh::check-for-changes] is performed. + +!!! note "Decreasing the frequency of polling for changes" + + Raising the value will delay how soon a change is detected which may impact UX expectations for responsiveness, but reduces resource usage when changes are rare. + +!!! info + + When using `ACCOUNT_PROVISIONER=LDAP`, the change detection feature is presently disabled. + +[gh::check-for-changes]: https://github.com/docker-mailserver/docker-mailserver/blob/v15.0.0/target/scripts/check-for-changes.sh#L37 +[gh::monitored-configs]: https://github.com/docker-mailserver/docker-mailserver/blob/v15.0.0/target/scripts/helpers/change-detection.sh#L30-L42 + +#### Rspamd + +##### ENABLE_RSPAMD + +Enable or disable [Rspamd][docs-rspamd]. + +- **0** => disabled +- 1 => enabled + +##### ENABLE_RSPAMD_REDIS + +Explicit control over running a Redis instance within the container. By default, this value will match what is set for [`ENABLE_RSPAMD`](#enable_rspamd). + +The purpose of this setting is to opt-out of starting an internal Redis instance when enabling Rspamd, replacing it with your own external instance. + +??? note "Configuring Rspamd for an external Redis instance" + + You will need to [provide configuration][rspamd-redis-config] at `/etc/rspamd/local.d/redis.conf` similar to: + + ``` + servers = "redis.example.test:6379"; + ``` + +[rspamd-redis-config]: https://rspamd.com/doc/configuration/redis.html + +- 0 => Disabled +- 1 => Enabled + +##### RSPAMD_CHECK_AUTHENTICATED + +This settings controls whether checks should be performed on emails coming from authenticated users (i.e. most likely outgoing emails). The default value is `0` in order to align better with SpamAssassin. **We recommend** reading through [the Rspamd documentation on scanning outbound emails][rspamd-scanning-outbound] though to decide for yourself whether you need and want this feature. + +!!! note "Not all checks and actions are disabled" + + DKIM signing of e-mails will still happen. + +- **0** => No checks will be performed for authenticated users +- 1 => All default checks will be performed for authenticated users + +[rspamd-scanning-outbound]: https://rspamd.com/doc/tutorials/scanning_outbound.html + +##### RSPAMD_GREYLISTING + +Controls whether the [Rspamd Greylisting module][rspamd-greylisting-module] is enabled. This module can further assist in avoiding spam emails by [greylisting] e-mails with a certain spam score. + +- **0** => Disabled +- 1 => Enabled + +[rspamd-greylisting-module]: https://rspamd.com/doc/modules/greylisting.html +[greylisting]: https://en.wikipedia.org/wiki/Greylisting_(email) + +##### RSPAMD_LEARN + +When enabled, + +1. the "[autolearning][rspamd-autolearn]" feature is turned on; +2. the Bayes classifier will be trained (with the help of Sieve scripts) when moving mails + 1. from anywhere to the `Junk` folder (learning this email as spam); + 2. from the `Junk` folder into the `INBOX` (learning this email as ham). + +!!! warning "Attention" + + As of now, the spam learning database is global (i.e. available to all users). If one user deliberately trains it with malicious data, then it will ruin your detection rate. + + This feature is suitably only for users who can tell ham from spam and users that can be trusted. + +[rspamd-autolearn]: https://rspamd.com/doc/configuration/statistic.html#autolearning + +- **0** => Disabled +- 1 => Enabled + +##### RSPAMD_HFILTER + +Can be used to enable or disable the [Hfilter group module][rspamd-docs-hfilter-group-module]. This is used by DMS to adjust the `HFILTER_HOSTNAME_UNKNOWN` symbol, increasing its default weight to act similar to Postfix's `reject_unknown_client_hostname`, without the need to outright reject a message. + +- 0 => Disabled +- **1** => Enabled + +[rspamd-docs-hfilter-group-module]: https://www.rspamd.com/doc/modules/hfilter.html + +##### RSPAMD_HFILTER_HOSTNAME_UNKNOWN_SCORE + +Can be used to control the score when the [`HFILTER_HOSTNAME_UNKNOWN` symbol](#rspamd_hfilter) applies. A higher score is more punishing. Setting it to 15 (the default score for rejecting an e-mail) is equivalent to rejecting the email when the check fails. + +Default: 6 (which corresponds to the `add_header` action) + + +##### RSPAMD_NEURAL + +Can be used to enable or disable the [Neural network module][rspamd-docs-neural-network]. This is an experimental anti-spam weigh method using three neural networks in the configuration added here. As far as we can tell it trains itself by using other modules to find out what spam is. It will take a while (a week or more) to train its first neural network. The config trains new networks all the time and discards old networks. +Since it is experimental, it is switched off by default. + +- **0** => Disabled +- 1 => Enabled + +[rspamd-docs-neural-network]: https://www.rspamd.com/doc/modules/neural.html + #### Reports ##### PFLOGSUMM_TRIGGER @@ -393,12 +581,18 @@ Changes the interval in which log files are rotated. The entire log output for the container is still available via `docker logs mailserver` (or your respective container name). If you want to configure external log rotation for that container output as well, : [Docker Logging Drivers](https://docs.docker.com/config/containers/logging/configure/). - By default, the logs are lost when the container is destroyed (eg: re-creating via `docker-compose down && docker-compose up -d`). To keep the logs, mount a volume (to `/var/log/mail/`). + By default, the logs are lost when the container is destroyed (eg: re-creating via `docker compose down && docker compose up -d`). To keep the logs, mount a volume (to `/var/log/mail/`). !!! note This variable can also determine the interval for Postfix's log summary reports, see [`PFLOGSUMM_TRIGGER`](#pflogsumm_trigger). +##### LOGROTATE_COUNT + +Defines how many files are kept by logrotate. + +- **4** => Number of files + #### SpamAssassin ##### ENABLE_SPAMASSASSIN @@ -406,71 +600,153 @@ Changes the interval in which log files are rotated. - **0** => SpamAssassin is disabled - 1 => SpamAssassin is enabled -##### SPAMASSASSIN_SPAM_TO_INBOX +??? info "SpamAssassin analyzes incoming mail and assigns a spam score" -- 0 => Spam messages will be bounced (_rejected_) without any notification (_dangerous_). -- **1** => Spam messages will be delivered to the inbox and tagged as spam using `SA_SPAM_SUBJECT`. + Integration with Amavis involves processing mail based on the assigned spam score via [`SA_TAG`, `SA_TAG2` and `SA_KILL`][amavis-docs::spam-score]. -##### ENABLE_SPAMASSASSIN_KAM + These settings have equivalent ENV supported by DMS for easy adjustments, as documented below. -[KAM](https://mcgrail.com/template/projects#KAM1) is a 3rd party SpamAssassin ruleset, provided by the McGrail Foundation. If SpamAssassin is enabled, KAM can be used in addition to the default ruleset. +[amavis-docs::spam-score]: https://www.ijs.si/software/amavisd/amavisd-new-docs.html#tagkill + +##### ENABLE_SPAMASSASSIN_KAM - **0** => KAM disabled - 1 => KAM enabled -##### MOVE_SPAM_TO_JUNK +[KAM](https://mcgrail.com/template/projects#KAM1) is a 3rd party SpamAssassin ruleset, provided by the McGrail Foundation. If SpamAssassin is enabled, KAM can be used in addition to the default ruleset. + +##### SPAMASSASSIN_SPAM_TO_INBOX + +- 0 => (_Amavis action: `D_BOUNCE`_): Spam messages will be bounced (_rejected_) without any notification (_dangerous_). +- **1** => (_Amavis action: `D_PASS`_): Spam messages will be delivered to the inbox. + +!!! note + + The Amavis action configured by this setting: -Spam messages can be moved in the Junk folder. -Note: this setting needs `SPAMASSASSIN_SPAM_TO_INBOX=1` + - Influences the behavior of the [`SA_KILL`](#sa_kill) setting. + - Applies to the Amavis config parameters `$final_spam_destiny` and `$final_bad_header_destiny`. -- 0 => Spam messages will be delivered in the mailbox. -- **1** => Spam messages will be delivered in the `Junk` folder. +!!! note "This ENV setting is related to" + + - [`MOVE_SPAM_TO_JUNK=1`](#move_spam_to_junk) + - [`MARK_SPAM_AS_READ=1`](#mark_spam_as_read) + - [`SPAM_SUBJECT`](#spam_subject) ##### SA_TAG -- **2.0** => add spam info headers if at, or above that level +- **2.0** => add 'spam info' headers at, or above this spam score + +Mail is not yet considered spam at this spam score, but for purposes like diagnostics it can be useful to identify mail with a spam score at a lower bound than `SA_TAG2`. + +??? example "`X-Spam` headers appended to mail" + + Send a simple mail to a local DMS account `hello@example.com`: -Note: this SpamAssassin setting needs `ENABLE_SPAMASSASSIN=1` + ```bash + docker exec dms swaks --server 0.0.0.0 --to hello@example.com --body 'spam' + ``` + + Inspecting the raw mail you will notice several `X-Spam` headers were added to the mail like this: + + ``` + X-Spam-Flag: NO + X-Spam-Score: 4.162 + X-Spam-Level: **** + X-Spam-Status: No, score=4.162 tagged_above=2 required=4 + tests=[BODY_SINGLE_WORD=1, DKIM_ADSP_NXDOMAIN=0.8, + NO_DNS_FOR_FROM=0.379, NO_RECEIVED=-0.001, NO_RELAYS=-0.001] + autolearn=no autolearn_force=no + ``` + + !!! info "The `X-Spam-Score` is `4.162`" + + High enough for `SA_TAG` to trigger adding these headers, but not high enough for `SA_TAG2` (_which would set `X-Spam-Flag: YES` instead_). ##### SA_TAG2 -- **6.31** => add 'spam detected' headers at that level +- **6.31** => add 'spam detected' headers at, or above this level + +When a spam score is high enough, mark mail as spam (_Appends the mail header: `X-Spam-Flag: YES`_). -Note: this SpamAssassin setting needs `ENABLE_SPAMASSASSIN=1` +!!! info "Interaction with other ENV" + + - [`SPAM_SUBJECT`](#spam_subject) modifies the mail subject to better communicate spam mail to the user. + - [`MOVE_SPAM_TO_JUNK=1`](#move_spam_to_junk): The mail is still delivered, but to the recipient(s) junk folder instead. This feature reduces the usefulness of `SPAM_SUBJECT`. ##### SA_KILL -- **6.31** => triggers spam evasive actions +- **10.0** => quarantine + triggers action to handle spam + +Controls the spam score threshold for triggering an action on mail that has a high spam score. + +??? tip "Choosing an appropriate `SA_KILL` value" + + The value should be high enough to be represent confidence in mail as spam: + + - Too low: The action taken may prevent legitimate mail (ham) that was incorrectly detected as spam from being delivered successfully. + - Too high: Allows more spam to bypass the `SA_KILL` trigger (_how to treat mail with high confidence that it is actually spam_). + + Experiences from DMS users with these settings has been [collected here][gh-issue::sa-tunables-insights], along with [some direct configuration guides][gh-issue::sa-tunables-guides] (_under "Resources for references"_). + +[gh-issue::sa-tunables-insights]: https://github.com/docker-mailserver/docker-mailserver/pull/3058#issuecomment-1420268148 +[gh-issue::sa-tunables-guides]: https://github.com/docker-mailserver/docker-mailserver/pull/3058#issuecomment-1416547911 + +??? info "Trigger action" + + DMS will configure Amavis with either of these actions based on the DMS [`SPAMASSASSIN_SPAM_TO_INBOX`](#spamassassin_spam_to_inbox) ENV setting: + + - `D_PASS` (**default**): + - Accept mail and deliver it to the recipient(s), despite the high spam score. A copy is still stored in quarantine. + - This is a good default to start with until you are more confident in an `SA_KILL` threshold that won't accidentally discard / bounce legitimate mail users are expecting to arrive but is detected as spam. + - `D_BOUNCE`: + - Additionally sends a bounce notification (DSN). + - The [DSN is suppressed][amavis-docs::actions] (_no bounce sent_) when the spam score exceeds the Amavis `$sa_dsn_cutoff_level` config setting (default: `10`). With the DMS `SA_KILL` default also being `10`, no DSN will ever be sent. + - `D_REJECT` / `D_DISCARD`: + - These two aren't configured by DMS, but are valid alternative action values if configuring Amavis directly. + +??? note "Quarantined mail" + + When mail has a spam score that reaches the `SA_KILL` threshold: -!!! note "This SpamAssassin setting needs `ENABLE_SPAMASSASSIN=1`" + - [It will be quarantined][amavis-docs::quarantine] regardless of the `SA_KILL` action to perform. + - With `D_PASS` the delivered mail also appends an `X-Quarantine-ID` mail header. The ID value of this header is part of the quarantined file name. - By default, `docker-mailserver` is configured to quarantine spam emails. + If emails are quarantined, they are compressed and stored at a location: - If emails are quarantined, they are compressed and stored in a location dependent on the `ONE_DIR` setting above. To inhibit this behaviour and deliver spam emails, set this to a very high value e.g. `100.0`. + - Default: `/var/lib/amavis/virusmails/` + - When the [`/var/mail-state/` volume][docs::dms-volumes-state] is present: `/var/mail-state/lib-amavis/virusmails/` - If `ONE_DIR=1` (default) the location is `/var/mail-state/lib-amavis/virusmails/`, or if `ONE_DIR=0`: `/var/lib/amavis/virusmails/`. These paths are inside the docker container. + !!! tip -##### SA_SPAM_SUBJECT + Easily list mail stored in quarantine with `find` and the quarantine path: -- **\*\*\*SPAM\*\*\*** => add tag to subject if spam detected + ```bash + find /var/lib/amavis/virusmails -type f + ``` -Note: this SpamAssassin setting needs `ENABLE_SPAMASSASSIN=1`. Add the SpamAssassin score to the subject line by inserting the keyword \_SCORE\_: **\*\*\*SPAM(\_SCORE\_)\*\*\***. +[amavis-docs::actions]: https://www.ijs.si/software/amavisd/amavisd-new-docs.html#actions +[amavis-docs::quarantine]: https://www.ijs.si/software/amavisd/amavisd-new-docs.html#quarantine ##### SA_SHORTCIRCUIT_BAYES_SPAM - **1** => will activate SpamAssassin short circuiting for bayes spam detection. -This will uncomment the respective line in ```/etc/spamassasin/local.cf``` +This will uncomment the respective line in `/etc/spamassassin/local.cf` -Note: activate this only if you are confident in your bayes database for identifying spam. +!!! warning + + Activate this only if you are confident in your bayes database for identifying spam. ##### SA_SHORTCIRCUIT_BAYES_HAM - **1** => will activate SpamAssassin short circuiting for bayes ham detection -This will uncomment the respective line in ```/etc/spamassasin/local.cf``` +This will uncomment the respective line in `/etc/spamassassin/local.cf` + +!!! warning -Note: activate this only if you are confident in your bayes database for identifying ham. + Activate this only if you are confident in your bayes database for identifying ham. #### Fetchmail @@ -485,16 +761,38 @@ Note: activate this only if you are confident in your bayes database for identif ##### FETCHMAIL_PARALLEL - **0** => `fetchmail` runs with a single config file `/etc/fetchmailrc` - **1** => `/etc/fetchmailrc` is split per poll entry. For every poll entry a separate fetchmail instance is started to allow having multiple imap idle configurations defined. +- **0** => `fetchmail` runs with a single config file `/etc/fetchmailrc` +- 1 => `/etc/fetchmailrc` is split per poll entry. For every poll entry a separate fetchmail instance is started to [allow having multiple imap idle connections per server][fetchmail-imap-workaround] (_when poll entries reference the same IMAP server_). + +[fetchmail-imap-workaround]: https://otremba.net/wiki/Fetchmail_(Debian)#Immediate_Download_via_IMAP_IDLE Note: The defaults of your fetchmailrc file need to be at the top of the file. Otherwise it won't be added correctly to all separate `fetchmail` instances. +#### Getmail -#### LDAP +##### ENABLE_GETMAIL + +Enable or disable `getmail`. + +- **0** => Disabled +- 1 => Enabled + +##### GETMAIL_POLL -##### ENABLE_LDAP +- **5** => `getmail` The number of minutes for the interval. Min: 1; Default: 5. -Deprecated. See [`ACCOUNT_PROVISIONER`](#account_provisioner). + +#### OAUTH2 + +##### ENABLE_OAUTH2 + +- **empty** => OAUTH2 authentication is disabled +- 1 => OAUTH2 authentication is enabled + +##### OAUTH2_INTROSPECTION_URL + +- => Specify the user info endpoint URL of the oauth2 provider (_eg: `https://oauth2.example.com/userinfo/`_) + +#### LDAP ##### LDAP_START_TLS @@ -504,8 +802,8 @@ Deprecated. See [`ACCOUNT_PROVISIONER`](#account_provisioner). ##### LDAP_SERVER_HOST - **empty** => mail.example.com -- => Specify the dns-name/ip-address where the ldap-server is listening, or an URI like `ldaps://mail.example.com` -- NOTE: If you going to use `docker-mailserver` in combination with `docker-compose.yml` you can set the service name here +- => Specify the `` / `` where the LDAP server is reachable via a URI like: `ldaps://mail.example.com`. +- Note: You must include the desired URI scheme (`ldap://`, `ldaps://`, `ldapi://`). ##### LDAP_SEARCH_BASE @@ -574,14 +872,13 @@ The following variables overwrite the default values for ```/etc/dovecot/dovecot ##### DOVECOT_DNPASS - **empty** => same as `LDAP_BIND_PW` -- => Password for LDAP dn sepecifified in `DOVECOT_DN`. +- => Password for LDAP dn specified in `DOVECOT_DN`. ##### DOVECOT_URIS - **empty** => same as `LDAP_SERVER_HOST` -- => Specify a space separated list of LDAP uris. -- Note: If the protocol is missing, `ldap://` will be used. -- Note: This deprecates `DOVECOT_HOSTS` (as it didn't allow to use LDAPS), which is currently still supported for backwards compatibility. +- => Specify a space separated list of LDAP URIs. +- Note: You must include the desired URI scheme (`ldap://`, `ldaps://`, `ldapi://`). ##### DOVECOT_LDAP_VERSION @@ -659,22 +956,26 @@ Note: This postgrey setting needs `ENABLE_POSTGREY=1` ##### SASLAUTHD_MECHANISMS -- **empty** => pam -- `ldap` => authenticate against ldap server -- `shadow` => authenticate against local user db -- `mysql` => authenticate against mysql db -- `rimap` => authenticate against imap server -- NOTE: can be a list of mechanisms like pam ldap shadow +DMS only implements support for these mechanisms: + +- **`ldap`** => Authenticate against an LDAP server +- `rimap` => Authenticate against an IMAP server ##### SASLAUTHD_MECH_OPTIONS - **empty** => None -- e.g. with SASLAUTHD_MECHANISMS rimap you need to specify the ip-address/servername of the imap server ==> xxx.xxx.xxx.xxx + +!!! info + + With `SASLAUTHD_MECHANISMS=rimap` you need to specify the ip-address / servername of the IMAP server, such as `SASLAUTHD_MECH_OPTIONS=127.0.0.1`. ##### SASLAUTHD_LDAP_SERVER -- **empty** => same as `LDAP_SERVER_HOST` -- Note: since version 10.0.0, you can specify a protocol here (like ldaps://); this deprecates SASLAUTHD_LDAP_SSL. +- **empty** => Use the same value as `LDAP_SERVER_HOST` + +!!! note + + You must include the desired URI scheme (`ldap://`, `ldaps://`, `ldapi://`). ##### SASLAUTHD_LDAP_START_TLS @@ -774,41 +1075,124 @@ you to replace both instead of just the envelope sender. - **empty** => Derived from [`OVERRIDE_HOSTNAME`](#override_hostname), `$DOMAINNAME` (internal), or the container's hostname - Set this if auto-detection fails, isn't what you want, or you wish to have a separate container handle DSNs -#### Default Relay Host +#### Relay Host + +Supported ENV for the [Relay Host][docs::relay-host] feature. + +!!! note "Prefer `DEFAULT_RELAY_HOST` instead of `RELAY_HOST`" + + This is advised unless you need support for sender domain opt-out (via `setup relay exclude-domain`). + + The implementation for `RELAY_HOST` is not compatible with LDAP. + +!!! tip "Opt-in for relay host support" + + Enable relaying only for specific sender domains instead by using `setup relay add-domain`. + + **NOTE:** Presently there is a caveat when relay host credentials are configured (_which is incompatible with opt-in_). ##### DEFAULT_RELAY_HOST -- **empty** => don't set default relayhost setting in main.cf -- default host and port to relay all mail through. - Format: `[example.com]:587` (don't forget the brackets if you need this to - be compatible with `$RELAY_USER` and `$RELAY_PASSWORD`, explained below). +Configures a default relay host. + +!!! info + + - All mail sent outbound from DMS will be relayed through the configured host, unless sender-dependent relayhost maps have been configured (_which have precedence_). + - The host value may optionally be wrapped in brackets (_skips DNS query for MX record_): `[mail.example.com]:587` vs `example.com:587` + +!!! abstract "Technical Details" -#### Multi-domain Relay Hosts + This ENV internally configures the Postfix `main.cf` setting: [`relayhost`][postfix-config::relayhost] ##### RELAY_HOST -- **empty** => don't configure relay host -- default host to relay mail through +Configures a default relay host. + +!!! note + + Expects a value like `mail.example.com`. Internally this will be wrapped to `[mail.example.com]`, so it should resolve to the MTA directly. + + !!! warning "Do not use with `DEFAULT_RELAY_HOST`" + + `RELAY_HOST` has precedence as it is configured with `sender_dependent_relayhost_maps`. + +!!! info + + - This is a legacy ENV. It is however required for the opt-out feature of `postfix-relaymap.cf` to work. + - Internal configuration however differs from `DEFAULT_RELAY_HOST`. + +!!! abstract "Technical Details" + + This feature is configured internally using the: + + - Postfix setting with config: [`sender_dependent_relayhost_maps = texthash:/etc/postfix/relayhost_map`][postfix-config::relayhost_maps] + - DMS Config volume support via: `postfix-relaymap.cf` (_generates `/etc/postfix/relayhost_map`_) + + All known mail domains managed by DMS will be configured to relay outbound mail to `RELAY_HOST` by adding them implicitly to `/etc/postfix/relayhost_map`, except for domains using the opt-out feature of `postfix-relaymap.cf`. ##### RELAY_PORT -- **empty** => 25 -- default port to relay mail through +Default => 25 -##### RELAY_USER +Support for configuring a different port than 25 for `RELAY_HOST` to use. + +!!! note -- **empty** => no default -- default relay username (if no specific entry exists in postfix-sasl-password.cf) + Requires `RELAY_HOST`. + +#### Relay Host Credentials + +!!! warning "Configuring relay host credentials enforces outbound authentication" + + Presently when `RELAY_USER` + `RELAY_PASSWORD` or `postfix-sasl-password.cf` are configured, all outbound mail traffic is configured to require a secure connection established and forbids the omission of credentials. + + Additional feature work is required to only enforce these requirements on mail sent through a configured relay host. + +##### RELAY_USER ##### RELAY_PASSWORD -- **empty** => no default -- password for default relay user +Provide the credentials to use with `RELAY_HOST` or `DEFAULT_RELAY_HOST`. + +!!! tip "Alternative credentials config" + + You may prefer to use `setup relay add-auth` to avoid risking ENV exposing secrets. + + - With the CLI command, you must provide relay credentials for each of your sender domains. + - Alternatively manually edit `postfix-sasl-password.cf` with the correct relayhost entry (_`DEFAULT_RELAY_HOST` value, or as defined in `/etc/postfix/relayhost_map`_) to provide credentials per relayhost configured. + +!!! abstract "Technical Details" + + Credentials for relay hosts are configured internally using the: + + - Postfix setting with config: [`smtp_sasl_password_maps = texthash:/etc/postfix/sasl_passwd`][postfix-config::sasl_passwd] + - DMS Config volume support via: `postfix-sasl-password.cf` (_generates `/etc/postfix/sasl_passwd`_) + + --- + + When `postfix-sasl-password.cf` is present, DMS will copy it internally to `/etc/postfix/sasl_passwd`. + + - DMS provides support for mapping credentials by sender domain: + - Explicitly via `setup relay add-auth` (_creates / updates `postfix-sasl-password.cf`_). + - Implicitly via the relay ENV support (_configures all known DMS managed domains to use the relay ENV_). + - Credentials can be explicitly configured for specific relay hosts instead of sender domains: + - Add the exact relayhost value (`host:port` / `[host]:port`) from the generated `/etc/postfix/relayhost_map`, or `main.cf:relayhost` (`DEFAULT_RELAY_HOST`). + - `setup relay ...` is missing support, you must instead add these manually to `postfix-sasl-password.cf`. + +[gh-issue::vmail-uid-cannot-be-root]: https://github.com/docker-mailserver/docker-mailserver/issues/4098#issuecomment-2257201025 [docs-rspamd]: ./security/rspamd.md -[docs-faq-onedir]: ../faq.md#what-about-docker-datadmsmail-state-folder-varmail-state-internally [docs-tls]: ./security/ssl.md [docs-tls-letsencrypt]: ./security/ssl.md#lets-encrypt-recommended [docs-tls-manual]: ./security/ssl.md#bring-your-own-certificates [docs-tls-selfsigned]: ./security/ssl.md#self-signed-certificates -[docs-accounts]: ./user-management/accounts.md#notes +[docs-accounts-quota]: ./account-management/overview.md#quotas +[docs::account-management::overview]: ./account-management/overview.md +[docs::relay-host]: ./advanced/mail-forwarding/relay-hosts.md +[docs::dms-volumes-state]: ./advanced/optional-config.md#volumes-state +[postfix-config::relayhost]: https://www.postfix.org/postconf.5.html#relayhost +[postfix-config::relayhost_maps]: https://www.postfix.org/postconf.5.html#sender_dependent_relayhost_maps +[postfix-config::sasl_passwd]: https://www.postfix.org/postconf.5.html#smtp_sasl_password_maps +[gh-issue::tls-legacy-workaround]: https://github.com/docker-mailserver/docker-mailserver/pull/2945#issuecomment-1949907964 +[gh-issue::hostname-compatibility]: https://github.com/docker-mailserver/docker-mailserver-helm/issues/168#issuecomment-2911782106 +[gh-issue::dms-fqdn]: https://github.com/docker-mailserver/docker-mailserver/issues/3520#issuecomment-1700191973 diff --git a/docs/content/config/pop3.md b/docs/content/config/pop3.md index 1acc8e85dc6..0cc9e17f600 100644 --- a/docs/content/config/pop3.md +++ b/docs/content/config/pop3.md @@ -4,7 +4,7 @@ hide: - toc # Hide Table of Contents for this page --- -If you want to use POP3(S), you have to add the ports 110 and/or 995 (TLS secured) and the environment variable `ENABLE_POP3` to your `docker-compose.yml`: +If you want to use POP3(S), you have to add the ports 110 and/or 995 (TLS secured) and the environment variable `ENABLE_POP3` to your `compose.yaml`: ```yaml mailserver: diff --git a/docs/content/config/security/fail2ban.md b/docs/content/config/security/fail2ban.md index 2bebf77ace5..a2df0937c18 100644 --- a/docs/content/config/security/fail2ban.md +++ b/docs/content/config/security/fail2ban.md @@ -4,116 +4,168 @@ hide: - toc # Hide Table of Contents for this page --- -Fail2Ban is installed automatically and bans IP addresses for 3 hours after 3 failed attempts in 10 minutes by default. +!!! quote "What is Fail2Ban (F2B)?" -## Configuration files + Fail2ban is an intrusion prevention software framework. Written in the Python programming language, it is designed to prevent against brute-force attacks. It is able to run on POSIX systems that have an interface to a packet-control system or firewall installed locally, such as \[NFTables\] or TCP Wrapper. -If you want to change this, you can easily edit our github example file: [`config-examples/fail2ban-jail.cf`][github-file-f2bjail]. + [Source][wikipedia-fail2ban] -You can do the same with the values from `fail2ban.conf`, e.g `dbpurgeage`. In that case you need to edit: [`config-examples/fail2ban-fail2ban.cf`][github-file-f2bconfig]. + [wikipedia-fail2ban]: https://en.wikipedia.org/wiki/Fail2ban -The configuration files need to be located at the root of the `/tmp/docker-mailserver/` volume bind (usually `./docker-data/dms/config/:/tmp/docker-mailserver/`). +## Configuration -This following configuration files from `/tmp/docker-mailserver/` will be copied during container startup. +Enabling Fail2Ban support can be done via ENV, but also requires granting at least the `NET_ADMIN` capability to interact with the kernel and ban IP addresses. -- `fail2ban-jail.cf` -> `/etc/fail2ban/jail.d/user-jail.local` -- `fail2ban-fail2ban.cf` -> `/etc/fail2ban/fail2ban.local` +!!! example -### Docker-compose config + === "Docker Compose" -Example configuration volume bind: + ```yaml title="compose.yaml" + services: + mailserver: + environment: + - ENABLE_FAIL2BAN=1 + cap_add: + - NET_ADMIN + ``` -```yaml - volumes: - - ./docker-data/dms/config/:/tmp/docker-mailserver/ -``` + === "Docker CLI" -!!! attention - `docker-mailserver` must be launched with the `NET_ADMIN` capability in order to be able to install the nftables rules that actually ban IP addresses. + ```bash + docker run --rm -it \ + --cap-add=NET_ADMIN \ + --env ENABLE_FAIL2BAN=1 + ``` - Thus either include `--cap-add=NET_ADMIN` in the `docker run` command, or the equivalent in `docker-compose.yml`: +!!! warning "Security risk of adding non-default capabilities" - ```yaml - cap_add: - - NET_ADMIN - ``` + DMS bundles F2B into the image for convenience to simplify integration and deployment. -## Running fail2ban in a rootless container + The [`NET_ADMIN`][security::cap-net-admin] and [`NET_RAW`][security::cap-net-raw] capabilities are not granted by default to the container root user, as they can be used to compromise security. -[`RootlessKit`][rootless::rootless-kit] is the _fakeroot_ implementation for supporting _rootless mode_ in Docker and Podman. By default RootlessKit uses the [`builtin` port forwarding driver][rootless::port-drivers], which does not propagate source IP addresses. + If this risk concerns you, it may be wiser to instead prefer only granting these capabilities to a dedicated Fail2Ban container ([example][lsio:f2b-image]). -It is necessary for `fail2ban` to have access to the real source IP addresses in order to correctly identify clients. This is achieved by changing the port forwarding driver to [`slirp4netns`][rootless::slirp4netns], which is slower than `builtin` but does preserve the real source IPs. +!!! bug "Running Fail2Ban on Older Kernels" -### Docker with `slirp4netns` port driver + DMS configures F2B to use [NFTables][network::nftables], not [IPTables (legacy)][network::iptables-legacy]. -For [rootless mode][rootless::docker] in Docker, create `~/.config/systemd/user/docker.service.d/override.conf` with the following content: + We have observed that older systems (for example NAS systems), do not support the modern NFTables rules. You will need to configure F2B to use legacy IPTables again, for example with the [`fail2ban-jail.cf`][github-file-f2bjail], see the [section on configuration further down below](#custom-files). -``` -[Service] -Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=slirp4netns" -``` +[security::cap-net-admin]: https://0xn3va.gitbook.io/cheat-sheets/container/escaping/excessive-capabilities#cap_net_admin +[security::cap-net-raw]: https://0xn3va.gitbook.io/cheat-sheets/container/escaping/excessive-capabilities#cap_net_raw +[lsio:f2b-image]: https://docs.linuxserver.io/images/docker-fail2ban +[network::nftables]: https://en.wikipedia.org/wiki/Nftables +[network::iptables-legacy]: https://developers.redhat.com/blog/2020/08/18/iptables-the-two-variants-and-their-relationship-with-nftables#two_variants_of_the_iptables_command -And then restart the daemon: +### DMS Defaults -```console -$ systemctl --user daemon-reload -$ systemctl --user restart docker -``` +DMS will automatically ban IP addresses of hosts that have generated 6 failed attempts over the course of the last week. The bans themselves last for one week. The Postfix jail is configured to use `mode = extra` in DMS. + +### Custom Files -!!! note +!!! question "What is [`docker-data/dms/config/`][docs::dms-volumes-config]?" - This changes the port driver for all rootless containers managed by Docker. +This following configuration files inside the `docker-data/dms/config/` volume will be copied inside the container during startup - Per container configuration is not supported, if you need that consider Podman instead. +1. `fail2ban-jail.cf` is copied to `/etc/fail2ban/jail.d/user-jail.local` + - with this file, you can adjust the configuration of individual jails and their defaults + - there is an example provided [in our repository on GitHub][github-file-f2bjail] +2. `fail2ban-fail2ban.cf` is copied to `/etc/fail2ban/fail2ban.local` + - with this file, you can adjust F2B behavior in general + - there is an example provided [in our repository on GitHub][github-file-f2bconfig] -### Podman with `slirp4netns` port driver +[docs::dms-volumes-config]: ../advanced/optional-config.md#volumes-config +[github-file-f2bjail]: https://github.com/docker-mailserver/docker-mailserver/blob/master/config-examples/fail2ban-jail.cf +[github-file-f2bconfig]: https://github.com/docker-mailserver/docker-mailserver/blob/master/config-examples/fail2ban-fail2ban.cf -[Rootless Podman][rootless::podman] requires adding the value `slirp4netns:port_handler=slirp4netns` to the `--network` CLI option, or `network_mode` setting in your `docker-compose.yml`. +### SASL +The `postfix` jail comes with `mode=extra` by default, which covers SASL login errors for our default SASL provider. Hence, the `postfix-sasl` jail has been disabled. If you switch to another SASL provider (e.g., SASLauthd), you may want to turn the `postfix-sasl` jail back on: -You must also add the ENV `NETWORK_INTERFACE=tap0`, because Podman uses a [hard-coded interface name][rootless::podman::interface] for `slirp4netns`. +```ini title="docker-data/dms/config/fail2ban-jail.cf" +[postfix-sasl] +enabled = true +``` +### Viewing All Bans -!!! example +When just running - ```yaml - services: - mailserver: - network_mode: "slirp4netns:port_handler=slirp4netns" - environment: - - ENABLE_FAIL2BAN=1 - - NETWORK_INTERFACE=tap0 - ... - ``` +```bash +setup fail2ban +``` -!!! note +the script will show all banned IP addresses. - `slirp4netns` is not compatible with user-defined networks. +To get a more detailed `status` view, run -## Manage bans +```bash +setup fail2ban status +``` -You can also manage and list the banned IPs with the [`setup.sh`][docs-setupsh] script. +### Managing Bans -### List bans +You can manage F2B with the `setup` script. The usage looks like this: -```sh -./setup.sh fail2ban +```bash +docker exec setup fail2ban [ ] ``` -### Un-ban +### Viewing the Log File -Here `192.168.1.15` is our banned IP. - -```sh -./setup.sh fail2ban unban 192.168.1.15 +```bash +docker exec setup fail2ban log ``` -[docs-setupsh]: ../setup.sh.md -[github-file-f2bjail]: https://github.com/docker-mailserver/docker-mailserver/blob/master/config-examples/fail2ban-jail.cf -[github-file-f2bconfig]: https://github.com/docker-mailserver/docker-mailserver/blob/master/config-examples/fail2ban-fail2ban.cf +## Running Inside A Rootless Container { #rootless-container } + +[`RootlessKit`][rootless::rootless-kit] is the _fakeroot_ implementation for supporting _rootless mode_ in Docker and Podman. By default, RootlessKit uses the [`builtin` port forwarding driver][rootless::port-drivers], which does not propagate source IP addresses. + +It is necessary for F2B to have access to the real source IP addresses in order to correctly identify clients. This is achieved by changing the port forwarding driver to [`slirp4netns`][rootless::slirp4netns], which is slower than the builtin driver but does preserve the real source IPs. + [rootless::rootless-kit]: https://github.com/rootless-containers/rootlesskit [rootless::port-drivers]: https://github.com/rootless-containers/rootlesskit/blob/v0.14.5/docs/port.md#port-drivers [rootless::slirp4netns]: https://github.com/rootless-containers/slirp4netns -[rootless::docker]: https://docs.docker.com/engine/security/rootless -[rootless::podman]: https://github.com/containers/podman/blob/v3.4.1/docs/source/markdown/podman-run.1.md#--networkmode---net -[rootless::podman::interface]: https://github.com/containers/podman/blob/v3.4.1/libpod/networking_slirp4netns.go#L264 + +=== "Docker" + + For [rootless mode][rootless::docker] in Docker, create `~/.config/systemd/user/docker.service.d/override.conf` with the following content: + + !!! danger inline end + + This changes the port driver for all rootless containers managed by Docker. Per container configuration is not supported, if you need that consider Podman instead. + + ```cf + [Service] + Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=slirp4netns" + ``` + + And then restart the daemon: + + ```console + $ systemctl --user daemon-reload + $ systemctl --user restart docker + ``` + + [rootless::docker]: https://docs.docker.com/engine/security/rootless + +=== "Podman" + + [Rootless Podman][rootless::podman] requires adding the value `slirp4netns:port_handler=slirp4netns` to the `--network` CLI option, or `network_mode` setting in your `compose.yaml`: + + !!! example + + ```yaml + services: + mailserver: + network_mode: "slirp4netns:port_handler=slirp4netns" + environment: + - ENABLE_FAIL2BAN=1 + - NETWORK_INTERFACE=tap0 + ... + ``` + + You must also add the ENV `NETWORK_INTERFACE=tap0`, because Podman uses a [hard-coded interface name][rootless::podman::interface] for `slirp4netns`. `slirp4netns` is not compatible with user-defined networks! + + [rootless::podman]: https://github.com/containers/podman/blob/v3.4.1/docs/source/markdown/podman-run.1.md#--networkmode---net + [rootless::podman::interface]: https://github.com/containers/podman/blob/v3.4.1/libpod/networking_slirp4netns.go#L264 diff --git a/docs/content/config/security/mail_crypt.md b/docs/content/config/security/mail_crypt.md index 7b641ea7fa4..463e91f6df4 100644 --- a/docs/content/config/security/mail_crypt.md +++ b/docs/content/config/security/mail_crypt.md @@ -3,7 +3,7 @@ title: 'Security | mail_crypt (email/storage encryption)' --- !!! info - + The Mail crypt plugin is used to secure email messages stored in a Dovecot system. Messages are encrypted before written to storage and decrypted after reading. Both operations are transparent to the user. In case of unauthorized access to the storage backend, the messages will, without access to the decryption keys, be unreadable to the offending party. @@ -30,26 +30,26 @@ Official Dovecot documentation: https://doc.dovecot.org/configuration_manual/mai } ``` -2. Shutdown your mailserver (`docker-compose down`) +2. Shutdown your mailserver (`docker compose down`) 3. You then need to [generate your global EC key](https://doc.dovecot.org/configuration_manual/mail_crypt_plugin/#ec-key). We named them `/certs/ecprivkey.pem` and `/certs/ecpubkey.pem` in step #1. -4. The EC key needs to be available in the container. I prefer to mount a /certs directory into the container: +4. The EC key needs to be available in the container. I prefer to mount a /certs directory into the container: ```yaml services: mailserver: - image: docker.io/mailserver/docker-mailserver:latest + image: ghcr.io/docker-mailserver/docker-mailserver:latest volumes: . . . - ./certs/:/certs . . . ``` -5. While you're editing the `docker-compose.yml`, add the configuration file: +5. While you're editing the `compose.yaml`, add the configuration file: ```yaml services: mailserver: - image: docker.io/mailserver/docker-mailserver:latest + image: ghcr.io/docker-mailserver/docker-mailserver:latest volumes: . . . - ./config/dovecot/10-custom.conf:/etc/dovecot/conf.d/10-custom.conf diff --git a/docs/content/config/security/rspamd.md b/docs/content/config/security/rspamd.md index e9a95700340..b8431172c19 100644 --- a/docs/content/config/security/rspamd.md +++ b/docs/content/config/security/rspamd.md @@ -2,227 +2,431 @@ title: 'Security | Rspamd' --- -!!! warning "The current state of Rspamd integration into DMS" +## About - Recent pull requests have stabilized integration of Rspamd to a point that we encourage users to test the feature. We are confident that there are no major bugs in our integration that make using Rspamd infeasible. Please note that there may still be (breaking) changes ahead as integration is still work in progress! +Rspamd is a ["fast, free and open-source spam filtering system"][rspamd-web]. DMS integrates Rspamd like any other service. We provide a basic but easy to maintain setup of Rspamd. If you want to take a look at the default configuration files for Rspamd that DMS adds, navigate to [`target/rspamd/` inside the repository][dms-repo::default-rspamd-configuration]. - We expect to stabilize this feature with version `v12.1.0`. +### Enable Rspamd -## About +Rspamd is presently opt-in for DMS, but intended to become the default anti-spam service in a future release. + +DMS offers two anti-spam solutions: -Rspamd is a ["fast, free and open-source spam filtering system"][homepage]. DMS integrates Rspamd like any other service. We provide a very simple but easy to maintain setup of Rspamd. +- Legacy (_Amavis, SpamAssassin, OpenDKIM, OpenDMARC_) +- Rspamd (_Provides equivalent features of software from the legacy solution_) -If you want to have a look at the default configuration files for Rspamd that DMS packs, navigate to [`target/rspamd/` inside the repository][dms-default-configuration]. Please consult the [section "The Default Configuration"](#the-default-configuration) section down below for a written overview. +While you could configure Rspamd to only replace some of the legacy services, it is advised to only use Rspamd with the legacy services disabled. -!!! note "AMD64 vs ARM64" +!!! example "Switch to Rspamd" - We are currently doing a best-effort installation of Rspamd for ARM64 (from the Debian backports repository for Debian 11). The current version difference is two minor versions (AMD64 is at version 3.4, ARM64 at 3.2 \[13th Feb 2023\]). + To use Rspamd add the following ENV config changes: + + ```env + ENABLE_RSPAMD=1 + + # Rspamd replaces the functionality of all these anti-spam services, disable them: + ENABLE_OPENDKIM=0 + ENABLE_OPENDMARC=0 + ENABLE_POLICYD_SPF=0 + ENABLE_AMAVIS=0 + ENABLE_SPAMASSASSIN=0 + # Greylisting is opt-in, if you had enabled Postgrey switch to the Rspamd equivalent: + ENABLE_POSTGREY=0 + RSPAMD_GREYLISTING=1 + + # Optional: Add anti-virus support with ClamAV (compatible with Rspamd): + ENABLE_CLAMAV=1 + ``` + +!!! info "Relevant Environment Variables" - Maintainers noticed only few differences, some of them with a big impact though. For those running Rspamd on ARM64, we recommend [disabling](#with-the-help-of-a-custom-file) the [DKIM signing module][dkim-signing-module] if you don't use it. + The following environment variables are related to Rspamd: + + 1. [`ENABLE_RSPAMD`](../environment.md#enable_rspamd) + 2. [`ENABLE_RSPAMD_REDIS`](../environment.md#enable_rspamd_redis) + 3. [`RSPAMD_CHECK_AUTHENTICATED`](../environment.md#rspamd_check_authenticated) + 4. [`RSPAMD_GREYLISTING`](../environment.md#rspamd_greylisting) + 5. [`RSPAMD_HFILTER`](../environment.md#rspamd_hfilter) + 6. [`RSPAMD_HFILTER_HOSTNAME_UNKNOWN_SCORE`](../environment.md#rspamd_hfilter_hostname_unknown_score) + 7. [`RSPAMD_LEARN`](../environment.md#rspamd_learn) + 8. [`SPAM_SUBJECT`](../environment.md#spam_subject) + 9. [`MOVE_SPAM_TO_JUNK`][docs::spam-to-junk] + 10. [`MARK_SPAM_AS_READ`](../environment.md#mark_spam_as_read) -## The Default Configuration +## Overview of Rspamd support ### Mode of Operation -The proxy worker operates in [self-scan mode][proxy-self-scan-mode]. This simplifies the setup as we do not require a normal worker. You can easily change this though by [overriding the configuration by DMS](#providing-custom-settings-overriding-settings). +!!! note "Attention" + + Read this section carefully if you want to understand how Rspamd is integrated into DMS and how it works (on a surface level). + +Rspamd is integrated as a milter into DMS. When enabled, Postfix's `main.cf` configuration file includes the parameter `rspamd_milter = inet:localhost:11332`, which is added to `smtpd_milters`. As a milter, Rspamd can inspect incoming and outgoing e-mails. + +Each mail is assigned what Rspamd calls symbols: when an e-mail matches a specific criterion, the e-mail receives a symbol. Afterward, Rspamd applies a _spam score_ (as usual with anti-spam software) to the e-mail. + +- The score itself is calculated by adding the values of the individual symbols applied earlier. The higher the spam score is, the more likely the e-mail is spam. +- Symbol values can be negative (i.e., these symbols indicate the mail is legitimate, maybe because [SPF and DKIM][docs::dkim-dmarc-spf] are verified successfully). On the other hand, symbol scores can be positive (i.e., these symbols indicate the e-mail is spam, perhaps because the e-mail contains numerous links). + +Rspamd then adds (a few) headers to the e-mail based on the spam score. Most important is `X-Spamd-Result`, which contains an overview of which symbols were applied. It could look like this: + +```txt +X-Spamd-Result default: False [-2.80 / 11.00]; R_SPF_NA(1.50)[no SPF record]; R_DKIM_ALLOW(-1.00)[example.com:s=dtag1]; DWL_DNSWL_LOW(-1.00)[example.com:dkim]; RWL_AMI_LASTHOP(-1.00)[192.0.2.42:from]; DMARC_POLICY_ALLOW(-1.00)[example.com,none]; RWL_MAILSPIKE_EXCELLENT(-0.40)[192.0.2.42:from]; FORGED_SENDER(0.30)[noreply@example.com,some-reply-address@bounce.example.com]; RCVD_IN_DNSWL_LOW(-0.10)[192.0.2.42:from]; MIME_GOOD(-0.10)[multipart/mixed,multipart/related,multipart/alternative,text/plain]; MIME_TRACE(0.00)[0:+,1:+,2:+,3:+,4:~,5:~,6:~]; RCVD_COUNT_THREE(0.00)[3]; RCPT_COUNT_ONE(0.00)[1]; REPLYTO_DN_EQ_FROM_DN(0.00)[]; ARC_NA(0.00)[]; TO_MATCH_ENVRCPT_ALL(0.00)[]; RCVD_TLS_LAST(0.00)[]; DKIM_TRACE(0.00)[example.com:+]; HAS_ATTACHMENT(0.00)[]; TO_DN_NONE(0.00)[]; FROM_NEQ_ENVFROM(0.00)[noreply@example.com,some-reply-address@bounce.example.com]; FROM_HAS_DN(0.00)[]; REPLYTO_DOM_NEQ_FROM_DOM(0.00)[]; PREVIOUSLY_DELIVERED(0.00)[receiver@anotherexample.com]; ASN(0.00)[asn:3320, ipnet:192.0.2.0/24, country:DE]; MID_RHS_MATCH_FROM(0.00)[]; MISSING_XM_UA(0.00)[]; HAS_REPLYTO(0.00)[some-reply-address@dms-reply.example.com] +``` + +And then there is a corresponding `X-Rspamd-Action` header, which shows the overall result and the action that is taken. In our example, it would be: + +```txt +X-Rspamd-Action no action +``` + +Since the score is `-2.80`, nothing will happen and the e-mail is not classified as spam. Our custom [`actions.conf`][dms-repo::rspamd-actions-config] defines what to do at certain scores: + +1. At a score of 4, the e-mail is to be _greylisted_; +2. At a score of 6, the e-mail is _marked with a header_ (`X-Spam: Yes`); +3. At a score of 11, the e-mail is outright _rejected_. + +--- + +There is more to spam analysis than meets the eye: we have not covered the [Bayes training and filters][rspamd-docs::bayes] here, nor have we discussed [Sieve rules for e-mails that are marked as spam][docs::spam-to-junk]. + +Even the calculation of the score with the individual symbols has been presented to you in a simplified manner. But with the knowledge from above, you're equipped to read on and use Rspamd confidently. Keep on reading to understand the integration even better - you will want to know about your anti-spam software, not only to keep the bad e-mail out, but also to make sure the good e-mail arrive properly! + +### Workers + +The proxy worker operates in [self-scan mode][rspamd-docs::proxy-self-scan-mode]. This simplifies the setup as we do not require a normal worker. You can easily change this though by [overriding the configuration by DMS](#providing-custom-settings-overriding-settings). DMS does not set a default password for the controller worker. You may want to do that yourself. In setups where you already have an authentication provider in front of the Rspamd webpage, you may want to [set the `secure_ip ` option to `"0.0.0.0/0"` for the controller worker](#with-the-help-of-a-custom-file) to disable password authentication inside Rspamd completely. ### Persistence with Redis -When Rspamd is enabled, we implicitly also start an instance of Redis in the container. Redis is configured to persist it's data via RDB snapshots to disk in the directory `/var/lib/redis` (_which is a symbolic link to `/var/mail-state/lib-redis/` when [`ONE_DIR=1`](../environment.md#one_dir) and a volume is mounted to `/var/mail-state/`_). With the volume mount the snapshot will restore the Redis data across container restarts, and provide a way to keep backup. +When Rspamd is enabled, we implicitly also start an instance of Redis in the container: + +- Redis is configured to persist its data via RDB snapshots to disk in the directory `/var/lib/redis` (_or the [`/var/mail-state/`][docs::dms-volumes-state] volume when present_). +- With the volume mount, the snapshot will restore the Redis data across container updates, and provide a way to keep a backup. +- Without a volume mount a containers internal state will persist across restarts until the container is recreated due to changes like ENV or upgrading the image for the container. + +Redis uses `/etc/redis/redis.conf` for configuration: + +- We adjust this file when enabling the internal Redis service. +- If you have an external instance of Redis to use, the internal Redis service can be opt-out via setting the ENV [`ENABLE_RSPAMD_REDIS=0`][docs::env::enable-redis] (_link also details required changes to the DMS Rspamd config_). + +If you are interested in using Valkey instead of Redis, please refer to [this guidance][gh-dms::guide::valkey]. + +### Web Interface + +Rspamd provides a [web interface][rspamd-docs::web-ui], which contains statistics and data Rspamd collects. The interface is enabled by default and reachable on port 11334. + +![Rspamd Web Interface](https://rspamd.com/img/webui.png) + +To use the web interface you will need to configure a password, [otherwise you won't be able to log in][rspamd-docs::web-ui::password]. + +??? example "Set a custom password" + + Add this line to [your Rspamd `custom-commands.conf` config](#with-the-help-of-a-custom-file) which sets the `password` option of the _controller worker_: + + ``` + set-option-for-controller password "your hashed password here" + ``` + + The password hash can be generated via the `rspamadm pw` command: + + ```bash + docker exec -it rspamadm pw + ``` + + --- + + **Related:** A minimal Rspamd `compose.yaml` [example with a reverse-proxy for web access][gh-dms::guide::rspamd-web]. + +### DNS + +DMS does not supply custom values for DNS servers (to Rspamd). If you need to use custom DNS servers, which could be required when using [DNS-based deny/allowlists](#rspamd-module-rbl), you need to adjust [`options.inc`][rspamd-docs::config::global] yourself. Make sure to also read our [FAQ page on DNS servers][docs::faq::dns-servers]. + +!!! warning + + Rspamd heavily relies on a properly working DNS server that it can use to resolve DNS queries. If your DNS server is misconfigured, you will encounter issues when Rspamd queries DNS to assess if mail is spam. Legitimate mail is then unintentionally marked as spam or worse, rejected entirely. -Redis uses `/etc/redis/redis.conf` for configuration. We adjust this file when enabling the internal Redis service. If you have an external instance of Redis to use, the internal Redis service can be opt-out via setting the ENV [`ENABLE_RSPAMD_REDIS=0`](../environment.md#enable_rspamd_redis) (_link also details required changes to the DMS rspamd config_). + When Rspamd is deciding if mail is spam, it will check DNS records for SPF, DKIM and DMARC. Each of those has an associated symbol for DNS temporary errors with a non-zero weight assigned. That weight contributes towards the spam score assessed by Rspamd which is normally desirable - provided your network DNS is functioning correctly, otherwise when DNS is broken all mail is biased towards spam due to these failed DNS lookups. + +!!! danger + + While we do not provide values for custom DNS servers by default, we set `soft_reject_on_timeout = true;` by default. This setting will cause a soft reject if a task (presumably a DNS request) timeout takes place. + + This setting is enabled to not allow spam to proceed just because DNS requests did not succeed. It could deny legitimate e-mails to pass though too in case your DNS setup is incorrect or not functioning properly. + +??? example "Setup a recursive DNS resolver for DMS to use" + + This example is specifically focused on how to run a local DNS service capable of recursive resolution to [properly support DNSBL services](#rspamd-module-rbl) such as [SpamHaus][spamhaus::faq::what-is-a-dnsbl]. + + --- + + Configure your DMS container (`mailserver`) to forward DNS queries through to the added `dns-recursor` container via adding the `dns` service setting as shown below. + + This `dns` setting requires an explicit IP address. The [implicit `default` network][docker-docs::compose::default-network] is explicitly configured with a subnet, so that a specific IP address can then be assigned to the `dns-recursor` container. + + In this example PowerDNS Recursor was chosen for the `dns-recursor` service, however you can use any DNS server that's capable of functioning as a recursive resolver (_eg: Bind 9, Knot, Technitium, Unbound_). + + ```yaml title="compose.yaml" + services: + # Append these settings to your real `compose.yaml` + mailserver: + dns: + - "10.10.10.10" + depends_on: + - dns-recursor + + dns-recursor: + # NOTE: `-master:latest` is the equivalent of DMS `:edge`, + # PowerDNS stable releases have a naming convention like: `powerdns/pdns-recursor-53:5.3.5` + # To track the latest stable release, follow their changelog: + # https://doc.powerdns.com/recursor/changelog/index.html + # https://github.com/PowerDNS/pdns/blob/master/Docker-README.md + image: powerdns/pdns-recursor-master:latest + # Uncomment `command` to enable logging: + # https://doc.powerdns.com/recursor/settings.html#quiet + #command: '--quiet=no' + restart: always + stop_grace_period: 0s + networks: + default: + ipv4_address: 10.10.10.10 + + networks: + default: + # Advised if your container host can be reached via IPv6: + enable_ipv6: true + ipam: + driver: default + config: + - subnet: "10.10.10.0/24" + ``` + + !!! info "Docker Compose includes embedded DNS" + + Docker Compose with user-defined networks (default) first route DNS queries internally to resolve IPs to containers, or perform rDNS on container IPs. + + If there is no match by the embedded DNS service (_`127.0.0.11:53`, only reachable within the container_), the DNS query will be forwarded to the configured `dns` service. + + !!! warning "Ensure IPv6 support if your container host routes IPv6" + + `enable_ipv6: true` will [prevent a security risk][docs::ipv6::security-risks] for published ports that are reachable via IPv6 connections to the container host. This concern isn't specific to the `dns-recursor` service itself, but rather the standard DMS container when publishing ports to it's internal network and the default `0.0.0.0` binding (all interfaces). + + If your host does not have an IPv6 enabled interface or you have `"userland-proxy": false` configured in `/etc/docker/daemon.json`, this is additional setting is not required. + +### Logs + +You can find the Rspamd logs at `/var/log/mail/rspamd.log`, and the corresponding logs for [Redis](#persistence-with-redis), if it is enabled, at `/var/log/supervisor/rspamd-redis.log`. We recommend inspecting these logs (with `docker exec -it less /var/log/mail/rspamd.log`) in case Rspamd does not work as expected. ### Modules -You can find a list of all Rspamd modules [on their website][modules]. +You can find a list of all Rspamd modules [on their website][rspamd-docs::modules]. #### Disabled By Default -DMS disables certain modules (clickhouse, elastic, greylist, neural, reputation, spamassassin, url_redirector, metric_exporter) by default. We believe these are not required in a standard setup, and they would otherwise needlessly use system resources. +DMS disables certain modules (`clickhouse`, `elastic`, `neural`, `reputation`, `spamassassin`, `url_redirector`, `metric_exporter`) by default. We believe these are not required in a standard setup, and they would otherwise needlessly use system resources. #### Anti-Virus (ClamAV) You can choose to enable ClamAV, and Rspamd will then use it to check for viruses. Just set the environment variable `ENABLE_CLAMAV=1`. -#### RBLs (Realtime Blacklists) / DNSBLs (DNS-based Blacklists) +#### RBLs (Real-time Blacklists) / DNSBLs (DNS-based Blacklists) { #rspamd-module-rbl } + +The [RBL module][rspamd-docs::modules::rbl] is enabled by default. As a consequence, Rspamd will perform DNS lookups to various blacklists. Whether an RBL or a DNSBL is queried depends on where the domain name was obtained: RBL servers are queried with IP addresses extracted from message headers, DNSBL server are queried with domains and IP addresses extracted from the message body ([source][www::rbl-vs-dnsbl]). + +??? warning "Rspamd & DNS Blocklists" + + When the RBL module is enabled, Rspamd will do a variety of DNS requests to (amongst other things) DNSBLs. There are a [variety of issues involved when using DNSBLs][spamhaus::faq::what-is-a-dnsbl]. Rspamd will try to mitigate some of them by properly evaluating all return codes. This evaluation is a best effort though, so if the DNSBL operators change or add return codes, it may take a while for Rspamd to adjust as well. -The [RBL module](https://rspamd.com/doc/modules/rbl.html) is enabled by default. As a consequence, Rspamd will perform DNS lookups to a variety of blacklists. Whether an RBL or a DNSBL is queried depends on where the domain name was obtained: RBL servers are queried with IP addresses extracted from message headers, DNSBL server are queried with domains and IP addresses extracted from the message body \[[source][rbl-vs-dnsbl]\]. +!!! danger "Properly querying DNS Blocklists" -!!! danger "Rspamd and DNS Block Lists" + To use DNS Blocklists (DNSBLs) properly, DMS must use a **private and recursive** DNS resolver. - When the RBL module is enabled, Rspamd will do a variety of DNS requests to (amongst other things) DNSBLs. There are a variety of issues involved when using DNSBLs. Rspamd will try to mitigate some of them by properly evaluating all return codes. This evaluation is a best effort though, so if the DNSBL operators change or add return codes, it may take a while for Rspamd to adjust as well. + DNSBL services are rate-limited, thus if your DNS queries are forwarded through a public resolver (_like Cloudflare's `1.1.1.1` or Google's `8.8.8.8`_) caching the DNSBL service responses received from a public DNS resolver will not be reliable when public load has triggered a rate limit. - If you want to use DNSBLs, **try to use your own DNS resolver** and make sure it is set up correctly, i.e. it should be a non-public & **recursive** resolver. Otherwise, you might not be able ([see this Spamhaus post](https://www.spamhaus.org/faq/section/DNSBL%20Usage#365)) to make use of the block lists. + Instead of relying on forwarding DNS queries, they must be resolved recursively (directly) via running your own private recursive DNS service (_See the [DNS section](#dns) for an example of how to do this_). ## Providing Custom Settings & Overriding Settings -### Manually +!!! info "Rspamd config overriding precedence" -DMS brings sane default settings for Rspamd. They are located at `/etc/rspamd/local.d/` inside the container (or `target/rspamd/local.d/` in the repository). If you want to change these settings and / or provide your own settings, you can + Rspamd has a layered approach for configuration with [`local.d` and `override.d` config directories][rspamd-docs::config-directories]. -1. place files at `/etc/rspamd/override.d/` which will override Rspamd settings and DMS settings -2. (re-)place files at `/etc/rspamd/local.d/` to override DMS settings and merge them with Rspamd settings + - DMS [extends the Rspamd default configs via `/etc/rspamd/local.d/`][dms-repo::default-rspamd-configuration]. + - User config changes should be handled separately as overrides via the [DMS Config Volume][docs::dms-volumes-config] (`docker-data/dms/config/`) with either: + - `./rspamd/override.d/` - Config files placed here are copied to `/etc/rspamd/override.d/` during container startup. + - [`./rspamd/custom-commands.conf`](#with-the-help-of-a-custom-file) - Applied after copying any provided configs from `rspamd/override.d/` (DMS Config volume) to `/etc/rspamd/override.d/`. -!!! warning "Clashing Overrides" +!!! abstract "Reference docs for Rspamd config" - Note that when also [using the `rspamd-commands` file](#with-the-help-of-a-custom-file), files in `override.d` may be overwritten in case you adjust them manually and with the help of the file. + - [Config Overview][rspamd-docs::config::overview], [Quickstart guide][rspamd-docs::config::quickstart], and [Config Syntax (UCL)][rspamd-docs::config::ucl-syntax] + - Global Options ([`options.inc`][rspamd-docs::config::global]) + - [Workers][rspamd-docs::config::workers] ([`worker-controller.inc`][rspamd-docs::config::worker-controller], [`worker-proxy.inc`][rspamd-docs::config::worker-proxy]) + - [Modules][rspamd-docs::modules] (_view each module page for their specific config options_) -### With the Help of a Custom File +!!! tip "View rendered config" -DMS provides the ability to do simple adjustments to Rspamd modules with the help of a single file. Just place a file called `rspamd-modules.conf` into the directory `docker-data/dms/config/` (which translates to `/tmp/docker-mailserver/` in the container). If this file is present, DMS will evaluate it. The structure is _very_ simple. Each line in the file looks like this: + `rspamadm configdump` will output the full rspamd configuration that is used should you need it for troubleshooting / inspection. -```txt -COMMAND ARGUMENT1 ARGUMENT2 ARGUMENT3 -``` + - You can also see which modules are enabled / disabled via `rspamadm configdump --modules-state` + - Specific config sections like `dkim` or `worker` can also be used to filter the output to just those sections: `rspamadm configdump dkim worker` + - Use `--show-help` to include inline documentation for many settings. -where `COMMAND` can be: +### Using `custom-commands.conf` { #with-the-help-of-a-custom-file } -1. `disable-module`: disables the module with name `ARGUMENT1` -2. `enable-module`: explicitly enables the module with name `ARGUMENT1` -3. `set-option-for-module`: sets the value for option `ARGUMENT2` to `ARGUMENT3` inside module `ARGUMENT1` -4. `set-option-for-controller`: set the value of option `ARGUMENT1` to `ARGUMENT2` for the controller worker -5. `set-option-for-proxy`: set the value of option `ARGUMENT1` to `ARGUMENT2` for the proxy worker -6. `set-common-option`: set the option `ARGUMENT1` that [defines basic Rspamd behaviour][basic-options] to value `ARGUMENT2` -7. `add-line`: this will add the complete line after `ARGUMENT1` (with all characters) to the file `/etc/rspamd/override.d/` +For convenience DMS provides a single config file that will directly create or modify multiple configs at `/etc/rspamd/override.d/`. This is handled as the final rspamd configuration step during container startup. -!!! example "An Example Is [Shown Down Below](#adjusting-and-extending-the-very-basic-configuration)" +DMS will apply this config when you provide `rspamd/custom-commands.conf` in your DMS Config volume. Configure it with directive lines as documented below. -!!! note "File Names & Extensions" +!!! note "Only use this feature for `option = value` changes" - For command 1 - 3, we append the `.conf` suffix to the module name to get the correct file name automatically. For commands 4 - 6, the file name is fixed (you don't even need to provide it). For command 7, you will need to provide the whole file name (including the suffix) yourself! + `custom-commands.conf` is only suitable for adding or replacing simple `option = value` settings for configs at `/etc/rspamd/override.d/`. + + - New settings are appended to the associated config file. + - When replacing an existing setting in an override config, that setting may be any matching line (_allowing for nested scopes, instead of only top-level keys_). + + Any changes involving more advanced [UCL config syntax][rspamd-docs::config::ucl-syntax] should instead add UCL config files directly to `rspamd/override.d/` (_in the DMS Config volume_). -You can also have comments (the line starts with `#`) and blank lines in `rspamd-modules.conf` - they are properly handled and not evaluated. +!!! info "`custom-commands.conf` syntax" -!!! tip "Adjusting Modules This Way" + There are 7 directives available to manage custom Rspamd configurations. Add these directive lines into `custom-commands.conf`, they will be processed sequentially. - These simple commands are meant to give users the ability to _easily_ alter modules and their options. As a consequence, they are not powerful enough to enable multi-line adjustments. If you need to do something more complex, we advise to do that [manually](#manually)! + **Directives:** -## Examples & Advanced Configuration + ```txt + # For /etc/rspamd/override.d/{options.inc,worker-controller.inc,worker-proxy}.inc + set-common-option
\\{ type \\; flags interval\\; \\}/' /etc/fail2ban/action.d/nftables.conf } -function _post_installation_steps -{ +function _post_installation_steps() { _log 'debug' 'Running post-installation steps (cleanup)' + _log 'debug' 'Deleting sensitive files (secrets)' + rm /etc/postsrsd.secret + + _log 'debug' 'Deleting default logwatch cronjob' + rm /etc/cron.daily/00logwatch + + _log 'trace' 'Removing leftovers from APT' apt-get "${QUIET}" clean rm -rf /var/lib/apt/lists/* - _log 'info' 'Finished installing packages' + # Irrelevant - Debian's default `chroot` jail config for Postfix needed a separate syslog socket: + rm /etc/rsyslog.d/postfix.conf } _pre_installation_steps -_install_postfix +_install_utils _install_packages _install_dovecot _install_rspamd diff --git a/target/scripts/check-for-changes.sh b/target/scripts/check-for-changes.sh index f23a2cb0b2a..281714fcf2c 100755 --- a/target/scripts/check-for-changes.sh +++ b/target/scripts/check-for-changes.sh @@ -1,13 +1,10 @@ #!/bin/bash -# TODO: Adapt for compatibility with LDAP -# Only the cert renewal change detection may be relevant for LDAP? - # CHKSUM_FILE global is imported from this file: # shellcheck source=./helpers/index.sh source /usr/local/bin/helpers/index.sh -_log_with_date 'debug' 'Starting changedetector' +_log 'debug' 'Starting changedetector' # ATTENTION: Do not remove! # This script requires some environment variables to be properly set. @@ -21,18 +18,20 @@ source /etc/dms-settings # usage with DMS_HOSTNAME, which should remove the need to call this: _obtain_hostname_and_domainname +# This is a helper to properly set all Rspamd-related environment variables +# correctly and in one place. +_rspamd_get_envs + # verify checksum file exists; must be prepared by start-mailserver.sh -if [[ ! -f ${CHKSUM_FILE} ]] -then +if [[ ! -f ${CHKSUM_FILE} ]]; then _exit_with_error "'${CHKSUM_FILE}' is missing" 0 fi -_log_with_date 'trace' "Using postmaster address '${POSTMASTER_ADDRESS}'" +_log 'trace' "Using postmaster address '${POSTMASTER_ADDRESS}'" -_log_with_date 'debug' "Changedetector is ready" +_log 'debug' "Changedetector is ready" -function _check_for_changes -{ +function _check_for_changes() { # get chksum and check it, no need to lock config yet _monitored_files_checksums >"${CHKSUM_FILE}.new" cmp --silent -- "${CHKSUM_FILE}" "${CHKSUM_FILE}.new" @@ -41,34 +40,51 @@ function _check_for_changes # 0 – files are identical # 1 – files differ # 2 – inaccessible or missing argument - if [[ ${?} -eq 1 ]] - then - _log_with_date 'info' 'Change detected' + if [[ ${?} -eq 1 ]]; then + _log 'info' 'Change detected' _create_lock # Shared config safety lock local CHANGED CHANGED=$(_get_changed_files "${CHKSUM_FILE}" "${CHKSUM_FILE}.new") - - # Handle any changes - _ssl_changes - _postfix_dovecot_changes - - _log_with_date 'debug' 'Reloading services due to detected changes' - - [[ ${ENABLE_AMAVIS} -eq 1 ]] && _reload_amavis - _reload_postfix - [[ ${SMTP_ONLY} -ne 1 ]] && dovecot reload + _handle_changes _remove_lock - _log_with_date 'debug' 'Completed handling of detected change' + _log 'debug' 'Completed handling of detected change' # mark changes as applied mv "${CHKSUM_FILE}.new" "${CHKSUM_FILE}" fi } -function _get_changed_files -{ +function _handle_changes() { + # Variable to identify any config updates dependent upon vhost changes. + local VHOST_UPDATED=0 + # These two configs are the source for /etc/postfix/vhost (managed mail domains) + if [[ ${ACCOUNT_PROVISIONER} == 'FILE' ]] \ + && [[ ${CHANGED} =~ ${DMS_DIR}/postfix-(accounts|virtual).cf ]]; then + _log 'trace' 'Regenerating vhosts (Postfix)' + # Regenerate via `helpers/postfix.sh`: + _create_postfix_vhost + + VHOST_UPDATED=1 + fi + + _ssl_changes + # TODO: Consider support relay host config change support for other provisioners + if [[ ${ACCOUNT_PROVISIONER} == 'FILE' ]]; then + _postfix_dovecot_changes + fi + + _rspamd_changes + + _log 'debug' 'Reloading services due to detected changes' + + [[ ${ENABLE_AMAVIS} -eq 1 ]] && _reload_amavis + _reload_postfix + [[ ${SMTP_ONLY} -ne 1 ]] && dovecot reload +} + +function _get_changed_files() { local CHKSUM_CURRENT=${1} local CHKSUM_NEW=${2} @@ -83,20 +99,17 @@ function _get_changed_files grep -Fxvf "${CHKSUM_CURRENT}" "${CHKSUM_NEW}" | sed -r 's/^\S+[[:space:]]+//' } -function _reload_amavis -{ - if [[ ${CHANGED} =~ ${DMS_DIR}/postfix-accounts.cf ]] || [[ ${CHANGED} =~ ${DMS_DIR}/postfix-virtual.cf ]] - then - # /etc/postfix/vhost was updated, amavis must refresh it's config by - # reading this file again in case of new domains, otherwise they will be ignored. - amavisd-new reload +function _reload_amavis() { + # /etc/postfix/vhost was updated, amavis must refresh it's config by + # reading this file again in case of new domains, otherwise they will be ignored. + if [[ ${VHOST_UPDATED} -eq 1 ]]; then + amavisd reload fi } # Also note that changes are performed in place and are not atomic # We should fix that and write to temporary files, stop, swap and start -function _postfix_dovecot_changes -{ +function _postfix_dovecot_changes() { local DMS_DIR=/tmp/docker-mailserver # Regenerate accounts via `helpers/accounts.sh`: @@ -108,7 +121,7 @@ function _postfix_dovecot_changes || [[ ${CHANGED} =~ ${DMS_DIR}/dovecot-quotas.cf ]] \ || [[ ${CHANGED} =~ ${DMS_DIR}/dovecot-masters.cf ]] then - _log_with_date 'trace' 'Regenerating accounts (Dovecot + Postfix)' + _log 'trace' 'Regenerating accounts (Dovecot + Postfix)' [[ ${SMTP_ONLY} -ne 1 ]] && _create_accounts fi @@ -116,14 +129,12 @@ function _postfix_dovecot_changes # - postfix-sasl-password.cf used by _relayhost_sasl # - _populate_relayhost_map relies on: # - postfix-relaymap.cf - # - postfix-accounts.cf + postfix-virtual.cf (both will be dropped in future) - if [[ ${CHANGED} =~ ${DMS_DIR}/postfix-accounts.cf ]] \ - || [[ ${CHANGED} =~ ${DMS_DIR}/postfix-virtual.cf ]] \ + if [[ ${VHOST_UPDATED} -eq 1 ]] \ || [[ ${CHANGED} =~ ${DMS_DIR}/postfix-relaymap.cf ]] \ || [[ ${CHANGED} =~ ${DMS_DIR}/postfix-sasl-password.cf ]] then - _log_with_date 'trace' 'Regenerating relay config (Postfix)' - _rebuild_relayhost + _log 'trace' 'Regenerating relay config (Postfix)' + _process_relayhost_configs fi # Regenerate system + virtual account aliases via `helpers/aliases.sh`: @@ -131,44 +142,33 @@ function _postfix_dovecot_changes [[ ${CHANGED} =~ ${DMS_DIR}/postfix-regexp.cf ]] && _handle_postfix_regexp_config [[ ${CHANGED} =~ ${DMS_DIR}/postfix-aliases.cf ]] && _handle_postfix_aliases_config - # Regenerate `/etc/postfix/vhost` (managed mail domains) via `helpers/postfix.sh`: - if [[ ${CHANGED} =~ ${DMS_DIR}/postfix-accounts.cf ]] \ - || [[ ${CHANGED} =~ ${DMS_DIR}/postfix-virtual.cf ]] - then - _log_with_date 'trace' 'Regenerating vhosts (Postfix)' - _create_postfix_vhost - fi - # Legacy workaround handled here, only seems necessary for _create_accounts: # - `helpers/accounts.sh` logic creates folders/files with wrong ownership. _chown_var_mail_if_necessary } -function _ssl_changes -{ +function _ssl_changes() { local REGEX_NEVER_MATCH='(?\!)' # _setup_ssl is required for: # manual - copy to internal DMS_TLS_PATH (/etc/dms/tls) that Postfix and Dovecot are configured to use. # acme.json - presently uses /etc/letsencrypt/live/ instead of DMS_TLS_PATH, # path may change requiring Postfix/Dovecot config update. - if [[ ${SSL_TYPE} == 'manual' ]] - then + if [[ ${SSL_TYPE} == 'manual' ]]; then # only run the SSL setup again if certificates have really changed. if [[ ${CHANGED} =~ ${SSL_CERT_PATH:-${REGEX_NEVER_MATCH}} ]] \ || [[ ${CHANGED} =~ ${SSL_KEY_PATH:-${REGEX_NEVER_MATCH}} ]] \ || [[ ${CHANGED} =~ ${SSL_ALT_CERT_PATH:-${REGEX_NEVER_MATCH}} ]] \ || [[ ${CHANGED} =~ ${SSL_ALT_KEY_PATH:-${REGEX_NEVER_MATCH}} ]] then - _log_with_date 'debug' 'Manual certificates have changed - extracting certificates' + _log 'debug' 'Manual certificates have changed - extracting certificates' _setup_ssl fi # `acme.json` is only relevant to Traefik, and is where it stores the certificates it manages. # When a change is detected it's assumed to be a possible cert renewal that needs to be # extracted for `docker-mailserver` services to adjust to. - elif [[ ${CHANGED} =~ /etc/letsencrypt/acme.json ]] - then - _log_with_date 'debug' "'/etc/letsencrypt/acme.json' has changed - extracting certificates" + elif [[ ${CHANGED} =~ /etc/letsencrypt/acme.json ]]; then + _log 'debug' "'/etc/letsencrypt/acme.json' has changed - extracting certificates" _setup_ssl # Prevent an unnecessary change detection from the newly extracted cert files by updating their hashes in advance: @@ -184,10 +184,35 @@ function _ssl_changes # They presently have no special handling other than to trigger a change that will restart Postfix/Dovecot. } -while true -do +function _rspamd_changes() { + # RSPAMD_DMS_D='/tmp/docker-mailserver/rspamd' + if [[ ${CHANGED} =~ ${RSPAMD_DMS_D}/.* ]]; then + + # "${RSPAMD_DMS_D}/override.d" + if [[ ${CHANGED} =~ ${RSPAMD_DMS_OVERRIDE_D}/.* ]]; then + _log 'trace' 'Rspamd - Copying configuration overrides' + rm "${RSPAMD_OVERRIDE_D}"/* + cp "${RSPAMD_DMS_OVERRIDE_D}"/* "${RSPAMD_OVERRIDE_D}" + fi + + # "${RSPAMD_DMS_D}/custom-commands.conf" + if [[ ${CHANGED} =~ ${RSPAMD_DMS_CUSTOM_COMMANDS_F} ]]; then + _log 'trace' 'Rspamd - Generating new configuration from custom commands' + _rspamd_handle_user_modules_adjustments + fi + + # "${RSPAMD_DMS_D}/dkim" + if [[ ${CHANGED} =~ ${RSPAMD_DMS_DKIM_D} ]]; then + _log 'trace' 'Rspamd - DKIM files updated' + fi + + _reload_rspamd + fi +} + +while true; do _check_for_changes - sleep 2 + sleep "${DMS_CONFIG_POLL:-2}" done exit 0 diff --git a/target/scripts/helpers/accounts.sh b/target/scripts/helpers/accounts.sh index fcc6cf9699d..aa344f3731b 100644 --- a/target/scripts/helpers/accounts.sh +++ b/target/scripts/helpers/accounts.sh @@ -9,8 +9,7 @@ DOVECOT_USERDB_FILE=/etc/dovecot/userdb DOVECOT_MASTERDB_FILE=/etc/dovecot/masterdb -function _create_accounts -{ +function _create_accounts() { : >/etc/postfix/vmailbox : >"${DOVECOT_USERDB_FILE}" @@ -19,18 +18,10 @@ function _create_accounts local DATABASE_ACCOUNTS='/tmp/docker-mailserver/postfix-accounts.cf' _create_masters - if [[ -f ${DATABASE_ACCOUNTS} ]] - then - _log 'trace' "Checking file line endings" - sed -i 's|\r||g' "${DATABASE_ACCOUNTS}" - + if [[ -f ${DATABASE_ACCOUNTS} ]]; then _log 'trace' "Regenerating postfix user list" echo "# WARNING: this file is auto-generated. Modify ${DATABASE_ACCOUNTS} to edit the user list." > /etc/postfix/vmailbox - # checking that ${DATABASE_ACCOUNTS} ends with a newline - # shellcheck disable=SC1003 - sed -i -e '$a\' "${DATABASE_ACCOUNTS}" - chown dovecot:dovecot "${DOVECOT_USERDB_FILE}" chmod 640 "${DOVECOT_USERDB_FILE}" @@ -40,26 +31,13 @@ function _create_accounts # creating users ; 'pass' is encrypted # comments and empty lines are ignored local LOGIN PASS USER_ATTRIBUTES - while IFS=$'|' read -r LOGIN PASS USER_ATTRIBUTES - do + while IFS=$'|' read -r LOGIN PASS USER_ATTRIBUTES; do # Setting variables for better readability USER=$(echo "${LOGIN}" | cut -d @ -f1) DOMAIN=$(echo "${LOGIN}" | cut -d @ -f2) - # test if user has a defined quota - if [[ -f /tmp/docker-mailserver/dovecot-quotas.cf ]] - then - declare -a USER_QUOTA - IFS=':' read -r -a USER_QUOTA < <(grep "${USER}@${DOMAIN}:" -i /tmp/docker-mailserver/dovecot-quotas.cf) - - if [[ ${#USER_QUOTA[@]} -eq 2 ]] - then - USER_ATTRIBUTES="${USER_ATTRIBUTES:+${USER_ATTRIBUTES} }userdb_quota_rule=*:bytes=${USER_QUOTA[1]}" - fi - fi - - if [[ -z ${USER_ATTRIBUTES} ]] - then + USER_ATTRIBUTES="$(_add_attribute_dovecot_quota "${LOGIN}" "${USER_ATTRIBUTES}")" + if [[ -z ${USER_ATTRIBUTES} ]]; then _log 'debug' "Creating user '${USER}' for domain '${DOMAIN}'" else _log 'debug' "Creating user '${USER}' for domain '${DOMAIN}' with attributes '${USER_ATTRIBUTES}'" @@ -68,8 +46,7 @@ function _create_accounts local POSTFIX_VMAILBOX_LINE DOVECOT_USERDB_LINE POSTFIX_VMAILBOX_LINE="${LOGIN} ${DOMAIN}/${USER}/" - if grep -qF "${POSTFIX_VMAILBOX_LINE}" /etc/postfix/vmailbox - then + if grep -qF "${POSTFIX_VMAILBOX_LINE}" /etc/postfix/vmailbox; then _log 'warn' "User '${USER}@${DOMAIN}' will not be added to '/etc/postfix/vmailbox' twice" else echo "${POSTFIX_VMAILBOX_LINE}" >>/etc/postfix/vmailbox @@ -77,20 +54,18 @@ function _create_accounts # Dovecot's userdb has the following format # user:password:uid:gid:(gecos):home:(shell):extra_fields - DOVECOT_USERDB_LINE="${LOGIN}:${PASS}:5000:5000::/var/mail/${DOMAIN}/${USER}::${USER_ATTRIBUTES}" - if grep -qF "${DOVECOT_USERDB_LINE}" "${DOVECOT_USERDB_FILE}" - then + DOVECOT_USERDB_LINE="${LOGIN}:${PASS}:${DMS_VMAIL_UID}:${DMS_VMAIL_GID}::/var/mail/${DOMAIN}/${USER}/home::${USER_ATTRIBUTES}" + if grep -qF "${DOVECOT_USERDB_LINE}" "${DOVECOT_USERDB_FILE}"; then _log 'warn' "Login '${LOGIN}' will not be added to '${DOVECOT_USERDB_FILE}' twice" else echo "${DOVECOT_USERDB_LINE}" >>"${DOVECOT_USERDB_FILE}" fi - mkdir -p "/var/mail/${DOMAIN}/${USER}" + mkdir -p "/var/mail/${DOMAIN}/${USER}/home" # copy user provided sieve file, if present - if [[ -e "/tmp/docker-mailserver/${LOGIN}.dovecot.sieve" ]] - then - cp "/tmp/docker-mailserver/${LOGIN}.dovecot.sieve" "/var/mail/${DOMAIN}/${USER}/.dovecot.sieve" + if [[ -e "/tmp/docker-mailserver/${LOGIN}.dovecot.sieve" ]]; then + cp "/tmp/docker-mailserver/${LOGIN}.dovecot.sieve" "/var/mail/${DOMAIN}/${USER}/home/.dovecot.sieve" fi done < <(_get_valid_lines_from_file "${DATABASE_ACCOUNTS}") @@ -98,68 +73,68 @@ function _create_accounts fi } -# Required when using Dovecot Quotas to avoid blacklisting risk from backscatter -# Note: This is a workaround only suitable for basic aliases that map to single real addresses, -# not multiple addresses (real accounts or additional aliases), those will not work with Postfix -# `quota-status` policy service and remain at risk of backscatter. +# Required when using Dovecot Quotas to avoid blacklisting risk from backscatter. +# Note: This is a workaround only suitable for basic aliases that map to a single real address, +# not multiple addresses (real accounts or additional aliases), those will not work with +# the Postfix `quota-status` policy service and remain at risk of backscatter. # -# see https://github.com/docker-mailserver/docker-mailserver/pull/2248#issuecomment-953313852 -# for more details on this method -function _create_dovecot_alias_dummy_accounts -{ +# for more details on this method, see: +# https://github.com/docker-mailserver/docker-mailserver/pull/2248#issuecomment-953313852 +function _create_dovecot_alias_dummy_accounts() { local DATABASE_VIRTUAL='/tmp/docker-mailserver/postfix-virtual.cf' + # NOTE: `DATABASE_ACCOUNTS` should be in scope (provided from the expected caller: `_create_accounts()`) + + # Add aliases associated to DMS mailbox accounts as dummy entries into Dovecot's userdb, + # These will share the same storage as a real account for the `quota-status` check to query. + if [[ -f ${DATABASE_VIRTUAL} ]] && [[ ${ENABLE_QUOTAS} -eq 1 ]]; then + local ALIAS ALIASED DOVECOT_USERDB_LINE + while read -r ALIAS ALIASED; do + # Skip any alias lacking `@` (aka local-part only) or is a catch-all domain (starts with `@`): + [[ ! ${ALIAS} =~ .+@.+ ]] && continue + + # ${REAL_FQUN} is a user's fully-qualified username + unset REAL_FQUN + local REAL_FQUN REAL_USERNAME REAL_DOMAINNAME + + # Support checking multiple aliased addresses (split by `,` delimiter): + # - The first local account matched will be associated to the alias + # - Does not support resolving FQUN if it were also an alias + for FQUN in ${ALIASED//,/ } + do + if grep -q "${FQUN}" "${DATABASE_ACCOUNTS}"; then + REAL_FQUN="${FQUN}" + REAL_USERNAME=$(cut -d '@' -f 1 <<< "${REAL_FQUN}") + REAL_DOMAINNAME=$(cut -d '@' -f 2 <<< "${REAL_FQUN}") + break + fi + done - if [[ -f ${DATABASE_VIRTUAL} ]] && [[ ${ENABLE_QUOTAS} -eq 1 ]] - then - # adding aliases to Dovecot's userdb - # ${REAL_FQUN} is a user's fully-qualified username - local ALIAS REAL_FQUN DOVECOT_USERDB_LINE - while read -r ALIAS REAL_FQUN - do - # alias is assumed to not be a proper e-mail - # these aliases do not need to be added to Dovecot's userdb - [[ ! ${ALIAS} == *@* ]] && continue - - # clear possibly already filled arrays - # do not remove the following line of code - unset REAL_ACC USER_QUOTA - declare -a REAL_ACC USER_QUOTA - - local REAL_USERNAME REAL_DOMAINNAME - REAL_USERNAME=$(cut -d '@' -f 1 <<< "${REAL_FQUN}") - REAL_DOMAINNAME=$(cut -d '@' -f 2 <<< "${REAL_FQUN}") - - if ! grep -q "${REAL_FQUN}" "${DATABASE_ACCOUNTS}" - then + if [[ -z ${REAL_FQUN} ]] || ! grep -q "${REAL_FQUN}" "${DATABASE_ACCOUNTS}"; then _log 'debug' "Alias '${ALIAS}' is non-local (or mapped to a non-existing account) and will not be added to Dovecot's userdb" continue fi _log 'debug' "Adding alias '${ALIAS}' for user '${REAL_FQUN}' to Dovecot's userdb" + # Clear possibly already filled arrays (do not remove the following line of code) + unset REAL_ACC USER_QUOTA + declare -a REAL_ACC USER_QUOTA + # ${REAL_ACC[0]} => real account name (e-mail address) == ${REAL_FQUN} # ${REAL_ACC[1]} => password hash # ${REAL_ACC[2]} => optional user attributes IFS='|' read -r -a REAL_ACC < <(grep "${REAL_FQUN}" "${DATABASE_ACCOUNTS}") - if [[ -z ${REAL_ACC[1]} ]] - then - _dms_panic__misconfigured 'postfix-accounts.cf' 'alias configuration' 'immediate' + if [[ -z ${REAL_ACC[1]} ]]; then + _dms_panic__misconfigured 'postfix-accounts.cf' 'alias configuration' fi - # test if user has a defined quota - if [[ -f /tmp/docker-mailserver/dovecot-quotas.cf ]] - then - IFS=':' read -r -a USER_QUOTA < <(grep "${REAL_FQUN}:" -i /tmp/docker-mailserver/dovecot-quotas.cf) - if [[ ${#USER_QUOTA[@]} -eq 2 ]] - then - REAL_ACC[2]="${REAL_ACC[2]:+${REAL_ACC[2]} }userdb_quota_rule=*:bytes=${USER_QUOTA[1]}" - fi - fi + # Update user attributes with custom quota if found for the `REAL_FQUN`: + REAL_ACC[2]="$(_add_attribute_dovecot_quota "${REAL_FQUN}" "${REAL_ACC[2]}")" - DOVECOT_USERDB_LINE="${ALIAS}:${REAL_ACC[1]}:5000:5000::/var/mail/${REAL_DOMAINNAME}/${REAL_USERNAME}::${REAL_ACC[2]:-}" - if grep -qi "^${ALIAS}:" "${DOVECOT_USERDB_FILE}" - then + DOVECOT_USERDB_LINE="${ALIAS}:${REAL_ACC[1]}:${DMS_VMAIL_UID}:${DMS_VMAIL_GID}::/var/mail/${REAL_DOMAINNAME}/${REAL_USERNAME}/home::${REAL_ACC[2]:-}" + # Match a full line with `-xF` to avoid regex patterns introducing false positives matching `ALIAS`: + if grep -qixF "${DOVECOT_USERDB_LINE}" "${DOVECOT_USERDB_FILE}"; then _log 'warn' "Alias '${ALIAS}' will not be added to '${DOVECOT_USERDB_FILE}' twice" else echo "${DOVECOT_USERDB_LINE}" >>"${DOVECOT_USERDB_FILE}" @@ -170,22 +145,13 @@ function _create_dovecot_alias_dummy_accounts # Support Dovecot master user: https://doc.dovecot.org/configuration_manual/authentication/master_users/ # Supporting LDAP users requires `auth_bind = yes` in `dovecot-ldap.conf.ext`, see docker-mailserver/docker-mailserver/pull/2535 for details -function _create_masters -{ +function _create_masters() { : >"${DOVECOT_MASTERDB_FILE}" local DATABASE_DOVECOT_MASTERS='/tmp/docker-mailserver/dovecot-masters.cf' - if [[ -f ${DATABASE_DOVECOT_MASTERS} ]] - then - _log 'trace' "Checking file line endings" - sed -i 's|\r||g' "${DATABASE_DOVECOT_MASTERS}" - + if [[ -f ${DATABASE_DOVECOT_MASTERS} ]]; then _log 'trace' "Regenerating dovecot masters list" - # checking that ${DATABASE_DOVECOT_MASTERS} ends with a newline - # shellcheck disable=SC1003 - sed -i -e '$a\' "${DATABASE_DOVECOT_MASTERS}" - chown dovecot:dovecot "${DOVECOT_MASTERDB_FILE}" chmod 640 "${DOVECOT_MASTERDB_FILE}" @@ -194,8 +160,7 @@ function _create_masters # creating users ; 'pass' is encrypted # comments and empty lines are ignored local LOGIN PASS - while IFS=$'|' read -r LOGIN PASS - do + while IFS=$'|' read -r LOGIN PASS; do _log 'debug' "Creating master user '${LOGIN}'" local DOVECOT_MASTERDB_LINE @@ -203,8 +168,7 @@ function _create_masters # Dovecot's masterdb has the following format # user:password DOVECOT_MASTERDB_LINE="${LOGIN}:${PASS}" - if grep -qF "${DOVECOT_MASTERDB_LINE}" "${DOVECOT_MASTERDB_FILE}" - then + if grep -qF "${DOVECOT_MASTERDB_LINE}" "${DOVECOT_MASTERDB_FILE}"; then _log 'warn' "Login '${LOGIN}' will not be added to '${DOVECOT_MASTERDB_FILE}' twice" else echo "${DOVECOT_MASTERDB_LINE}" >>"${DOVECOT_MASTERDB_FILE}" @@ -212,3 +176,19 @@ function _create_masters done < <(_get_valid_lines_from_file "${DATABASE_DOVECOT_MASTERS}") fi } + +function _add_attribute_dovecot_quota() { + local MAIL_ACCOUNT="${1}" + local USER_ATTRIBUTES="${2}" + + if [[ -f /tmp/docker-mailserver/dovecot-quotas.cf ]]; then + declare -a USER_QUOTA + IFS=':' read -r -a USER_QUOTA < <(grep -i "${MAIL_ACCOUNT}:" /tmp/docker-mailserver/dovecot-quotas.cf) + + if [[ ${#USER_QUOTA[@]} -eq 2 ]]; then + USER_ATTRIBUTES="${USER_ATTRIBUTES:+${USER_ATTRIBUTES} }userdb_quota_rule=*:bytes=${USER_QUOTA[1]}" + fi + fi + + echo "${USER_ATTRIBUTES}" +} diff --git a/target/scripts/helpers/aliases.sh b/target/scripts/helpers/aliases.sh index 9ded6da3dfe..b0f2fa1af95 100644 --- a/target/scripts/helpers/aliases.sh +++ b/target/scripts/helpers/aliases.sh @@ -6,47 +6,31 @@ # `setup-stack.sh:_setup_ldap` does not seem to configure for `/etc/postfix/virtual however.` # NOTE: `accounts.sh` and `relay.sh:_populate_relayhost_map` also process on `postfix-virtual.cf`. -function _handle_postfix_virtual_config -{ +function _handle_postfix_virtual_config() { : >/etc/postfix/virtual local DATABASE_VIRTUAL=/tmp/docker-mailserver/postfix-virtual.cf - if [[ -f ${DATABASE_VIRTUAL} ]] - then - # fixing old virtual user file - if grep -q ",$" "${DATABASE_VIRTUAL}" - then - sed -i -e "s|, |,|g" -e "s|,$||g" "${DATABASE_VIRTUAL}" - fi - + if [[ -f ${DATABASE_VIRTUAL} ]]; then cp -f "${DATABASE_VIRTUAL}" /etc/postfix/virtual else _log 'debug' "'${DATABASE_VIRTUAL}' not provided - no mail alias/forward created" fi } -function _handle_postfix_regexp_config -{ +# TODO: Investigate why this file is always created, nothing seems to append only the cp below? +function _handle_postfix_regexp_config() { : >/etc/postfix/regexp - if [[ -f /tmp/docker-mailserver/postfix-regexp.cf ]] - then + if [[ -f /tmp/docker-mailserver/postfix-regexp.cf ]]; then _log 'trace' "Adding regexp alias file postfix-regexp.cf" cp -f /tmp/docker-mailserver/postfix-regexp.cf /etc/postfix/regexp - - if ! grep 'virtual_alias_maps.*pcre:/etc/postfix/regexp' /etc/postfix/main.cf - then - sed -i -E \ - 's|virtual_alias_maps(.*)|virtual_alias_maps\1 pcre:/etc/postfix/regexp|g' \ - /etc/postfix/main.cf - fi + _add_to_or_update_postfix_main 'virtual_alias_maps' 'pcre:/etc/postfix/regexp' fi } -function _handle_postfix_aliases_config -{ +function _handle_postfix_aliases_config() { _log 'trace' 'Configuring root alias' echo "root: ${POSTMASTER_ADDRESS}" >/etc/aliases @@ -59,8 +43,7 @@ function _handle_postfix_aliases_config } # Other scripts should call this method, rather than the ones above: -function _create_aliases -{ +function _create_aliases() { _handle_postfix_virtual_config _handle_postfix_regexp_config _handle_postfix_aliases_config diff --git a/target/scripts/helpers/change-detection.sh b/target/scripts/helpers/change-detection.sh index 412b6322dea..a37df9ea2cf 100644 --- a/target/scripts/helpers/change-detection.sh +++ b/target/scripts/helpers/change-detection.sh @@ -12,8 +12,7 @@ CHKSUM_FILE=/tmp/docker-mailserver-config-chksum # Once container startup scripts complete, take a snapshot of # the config state via storing a list of files content hashes. -function _prepare_for_change_detection -{ +function _prepare_for_change_detection() { _log 'debug' 'Setting up configuration checksum file' _log 'trace' "Creating '${CHKSUM_FILE}'" @@ -22,8 +21,7 @@ function _prepare_for_change_detection # Returns a list of changed files, each line is a value pair of: # -function _monitored_files_checksums -{ +function _monitored_files_checksums() { # If a wildcard path pattern (or an empty ENV) would yield an invalid path # or no results, `shopt -s nullglob` prevents it from being added. shopt -s nullglob @@ -31,8 +29,7 @@ function _monitored_files_checksums # Supported user provided configs: local DMS_DIR=/tmp/docker-mailserver - if [[ -d ${DMS_DIR} ]] - then + if [[ -d ${DMS_DIR} ]]; then STAGING_FILES+=( "${DMS_DIR}/postfix-accounts.cf" "${DMS_DIR}/postfix-virtual.cf" @@ -43,11 +40,16 @@ function _monitored_files_checksums "${DMS_DIR}/dovecot-quotas.cf" "${DMS_DIR}/dovecot-masters.cf" ) + + # Check whether Rspamd is used and if so, monitor it's changes as well + if [[ ${ENABLE_RSPAMD} -eq 1 ]] && [[ -d ${RSPAMD_DMS_D} ]]; then + readarray -d '' STAGING_FILES_RSPAMD < <(find "${RSPAMD_DMS_D}" -type f -print0) + STAGING_FILES+=("${STAGING_FILES_RSPAMD[@]}") + fi fi # SSL certs: - if [[ ${SSL_TYPE:-} == 'manual' ]] - then + if [[ ${SSL_TYPE:-} == 'manual' ]]; then # When using "manual" as the SSL type, # the following variables may contain the certificate files STAGING_FILES+=( @@ -56,8 +58,7 @@ function _monitored_files_checksums "${SSL_ALT_CERT_PATH:-}" "${SSL_ALT_KEY_PATH:-}" ) - elif [[ ${SSL_TYPE:-} == 'letsencrypt' ]] - then + elif [[ ${SSL_TYPE:-} == 'letsencrypt' ]]; then # React to any cert changes within the following LetsEncrypt locations: STAGING_FILES+=( /etc/letsencrypt/acme.json @@ -69,13 +70,11 @@ function _monitored_files_checksums # If the file actually exists, add to CHANGED_FILES # and generate a content hash entry: - for FILE in "${STAGING_FILES[@]}" - do + for FILE in "${STAGING_FILES[@]}"; do [[ -f "${FILE}" ]] && CHANGED_FILES+=("${FILE}") done - if [[ -n ${CHANGED_FILES:-} ]] - then + if [[ -n ${CHANGED_FILES:-} ]]; then sha512sum -- "${CHANGED_FILES[@]}" fi } diff --git a/target/scripts/helpers/database/db.sh b/target/scripts/helpers/database/db.sh index 66476772e81..88717885add 100644 --- a/target/scripts/helpers/database/db.sh +++ b/target/scripts/helpers/database/db.sh @@ -18,8 +18,7 @@ DATABASE_PASSWD="${DMS_CONFIG}/postfix-sasl-password.cf" DATABASE_RELAY="${DMS_CONFIG}/postfix-relaymap.cf" # Individual scripts with convenience methods to manage operations easier: -function _db_import_scripts -{ +function _db_import_scripts() { # This var is stripped by shellcheck from source paths below, # like the shellcheck source-path above, it shouold match this scripts # parent directory, with the rest of the relative path in the source lines: @@ -35,8 +34,7 @@ function _db_entry_add_or_append { _db_operation 'append' "${@}" ; } # Only us function _db_entry_add_or_replace { _db_operation 'replace' "${@}" ; } function _db_entry_remove { _db_operation 'remove' "${@}" ; } -function _db_operation -{ +function _db_operation() { local DB_ACTION=${1} local DATABASE=${2} local KEY=${3} @@ -63,8 +61,7 @@ function _db_operation [[ ${DATABASE} == "${DATABASE_VIRTUAL}" ]] && V_DELIMITER=',' # Perform requested operation: - if _db_has_entry_with_key "${KEY}" "${DATABASE}" - then + if _db_has_entry_with_key "${KEY}" "${DATABASE}"; then # Find entry for key and return status code: case "${DB_ACTION}" in ( 'append' ) @@ -79,8 +76,7 @@ function _db_operation ;; ( 'remove' ) - if [[ -z ${VALUE} ]] - then # Remove entry for KEY: + if [[ -z ${VALUE} ]]; then # Remove entry for KEY: sedfile --strict -i "/^${KEY_LOOKUP}/d" "${DATABASE}" else # Remove target VALUE from entry: __db_list_already_contains_value || return 0 @@ -128,22 +124,20 @@ function _db_operation } # Internal method for: _db_operation -function __db_list_already_contains_value -{ +function __db_list_already_contains_value() { # Avoids accidentally matching a substring (case-insensitive acceptable): # 1. Extract the current value of the entry (`\1`), - # 2. If a value list, split into separate lines (`\n`+`g`) at V_DELIMITER, + # 2. Value list support: Split values into separate lines (`\n`+`g`) at V_DELIMITER, # 3. Check each line for an exact match of the target VALUE - sed -e "s/^${KEY_LOOKUP}\(.*\)/\1/" \ - -e "s/${V_DELIMITER}/\n/g" \ - "${DATABASE}" | grep -qi "^${_VALUE_}$" + sed -ne "s/^${KEY_LOOKUP}\+\(.*\)/\1/p" "${DATABASE}" \ + | sed -e "s/${V_DELIMITER}/\n/g" \ + | grep -qi "^${_VALUE_}$" } # Internal method for: _db_operation + _db_has_entry_with_key # References global vars `DATABASE_*`: -function __db_get_delimiter_for -{ +function __db_get_delimiter_for() { local DATABASE=${1} case "${DATABASE}" in @@ -173,8 +167,7 @@ function __db_get_delimiter_for # `\` can escape these (`/` exists in postfix-account.cf base64 encoded pw hash), # But otherwise care should be taken with `\`, which should be forbidden for input here? # NOTE: Presently only `.` is escaped with `\` via `_escape`. -function __escape_sed_replacement -{ +function __escape_sed_replacement() { # Matches any `/` or `&`, and escapes them with `\` (`\\\1`): sed 's/\([/&]\)/\\\1/g' <<< "${ENTRY}" } @@ -183,8 +176,7 @@ function __escape_sed_replacement # Validation Methods # -function _db_has_entry_with_key -{ +function _db_has_entry_with_key() { local KEY=${1} local DATABASE=${2} @@ -204,8 +196,7 @@ function _db_has_entry_with_key grep --quiet --no-messages --ignore-case "^${KEY_LOOKUP}" "${DATABASE}" } -function _db_should_exist_with_content -{ +function _db_should_exist_with_content() { local DATABASE=${1} [[ -f ${DATABASE} ]] || _exit_with_error "'${DATABASE}' does not exist" diff --git a/target/scripts/helpers/database/manage/dovecot-quotas.sh b/target/scripts/helpers/database/manage/dovecot-quotas.sh index 802eb78f651..098d17d47e9 100644 --- a/target/scripts/helpers/database/manage/dovecot-quotas.sh +++ b/target/scripts/helpers/database/manage/dovecot-quotas.sh @@ -3,8 +3,7 @@ # Manage DB writes for: DATABASE_QUOTA # Logic to perform for requested operations handled here: -function _manage_dovecot_quota -{ +function _manage_dovecot_quota() { local ACTION=${1} local MAIL_ACCOUNT=${2} # Only for ACTION 'update': diff --git a/target/scripts/helpers/database/manage/postfix-accounts.sh b/target/scripts/helpers/database/manage/postfix-accounts.sh index ad91982f84c..94641831424 100644 --- a/target/scripts/helpers/database/manage/postfix-accounts.sh +++ b/target/scripts/helpers/database/manage/postfix-accounts.sh @@ -5,8 +5,7 @@ # - DATABASE_DOVECOT_MASTERS # Logic to perform for requested operations handled here: -function _manage_accounts -{ +function _manage_accounts() { local ACTION=${1} local DATABASE=${2} local MAIL_ACCOUNT=${3} @@ -14,6 +13,7 @@ function _manage_accounts local PASSWD=${4} _arg_expect_mail_account + _arg_check_mail_account case "${ACTION}" in ( 'create' | 'update' ) @@ -56,12 +56,11 @@ function _manage_accounts_dovecotmaster_delete { _manage_accounts 'delete' "${DA # # These validation helpers rely on: -# - Exteral vars to be declared prior to calling them (MAIL_ACCOUNT, PASSWD, DATABASE). +# - External vars to be declared prior to calling them (MAIL_ACCOUNT, PASSWD, DATABASE). # - Calling external method '__usage' as part of error handling. # Also used by setquota, delquota -function _arg_expect_mail_account -{ +function _arg_expect_mail_account() { [[ -z ${MAIL_ACCOUNT} ]] && { __usage ; _exit_with_error 'No account specified' ; } # Dovecot Master accounts are validated (they are not email addresses): @@ -71,30 +70,42 @@ function _arg_expect_mail_account [[ ${MAIL_ACCOUNT} =~ .*\@.* ]] || { __usage ; _exit_with_error "'${MAIL_ACCOUNT}' should include the domain (eg: user@example.com)" ; } } -function _account_should_not_exist_yet -{ +# Checks the mail account string, e.g. on uppercase letters. +function _arg_check_mail_account() { + if grep -q -E '[[:upper:]]+' <<< "${MAIL_ACCOUNT}"; then + local MAIL_ACCOUNT_NORMALIZED=${MAIL_ACCOUNT,,} + _log 'warn' "Mail account '${MAIL_ACCOUNT}' has uppercase letters and will be normalized to '${MAIL_ACCOUNT_NORMALIZED}'" + MAIL_ACCOUNT=${MAIL_ACCOUNT_NORMALIZED} + fi +} + +function _account_should_not_exist_yet() { __account_already_exists && _exit_with_error "'${MAIL_ACCOUNT}' already exists" + if [[ -f ${DATABASE_VIRTUAL} ]] && grep -q "^${MAIL_ACCOUNT}" "${DATABASE_VIRTUAL}"; then + _exit_with_error "'${MAIL_ACCOUNT}' is already defined as an alias" + fi } # Also used by delmailuser, setquota, delquota -function _account_should_already_exist -{ +function _account_should_already_exist() { ! __account_already_exists && _exit_with_error "'${MAIL_ACCOUNT}' does not exist" } -function __account_already_exists -{ +function __account_already_exists() { local DATABASE=${DATABASE:-"${DATABASE_ACCOUNTS}"} _db_has_entry_with_key "${MAIL_ACCOUNT}" "${DATABASE}" } # Also used by addsaslpassword -function _password_request_if_missing -{ - if [[ -z ${PASSWD} ]] - then +function _password_request_if_missing() { + local PASSWD_CONFIRM + if [[ -z ${PASSWD} ]]; then read -r -s -p 'Enter Password: ' PASSWD echo [[ -z ${PASSWD} ]] && _exit_with_error 'Password must not be empty' + + read -r -s -p 'Confirm Password: ' PASSWD_CONFIRM + echo + [[ ${PASSWD} != "${PASSWD_CONFIRM}" ]] && _exit_with_error 'Passwords do not match!' fi } diff --git a/target/scripts/helpers/database/manage/postfix-virtual.sh b/target/scripts/helpers/database/manage/postfix-virtual.sh index c4108f90579..c2ff5488ae1 100644 --- a/target/scripts/helpers/database/manage/postfix-virtual.sh +++ b/target/scripts/helpers/database/manage/postfix-virtual.sh @@ -11,8 +11,7 @@ # mail to an alias address. # Logic to perform for requested operations handled here: -function _manage_virtual_aliases -{ +function _manage_virtual_aliases() { local ACTION=${1} local MAIL_ALIAS=${2} local RECIPIENT=${3} @@ -25,6 +24,9 @@ function _manage_virtual_aliases case "${ACTION}" in # Associate RECIPIENT to MAIL_ALIAS: ( 'update' ) + if [[ -f ${DATABASE_ACCOUNTS} ]] && grep -q "^${MAIL_ALIAS}" "${DATABASE_ACCOUNTS}"; then + _exit_with_error "'${MAIL_ALIAS}' is already defined as an account" + fi _db_entry_add_or_append "${DATABASE_VIRTUAL}" "${MAIL_ALIAS}" "${RECIPIENT}" ;; diff --git a/target/scripts/helpers/dns.sh b/target/scripts/helpers/dns.sh index 4301fa7d577..acb32bc727d 100644 --- a/target/scripts/helpers/dns.sh +++ b/target/scripts/helpers/dns.sh @@ -2,15 +2,13 @@ # Outputs the DNS label count (delimited by `.`) for the given input string. # Useful for determining an FQDN like `mail.example.com` (3), vs `example.com` (2). -function _get_label_count -{ +function _get_label_count() { awk -F '.' '{ print NF }' <<< "${1}" } # Sets HOSTNAME and DOMAINNAME globals used throughout the scripts, -# and any subprocesses called that intereact with it. -function _obtain_hostname_and_domainname -{ +# and any subprocesses called that interact with it. +function _obtain_hostname_and_domainname() { # Normally this value would match the output of `hostname` which mirrors `/proc/sys/kernel/hostname`, # However for legacy reasons, the system ENV `HOSTNAME` was replaced here with `hostname -f` instead. # @@ -27,8 +25,7 @@ function _obtain_hostname_and_domainname # If the container is misconfigured.. `hostname -f` (which derives it's return value from `/etc/hosts` or DNS query), # will result in an error that returns an empty value. This warrants a panic. - if [[ -z ${HOSTNAME} ]] - then + if [[ -z ${HOSTNAME} ]]; then _dms_panic__misconfigured 'obtain_hostname' '/etc/hosts' fi @@ -39,10 +36,8 @@ function _obtain_hostname_and_domainname # `hostname -d` was probably not the correct command for this intention either. # Needs further investigation for relevance, and if `/etc/hosts` is important for consumers # of this variable or if a more deterministic approach with `cut` should be relied on. - if [[ $(_get_label_count "${HOSTNAME}") -gt 2 ]] - then - if [[ -n ${OVERRIDE_HOSTNAME} ]] - then + if [[ $(_get_label_count "${HOSTNAME}") -gt 2 ]]; then + if [[ -n ${OVERRIDE_HOSTNAME:-} ]]; then # Emulates the intended behaviour of `hostname -d`: # Assign the HOSTNAME value minus everything up to and including the first `.` DOMAINNAME=${HOSTNAME#*.} diff --git a/target/scripts/helpers/error.sh b/target/scripts/helpers/error.sh index 98e758920bd..d4462b9f88a 100644 --- a/target/scripts/helpers/error.sh +++ b/target/scripts/helpers/error.sh @@ -1,9 +1,7 @@ #!/bin/bash -function _exit_with_error -{ - if [[ -n ${1+set} ]] - then +function _exit_with_error() { + if [[ -n ${1:-} ]]; then _log 'error' "${1}" else _log 'error' "Call to '_exit_with_error' is missing a message to log" @@ -20,12 +18,10 @@ function _exit_with_error # PANIC_TYPE => (Internal value for matching). You should use the convenience methods below based on your panic type. # PANIC_INFO => Provide your own message string to insert into the error message for that PANIC_TYPE. # PANIC_SCOPE => Optionally provide a string for debugging to better identify/locate the source of the panic. -function dms_panic -{ +function dms_panic() { local PANIC_TYPE=${1:-} local PANIC_INFO=${2:-} - local PANIC_SCOPE=${3-} # optional, must not be :- but just - - local PANIC_STRATEGY=${4:-} # optional + local PANIC_SCOPE=${3:-} local SHUTDOWN_MESSAGE @@ -39,7 +35,7 @@ function dms_panic ;; ( 'no-file' ) # PANIC_INFO == - SHUTDOWN_MESSAGE="File ${PANIC_INFO} does not exist!" + SHUTDOWN_MESSAGE="Missing expected file(s): ${PANIC_INFO}" ;; ( 'misconfigured' ) # PANIC_INFO == @@ -59,11 +55,10 @@ function dms_panic ;; esac - if [[ -n ${PANIC_SCOPE:-} ]] - then - _shutdown "${PANIC_SCOPE} | ${SHUTDOWN_MESSAGE}" "${PANIC_STRATEGY}" + if [[ -n ${PANIC_SCOPE:-} ]]; then + _shutdown "${PANIC_SCOPE} | ${SHUTDOWN_MESSAGE}" else - _shutdown "${SHUTDOWN_MESSAGE}" "${PANIC_STRATEGY}" + _shutdown "${SHUTDOWN_MESSAGE}" fi } @@ -77,22 +72,34 @@ function _dms_panic__general { dms_panic 'general' "${1:-}" "${2:-}" # Call this method when you want to panic (i.e. emit an 'ERROR' log, and exit uncleanly). # `dms_panic` methods should be preferred if your failure type is supported. -function _shutdown -{ +trap "exit 1" SIGUSR1 +SCRIPT_PID=${$} +function _shutdown() { _log 'error' "${1:-_shutdown called without message}" _log 'error' 'Shutting down' sleep 1 - kill 1 + kill -SIGTERM 1 # Trigger graceful DMS shutdown. + kill -SIGUSR1 "${SCRIPT_PID}" # Stop start-mailserver.sh execution, even when _shutdown() is called from a subshell. +} - if [[ ${2:-wait} == 'immediate' ]] - then - # In case the user requested an immediate exit, he ensure he is not in a subshell - # call and exiting the whole script is safe. This way, we make the shutdown quicker. - exit 1 - else - # We can simply wait until Supervisord has terminated all processes; this way, - # we do not return from a subshell call and continue as if nothing happened. - sleep 1000 - fi +# Calling this function sets up a handler for the `ERR` signal, that occurs when +# an error is not properly checked (e.g., in an `if`-clause or in an `&&` block). +# +# This is mostly useful for debugging. It also helps when using something like `set -eE`, +# as it shows where the script aborts. +function _trap_err_signal() { + trap '__log_unexpected_error "${FUNCNAME[0]:-}" "${BASH_COMMAND:-}" "${LINENO:-}" "${?:-}"' ERR + + # shellcheck disable=SC2317 + function __log_unexpected_error() { + local MESSAGE="Unexpected error occurred :: script = ${SCRIPT:-${0}} " + MESSAGE+=" | function = ${1:-none (global)}" + MESSAGE+=" | command = ${2:-?}" + MESSAGE+=" | line = ${3:-?}" + MESSAGE+=" | exit code = ${4:-?}" + + _log 'error' "${MESSAGE}" + return 0 + } } diff --git a/target/scripts/helpers/index.sh b/target/scripts/helpers/index.sh index 1e919513de9..0d77c9c4740 100644 --- a/target/scripts/helpers/index.sh +++ b/target/scripts/helpers/index.sh @@ -3,8 +3,7 @@ # shellcheck source-path=target/scripts/helpers # This file serves as a single import for all helpers -function _import_scripts -{ +function _import_scripts() { local PATH_TO_SCRIPTS='/usr/local/bin/helpers' source "${PATH_TO_SCRIPTS}/accounts.sh" @@ -17,6 +16,7 @@ function _import_scripts source "${PATH_TO_SCRIPTS}/network.sh" source "${PATH_TO_SCRIPTS}/postfix.sh" source "${PATH_TO_SCRIPTS}/relay.sh" + source "${PATH_TO_SCRIPTS}/rspamd.sh" source "${PATH_TO_SCRIPTS}/ssl.sh" source "${PATH_TO_SCRIPTS}/utils.sh" diff --git a/target/scripts/helpers/lock.sh b/target/scripts/helpers/lock.sh index 68bbd537a96..9aa5520088e 100644 --- a/target/scripts/helpers/lock.sh +++ b/target/scripts/helpers/lock.sh @@ -7,15 +7,12 @@ SCRIPT_NAME=$(basename "$0") # prevent removal by other instances of docker-mailserver LOCK_ID=$(uuid) -function _create_lock -{ +function _create_lock() { LOCK_FILE="/tmp/docker-mailserver/${SCRIPT_NAME}.lock" - while [[ -e "${LOCK_FILE}" ]] - do + while [[ -e "${LOCK_FILE}" ]]; do # Handle stale lock files left behind on crashes # or premature/non-graceful exits of containers while they're making changes - if [[ -n "$(find "${LOCK_FILE}" -mmin +1 2>/dev/null)" ]] - then + if [[ -n "$(find "${LOCK_FILE}" -mmin +1 2>/dev/null)" ]]; then _log 'warn' 'Lock file older than 1 minute - removing stale lock file' rm -f "${LOCK_FILE}" else @@ -29,12 +26,10 @@ function _create_lock echo "${LOCK_ID}" >"${LOCK_FILE}" } -function _remove_lock -{ +function _remove_lock() { LOCK_FILE="${LOCK_FILE:-"/tmp/docker-mailserver/${SCRIPT_NAME}.lock"}" [[ -z "${LOCK_ID}" ]] && _exit_with_error "Cannot remove '${LOCK_FILE}' as there is no LOCK_ID set" - if [[ -e "${LOCK_FILE}" ]] && grep -q "${LOCK_ID}" "${LOCK_FILE}" # Ensure we don't delete a lock that's not ours - then + if [[ -e "${LOCK_FILE}" ]] && grep -q "${LOCK_ID}" "${LOCK_FILE}"; then # Ensure we don't delete a lock that's not ours rm -f "${LOCK_FILE}" _log 'trace' "Removed lock '${LOCK_FILE}'" fi diff --git a/target/scripts/helpers/log.sh b/target/scripts/helpers/log.sh index 80d8c4b0698..912312044b2 100644 --- a/target/scripts/helpers/log.sh +++ b/target/scripts/helpers/log.sh @@ -42,22 +42,18 @@ RESET=$(echo -ne '\e[0m') # If the first argument is not set or invalid, an error # message is logged. Likewise when the second argument # is missing. Both failures will return with exit code '1'. -function _log -{ - if [[ -z ${1+set} ]] - then +function _log() { + if [[ -z ${1:-} ]]; then _log 'error' "Call to '_log' is missing a valid log level" return 1 fi - if [[ -z ${2+set} ]] - then + if [[ -z ${2:-} ]]; then _log 'error' "Call to '_log' is missing a message to log" return 1 fi - local LEVEL_AS_INT - local MESSAGE="${RESET}[" + local LEVEL_AS_INT LOG_COLOR LOG_LEVEL_NAME MESSAGE case "$(_get_log_level_or_default)" in ( 'trace' ) LEVEL_AS_INT=5 ;; @@ -70,27 +66,35 @@ function _log case "${1}" in ( 'trace' ) [[ ${LEVEL_AS_INT} -ge 5 ]] || return 0 - MESSAGE+=" ${CYAN}TRACE " + LOG_COLOR='CYAN' + LOG_LEVEL_NAME='TRACE' ;; ( 'debug' ) [[ ${LEVEL_AS_INT} -ge 4 ]] || return 0 - MESSAGE+=" ${PURPLE}DEBUG " + LOG_COLOR='PURPLE' + LOG_LEVEL_NAME='DEBUG' ;; ( 'info' ) [[ ${LEVEL_AS_INT} -ge 3 ]] || return 0 - MESSAGE+=" ${BLUE}INF " + LOG_COLOR='BLUE' + # the whitespace is intentional (for alignment purposes) + LOG_LEVEL_NAME='INFO ' ;; ( 'warn' ) [[ ${LEVEL_AS_INT} -ge 2 ]] || return 0 - MESSAGE+=" ${LYELLOW}WARNING " + LOG_COLOR='LYELLOW' + # the whitespace is intentional (for alignment purposes) + LOG_LEVEL_NAME='WARN ' ;; ( 'error' ) [[ ${LEVEL_AS_INT} -ge 1 ]] || return 0 - MESSAGE+=" ${LRED}ERROR " ;; + LOG_COLOR='LRED' + LOG_LEVEL_NAME='ERROR' + ;; ( * ) _log 'error' "Call to '_log' with invalid log level argument '${1}'" @@ -98,34 +102,22 @@ function _log ;; esac - MESSAGE+="${RESET}] ${2}" - - if [[ ${1} =~ ^(warn|error)$ ]] - then - echo -e "${MESSAGE}" >&2 - else - echo -e "${MESSAGE}" - fi -} + MESSAGE="$(date --rfc-3339='seconds') ${!LOG_COLOR}${LOG_LEVEL_NAME}${RESET} $(basename "${0}"): ${2}" -# Like `_log` but adds a timestamp in front of the message. -function _log_with_date -{ - _log "${1}" "$(date '+%Y-%m-%d %H:%M:%S') ${2}" + # All logs should go through to STDERR, + # STDOUT is only appropriate for expected program output + echo -e "${MESSAGE}" >&2 } # Get the value of the environment variable LOG_LEVEL if # it is set. Otherwise, try to query the common environment # variables file. If this does not yield a value either, # use the default log level. -function _get_log_level_or_default -{ - if [[ -n ${LOG_LEVEL+set} ]] - then +function _get_log_level_or_default() { + if [[ -n ${LOG_LEVEL:-} ]]; then echo "${LOG_LEVEL}" - elif [[ -e /etc/dms-settings ]] - then - grep "^LOG_LEVEL=" /etc/dms-settings | cut -d "'" -f 2 + elif [[ -e /etc/dms-settings ]] && grep -q -E "^LOG_LEVEL='[a-z]+'" /etc/dms-settings; then + grep '^LOG_LEVEL=' /etc/dms-settings | cut -d "'" -f 2 else echo 'info' fi @@ -133,7 +125,6 @@ function _get_log_level_or_default # This function checks whether the log level is the one # provided as the first argument. -function _log_level_is -{ +function _log_level_is() { [[ $(_get_log_level_or_default) =~ ^${1}$ ]] } diff --git a/target/scripts/helpers/network.sh b/target/scripts/helpers/network.sh index a1e3665d180..a710df31370 100644 --- a/target/scripts/helpers/network.sh +++ b/target/scripts/helpers/network.sh @@ -1,12 +1,9 @@ #!/bin/bash -function _mask_ip_digit -{ - if [[ ${1} -ge 8 ]] - then +function _mask_ip_digit() { + if [[ ${1} -ge 8 ]]; then MASK=255 - elif [[ ${1} -le 0 ]] - then + elif [[ ${1} -le 0 ]]; then MASK=0 else VALUES=(0 128 192 224 240 248 252 254 255) @@ -23,15 +20,13 @@ function _mask_ip_digit # like 1.2.3.4/16 to subnet with cidr suffix # like 1.2.0.0/16. # Assumes correct IP and subnet are provided. -function _sanitize_ipv4_to_subnet_cidr -{ +function _sanitize_ipv4_to_subnet_cidr() { local DIGIT_PREFIX_LENGTH="${1#*/}" declare -a MASKED_DIGITS DIGITS IFS='.' ; read -r -a DIGITS < <(echo "${1%%/*}") ; unset IFS - for ((i = 0 ; i < 4 ; i++)) - do + for ((i = 0 ; i < 4 ; i++)); do MASKED_DIGITS[i]=$(_mask_ip_digit "${DIGIT_PREFIX_LENGTH}" "${DIGITS[i]}") DIGIT_PREFIX_LENGTH=$((DIGIT_PREFIX_LENGTH - 8)) done diff --git a/target/scripts/helpers/postfix.sh b/target/scripts/helpers/postfix.sh index 09ad14c884f..0bb1c15898d 100644 --- a/target/scripts/helpers/postfix.sh +++ b/target/scripts/helpers/postfix.sh @@ -17,8 +17,7 @@ # Should not be a concern for most types used by `docker-mailserver`: texthash, ldap, pcre, tcp, unionmap, unix. # The only other type in use by `docker-mailserver` is the hash type for /etc/aliases, which `postalias` handles. -function _create_postfix_vhost -{ +function _create_postfix_vhost() { # `main.cf` configures `virtual_mailbox_domains = /etc/postfix/vhost` # NOTE: Amavis also consumes this file. local DATABASE_VHOST='/etc/postfix/vhost' @@ -29,43 +28,43 @@ function _create_postfix_vhost } # Filter unique values into a proper DATABASE_VHOST config: -function _create_vhost -{ +function _create_vhost() { : >"${DATABASE_VHOST}" - if [[ -f ${TMP_VHOST} ]] - then + if [[ -f ${TMP_VHOST} ]]; then sort < "${TMP_VHOST}" | uniq >>"${DATABASE_VHOST}" rm "${TMP_VHOST}" fi } # Collects domains from configs (DATABASE_) into TMP_VHOST -function _vhost_collect_postfix_domains -{ +function _vhost_collect_postfix_domains() { local DATABASE_ACCOUNTS='/tmp/docker-mailserver/postfix-accounts.cf' local DATABASE_VIRTUAL='/tmp/docker-mailserver/postfix-virtual.cf' local DOMAIN UNAME - # getting domains FROM mail accounts - if [[ -f ${DATABASE_ACCOUNTS} ]] - then - while IFS=$'|' read -r LOGIN _ - do - DOMAIN=$(echo "${LOGIN}" | cut -d @ -f2) + # Extract domains from mail accounts: + if [[ -f ${DATABASE_ACCOUNTS} ]]; then + while IFS=$'|' read -r MAIL_ACCOUNT _; do + # It is expected valid lines have the format local-part@domain-part: + DOMAIN=$(cut -d '@' -f 2 <<< "${MAIL_ACCOUNT}") + echo "${DOMAIN}" >>"${TMP_VHOST}" done < <(_get_valid_lines_from_file "${DATABASE_ACCOUNTS}") fi - # getting domains FROM mail aliases - if [[ -f ${DATABASE_VIRTUAL} ]] - then - while read -r FROM _ - do - UNAME=$(echo "${FROM}" | cut -d @ -f1) - DOMAIN=$(echo "${FROM}" | cut -d @ -f2) + # TODO: Consider if virtual aliases should be configured to the same vhost file: + # https://github.com/docker-mailserver/docker-mailserver/issues/2813#issuecomment-1272394563 + # Extract domains from virtual alias config: + # Aliases may have the forms: 'local-part@domain-part', only 'local-part', or '@domain-part' (wildcard catch-all) + if [[ -f ${DATABASE_VIRTUAL} ]]; then + while read -r ALIAS_FIELD _; do + UNAME=$(cut -d '@' -f 1 <<< "${ALIAS_FIELD}") + DOMAIN=$(cut -d '@' -f 2 <<< "${ALIAS_FIELD}") - # if they are equal it means the line looks like: "user1 other@domain.tld" + # Only add valid domain-parts found: + # The '@' is optional for an alias key (eg: "user1 other@domain.tld"), + # but cut with -f2 would output the same value as it would -f1 when '@' is missing. [[ ${UNAME} != "${DOMAIN}" ]] && echo "${DOMAIN}" >>"${TMP_VHOST}" done < <(_get_valid_lines_from_file "${DATABASE_VIRTUAL}") fi @@ -78,8 +77,7 @@ function _vhost_collect_postfix_domains # - `main.cf:mydestination` setting removes `$mydestination` as an LDAP bugfix. # - `main.cf:virtual_mailbox_domains` uses `/etc/postfix/vhost`, but may # conditionally include a 2nd table (ldap:/etc/postfix/ldap-domains.cf). -function _vhost_ldap_support -{ +function _vhost_ldap_support() { [[ ${ACCOUNT_PROVISIONER} == 'LDAP' ]] && echo "${DOMAINNAME}" >>"${TMP_VHOST}" } @@ -100,3 +98,44 @@ function _vhost_ldap_support # # /etc/aliases is handled by `alias.sh` and uses `postalias` to update the Postfix alias database. No need for `postmap`. # http://www.postfix.org/postalias.1.html + +# Add a key with a value to Postfix's main configuration file +# or update an existing key. An already existing key can be updated +# by either appending to the existing value (default) or by prepending. +# +# @param ${1} = key name in Postfix's main configuration file +# @param ${2} = new value (appended or prepended) +# @param ${3} = action "append" (default) or "prepend" [OPTIONAL] +function _add_to_or_update_postfix_main() { + local KEY=${1:?Key name is required} + local NEW_VALUE=${2:?New value is required} + local ACTION=${3:-append} + local CURRENT_VALUE + + # Get current value from /etc/postfix/main.cf + _adjust_mtime_for_postfix_maincf + CURRENT_VALUE=$(postconf -h "${KEY}" 2>/dev/null) + + # If key does not exist or value is empty, add it - otherwise update with ACTION: + if [[ -z ${CURRENT_VALUE} ]]; then + postconf "${KEY} = ${NEW_VALUE}" + else + # If $NEW_VALUE is already present --> nothing to do, skip. + if [[ " ${CURRENT_VALUE} " == *" ${NEW_VALUE} "* ]]; then + return 0 + fi + + case "${ACTION}" in + ('append') + postconf "${KEY} = ${CURRENT_VALUE} ${NEW_VALUE}" + ;; + ('prepend') + postconf "${KEY} = ${NEW_VALUE} ${CURRENT_VALUE}" + ;; + (*) + _log 'error' "Action '${3}' in _add_to_or_update_postfix_main is unknown" + return 1 + ;; + esac + fi +} diff --git a/target/scripts/helpers/relay.sh b/target/scripts/helpers/relay.sh index b930489435e..b92da3b42c4 100644 --- a/target/scripts/helpers/relay.sh +++ b/target/scripts/helpers/relay.sh @@ -4,9 +4,9 @@ # Description: # This helper is responsible for configuring outbound SMTP (delivery) through relay-hosts. # -# When mail is sent from Postfix, it is considered relaying to that destination (or the next hop). -# By default delivery external of the container would be direct to the MTA of the recipient address (destination). -# Alternatively mail can be indirectly delivered to the destination by routing through a different MTA (relay-host service). +# When mail is sent to Postfix and the destination is not a domain DMS manages, this requires relaying to that destination (or the next hop). +# By default outbound mail delivery would be direct to the MTA of the recipient address (destination). +# Alternatively mail can be delivered indirectly to that destination by routing through a different MTA (relay-host service). # # This helper is only concerned with relaying mail from authenticated submission (ports 587 + 465). # Thus it does not deal with `relay_domains` (which routes through `relay_transport` transport, default: `master.cf:relay`), @@ -37,35 +37,36 @@ # `postfix reload` or `supervisorctl restart postfix` should be run to properly apply config (which it is). # Otherwise use another table type such as `hash` and run `postmap` on the table after modification. # -# WARNING: Databases (tables above) are rebuilt during change detection. There is a minor chance of -# a lookup occurring during a rebuild of these files that may affect or delay delivery? +# WARNING: Databases (tables above) are rebuilt during change detection. +# There is a minor chance of a lookup occurring during a rebuild of these files that may affect or delay delivery? # TODO: Should instead perform an atomic operation with a temporary file + `mv` to replace? # Or switch back to using `hash` table type if plaintext access is not needed (unless retaining file for postmap). # Either way, plaintext copy is likely accessible if using our supported configs for providing them to the container. -# NOTE: Present support has enforced wrapping the relay host with `[]` (prevents DNS MX record lookup), -# which restricts what is supported by RELAY_HOST, although you usually do want to provide MX host directly. -# NOTE: Present support expects to always append a port with an implicit default of `25`. -# NOTE: DEFAULT_RELAY_HOST imposes neither restriction. +# NOTE: Present support has enforced wrapping the `RELAY_HOST` value with `[]` (prevents DNS MX record lookup), +# shouldn't be an issue as you typically do want to provide the MX host directly? This was presumably for config convenience. +# NOTE: Present support expects to always append a port (_with an implicit default of `25`_). +# NOTE: The `DEFAULT_RELAY_HOST` ENV imposes neither restriction. # -# TODO: RELAY_PORT should be optional, it will use the transport default port (`postconf smtp_tcp_port`), +# TODO: `RELAY_PORT` should be optional (Postfix would fallback to the transports default port (`postconf smtp_tcp_port`), # That shouldn't be a breaking change, as long as the mapping is maintained correctly. -# TODO: RELAY_HOST should consider dropping `[]` and require the user to include that? -# Future refactor for _populate_relayhost_map may warrant dropping these two ENV in favor of DEFAULT_RELAY_HOST? -function _env_relay_host -{ +# TODO: `RELAY_HOST` should consider dropping the implicit `[]` and require the user to include that? +# +# A future refactor of `_populate_relayhost_map()` may warrant dropping those two ENV in favor of `DEFAULT_RELAY_HOST`? +function _env_relay_host() { echo "[${RELAY_HOST}]:${RELAY_PORT:-25}" } # Responsible for `postfix-sasl-password.cf` support: # `/etc/postfix/sasl_passwd` example at end of file. -function _relayhost_sasl -{ - if [[ ! -f /tmp/docker-mailserver/postfix-sasl-password.cf ]] \ - && [[ -z ${RELAY_USER} || -z ${RELAY_PASSWORD} ]] - then - _log 'warn' "Missing relay-host mapped credentials provided via ENV, or from postfix-sasl-password.cf" +function _relayhost_sasl() { + local DATABASE_SASL_PASSWD='/tmp/docker-mailserver/postfix-sasl-password.cf' + + # Only relevant when required credential sources are provided: + if [[ ! -f ${DATABASE_SASL_PASSWD} ]] \ + && [[ -z ${RELAY_USER} || -z ${RELAY_PASSWORD} ]]; then + _log 'warn' "Missing relay-host mapped credentials provided via ENV, or from ${DATABASE_SASL_PASSWD}" return 1 fi @@ -76,9 +77,7 @@ function _relayhost_sasl chown root:root /etc/postfix/sasl_passwd chmod 0600 /etc/postfix/sasl_passwd - local DATABASE_SASL_PASSWD='/tmp/docker-mailserver/postfix-sasl-password.cf' - if [[ -f ${DATABASE_SASL_PASSWD} ]] - then + if [[ -f ${DATABASE_SASL_PASSWD} ]]; then # Add domain-specific auth from config file: _get_valid_lines_from_file "${DATABASE_SASL_PASSWD}" >> /etc/postfix/sasl_passwd @@ -86,130 +85,121 @@ function _relayhost_sasl postconf 'smtp_sender_dependent_authentication = yes' fi - # Add an authenticated relay host defined via ENV config: - if [[ -n ${RELAY_USER} ]] && [[ -n ${RELAY_PASSWORD} ]] - then - echo "$(_env_relay_host) ${RELAY_USER}:${RELAY_PASSWORD}" >> /etc/postfix/sasl_passwd + # Support authentication to a primary relayhost (when configured with credentials via ENV): + if [[ -n ${DEFAULT_RELAY_HOST} || -n ${RELAY_HOST} ]] \ + && [[ -n ${RELAY_USER} && -n ${RELAY_PASSWORD} ]]; then + echo "${DEFAULT_RELAY_HOST:-$(_env_relay_host)} ${RELAY_USER}:${RELAY_PASSWORD}" >>/etc/postfix/sasl_passwd fi - # Technically if only a single relay host is configured, a `static` lookup table could be used instead?: - # postconf "smtp_sasl_password_maps = static:${RELAY_USER}:${RELAY_PASSWORD}" - postconf 'smtp_sasl_password_maps = texthash:/etc/postfix/sasl_passwd' + # Enable credential lookup + SASL authentication to relayhost: + # - `noanonymous` enforces authentication requirement + # - `encrypt` enforces requirement for a secure connection (prevents sending credentials over cleartext, aka mandatory TLS) + postconf \ + 'smtp_sasl_password_maps = texthash:/etc/postfix/sasl_passwd' \ + 'smtp_sasl_auth_enable = yes' \ + 'smtp_sasl_security_options = noanonymous' \ + 'smtp_tls_security_level = encrypt' } # Responsible for `postfix-relaymap.cf` support: # `/etc/postfix/relayhost_map` example at end of file. # -# Present support uses a table lookup for sender address or domain mapping to relay-hosts, -# Populated via `postfix-relaymap.cf `, which also features a non-standard way to exclude implicitly added internal domains from the feature. -# It also maps all known sender domains (from configs postfix-accounts + postfix-virtual.cf) to the same ENV configured relay-host. -# -# TODO: The account + virtual config parsing and appending to /etc/postfix/relayhost_map seems to be an excessive `main.cf:relayhost` -# implementation, rather than leveraging that for the same purpose and selectively overriding only when needed with `/etc/postfix/relayhost_map`. -# If the issue was to opt-out select domains, if avoiding a default relay-host was not an option, then mapping those sender domains or addresses -# to a separate transport (which can drop the `relayhost` setting) would be more appropriate. -# TODO: With `sender_dependent_default_transport_maps`, we can extract out the excluded domains and route them through a separate transport. -# while deprecating that support in favor of a transport config, similar to what is offered currently via sasl_passwd and relayhost_map. -function _populate_relayhost_map -{ +# `postfix-relaymap.cf` represents table syntax expected for `/etc/postfix/relayhost_map`, except that it adds an opt-out parsing feature. +# All known mail domains managed by DMS (/etc/postfix/vhost) are implicitly configured to use `RELAY_HOST` + `RELAY_PORT` as the default relay. +# This approach is effectively equivalent to using `main.cf:relayhost`, but with an excessive workaround to support the explicit opt-out feature. +# +# TODO: Refactor this feature support so that in `main.cf`: +# - Relay all outbound mail through an external MTA by default (works without credentials): +# `relayhost = ${DEFAULT_RELAY_HOST}` +# - Opt-in to relaying - Selectively relay outbound mail by sender/domain to an external MTA (relayhost can vary): +# `sender_dependent_relayhost_maps = texthash:/etc/postfix/relayhost_map` +# - Opt-out from relaying - Selectively prevent outbound mail from relaying via separate transport mappings (where relayhost is not configured): +# By sender: `sender_dependent_default_transport_maps = texthash:/etc/postfix/sender_transport_map` (the current opt-out feature could utilize this instead) +# By recipient (has precedence): `transport_maps = texthash:/etc/postfix/recipient_transport_map` +# +# Support for relaying via port 465 or equivalent requires additional config support (as needed for 465 vs 587 transports extending smtpd) +# - Default relay transport is configured by `relay_transport`, with default transport port configured by `smtp_tcp_port`. +# - The `relay` transport itself extends from `smtp` transport. More than one can be configured with separate settings via `master.cf`. + +function _populate_relayhost_map() { # Create the relayhost_map config file: : >/etc/postfix/relayhost_map chown root:root /etc/postfix/relayhost_map chmod 0600 /etc/postfix/relayhost_map - # Matches lines that are not comments or only white-space: - local MATCH_VALID='^\s*[^#[:space:]]' + _multiple_relayhosts + _legacy_support - # This config is mostly compatible with `/etc/postfix/relayhost_map`, but additionally supports - # not providing a relay host for a sender domain to opt-out of RELAY_HOST? (2nd half of function) - if [[ -f /tmp/docker-mailserver/postfix-relaymap.cf ]] - then - _log 'trace' "Adding relay mappings from postfix-relaymap.cf" + postconf 'sender_dependent_relayhost_maps = texthash:/etc/postfix/relayhost_map' +} + +function _multiple_relayhosts() { + if [[ -f ${DATABASE_RELAYHOSTS} ]]; then + _log 'trace' "Adding relay mappings from ${DATABASE_RELAYHOSTS}" + # Matches lines that are not comments or only white-space: + local MATCH_VALID='^\s*[^#[:space:]]' # Match two values with some white-space between them (eg: `@example.test [relay.service.test]:465`): local MATCH_VALUE_PAIR='\S*\s+\S' - # Copy over lines which are not a comment *and* have a destination. - sed -n -r "/${MATCH_VALID}${MATCH_VALUE_PAIR}/p" /tmp/docker-mailserver/postfix-relaymap.cf >>/etc/postfix/relayhost_map + # Copy over lines which are not a comment *and* have a relay destination. + # Extra condition is due to legacy support (due to opt-out feature), otherwise `_get_valid_lines_from_file()` would be valid. + sed -n -r "/${MATCH_VALID}${MATCH_VALUE_PAIR}/p" "${DATABASE_RELAYHOSTS}" >> /etc/postfix/relayhost_map fi +} - # Everything below here is to parse `postfix-accounts.cf` and `postfix-virtual.cf`, - # extracting out the domain parts (value of email address after `@`), and then - # adding those as mappings to ENV configured RELAY_HOST for lookup in `/etc/postfix/relayhost_map`. - # Provided `postfix-relaymap.cf` didn't exclude any of the domains, - # and they don't already exist within `/etc/postfix/relayhost_map`. - # - # TODO: Breaking change. Replace this lower half and remove the opt-out feature from `postfix-relaymap.cf`. - # Leverage `main.cf:relayhost` for setting a default relayhost as it was prior to this feature addition. - # Any sender domains or addresses that need to opt-out of that default relay-host can either - # map to a different relay-host, or use a separate transport (needs feature support added). - - # Args: - function _list_domain_parts - { - [[ -f $2 ]] && sed -n -r "/${MATCH_VALID}/ ${1}" "${2}" - } - # Matches and outputs (capture group via `/\1/p`) the domain part (value of address after `@`) in the config file. - local PRINT_DOMAIN_PART_ACCOUNTS='s/^[^@|]*@([^\|]+)\|.*$/\1/p' - local PRINT_DOMAIN_PART_VIRTUAL='s/^\s*[^@[:space:]]*@(\S+)\s.*/\1/p' - - { - _list_domain_parts "${PRINT_DOMAIN_PART_ACCOUNTS}" /tmp/docker-mailserver/postfix-accounts.cf - _list_domain_parts "${PRINT_DOMAIN_PART_VIRTUAL}" /tmp/docker-mailserver/postfix-virtual.cf - } | sort -u | while read -r DOMAIN_PART - do - # DOMAIN_PART not already present in `/etc/postfix/relayhost_map`, and not listed as a relay opt-out domain in `postfix-relaymap.cf` - # `^@${DOMAIN_PART}\b` - To check for existing entry, the `\b` avoids accidental partial matches on similar domain parts. - # `^\s*@${DOMAIN_PART}\s*$` - Matches line with only a domain part (eg: @example.test) to avoid including a mapping for those domains to the RELAY_HOST. - if ! grep -q -e "^@${DOMAIN_PART}\b" /etc/postfix/relayhost_map && ! grep -qs -e "^\s*@${DOMAIN_PART}\s*$" /tmp/docker-mailserver/postfix-relaymap.cf - then - _log 'trace' "Adding relay mapping for ${DOMAIN_PART}" - echo "@${DOMAIN_PART} $(_env_relay_host)" >> /etc/postfix/relayhost_map - fi - done +# Implicitly force configure all domains DMS manages to be relayed that haven't yet been configured or provided an explicit opt-out. +# This would normally be handled via an opt-in approach, or through `main.cf:relayhost` with an opt-out approach (sender_dependent_default_transport_maps) +function _legacy_support() { + local DATABASE_VHOST='/etc/postfix/vhost' - postconf 'sender_dependent_relayhost_maps = texthash:/etc/postfix/relayhost_map' -} + # Only relevant when `RELAY_HOST` is configured: + [[ -z ${RELAY_HOST} ]] && return 1 -function _relayhost_configure_postfix -{ - postconf \ - 'smtp_sasl_auth_enable = yes' \ - 'smtp_sasl_security_options = noanonymous' \ - 'smtp_tls_security_level = encrypt' + # Configures each `SENDER_DOMAIN` to send outbound mail through the default `RELAY_HOST` + `RELAY_PORT` + # (by adding an entry in `/etc/postfix/relayhost_map`) provided it: + # - `/etc/postfix/relayhost_map` doesn't already have it as an existing entry. + # - `postfix-relaymap.cf` has no explicit opt-out (SENDER_DOMAIN key exists, but with no relayhost value assigned) + # + # NOTE: /etc/postfix/vhost represents managed mail domains sourced from `postfix-accounts.cf` and `postfix-virtual.cf`. + while read -r SENDER_DOMAIN; do + local MATCH_EXISTING_ENTRY="^@${SENDER_DOMAIN}\s+" + local MATCH_OPT_OUT_LINE="^\s*@${SENDER_DOMAIN}\s*$" + + # NOTE: `-E` is required for `\s+` syntax to avoid escaping `+` + if ! grep -q -E "${MATCH_EXISTING_ENTRY}" /etc/postfix/relayhost_map && ! grep -qs "${MATCH_OPT_OUT_LINE}" "${DATABASE_RELAYHOSTS}"; then + _log 'trace' "Configuring '${SENDER_DOMAIN}' for the default relayhost '${RELAY_HOST}'" + echo "@${SENDER_DOMAIN} $(_env_relay_host)" >> /etc/postfix/relayhost_map + fi + done < <(_get_valid_lines_from_file "${DATABASE_VHOST}") } -function _setup_relayhost -{ +function _setup_relayhost() { _log 'debug' 'Setting up Postfix Relay Hosts' - if [[ -n ${DEFAULT_RELAY_HOST} ]] - then - _log 'trace' "Setting default relay host ${DEFAULT_RELAY_HOST} to /etc/postfix/main.cf" + if [[ -n ${DEFAULT_RELAY_HOST} ]]; then + _log 'trace' "Setting default relay host ${DEFAULT_RELAY_HOST}" postconf "relayhost = ${DEFAULT_RELAY_HOST}" fi - if [[ -n ${RELAY_HOST} ]] - then - _log 'trace' "Setting up relay hosts (default: ${RELAY_HOST})" + _process_relayhost_configs +} - _relayhost_sasl - _populate_relayhost_map +# Called during initial container setup, or by change detection event: +function _process_relayhost_configs() { + local DATABASE_RELAYHOSTS='/tmp/docker-mailserver/postfix-relaymap.cf' - _relayhost_configure_postfix + # One of these must configure a relayhost for the feature to relevant: + if [[ ! -f ${DATABASE_RELAYHOSTS} ]] \ + && [[ -z ${DEFAULT_RELAY_HOST} ]] \ + && [[ -z ${RELAY_HOST} ]]; then + return 1 fi -} -function _rebuild_relayhost -{ - if [[ -n ${RELAY_HOST} ]] - then - _relayhost_sasl - _populate_relayhost_map - fi + _relayhost_sasl + _populate_relayhost_map } - # # Config examples for reference # diff --git a/target/scripts/helpers/rspamd.sh b/target/scripts/helpers/rspamd.sh new file mode 100644 index 00000000000..1de0fb6a80c --- /dev/null +++ b/target/scripts/helpers/rspamd.sh @@ -0,0 +1,156 @@ +#! /bin/bash + +# shellcheck disable=SC2034 # VAR appears unused. + +# Perform a specific command as the Rspamd user (`_rspamd`). This is useful +# in case you want to have correct permissions on newly created files or if +# you want to check whether Rspamd can perform a specific action. +# +# @flag ${1} = '--quiet' to indicate whether log should be disabled [OPTIONAL] +function __do_as_rspamd_user() { + if [[ ${1:-} != '--quiet' ]]; then + _log 'trace' "Running '${*}' as user '_rspamd'" + else + shift 1 + fi + + su _rspamd -s /bin/bash -c "${*} 2>${__RSPAMD_ERR_LOG_FILE:-/dev/null}" +} + +# Create a temporary log file (with `mktemp`) that one can filter to search +# for error messages. This is required as `rspamadm` sometimes prints an error +# but does not exit with an error. +# +# The file created is managed in the ENV `__RSPAMD_ERR_LOG_FILE`. This ENV is +# meant for internal usage; do not use it on your scripts. The log file is cleaned +# up when the script exits. +function __create_rspamd_err_log() { + _log 'trace' "Creating Rspamd error log" + trap 'rm -f "${__RSPAMD_ERR_LOG_FILE}"' EXIT # cleanup when we exit + __RSPAMD_ERR_LOG_FILE=$(__do_as_rspamd_user --quiet mktemp) +} + +# Print the Rspamd temporary error log. This will succeed only when the log has been +# created before. +function __print_rspamd_err_log() { + [[ -v __RSPAMD_ERR_LOG_FILE ]] && __do_as_rspamd_user cat "${__RSPAMD_ERR_LOG_FILE}" +} + +# Print the Rspamd temporary error log. We use `grep` but with "fixed strings", which +# means the message you provide is evaluated as-is, not as a regular expression. This +# will succeed only when the log has been created before. +# +# @param ${1} = message to filter by +function __filter_rspamd_err_log() { + if [[ -v __RSPAMD_ERR_LOG_FILE ]]; then + __do_as_rspamd_user grep \ + --quiet \ + --ignore-case \ + --fixed-strings \ + "${1:?A message for filtering is required}" \ + "${__RSPAMD_ERR_LOG_FILE}" + fi +} + +# Calling this function brings common Rspamd-related environment variables +# into the current context. The environment variables are `readonly`, i.e. +# they cannot be modified. Use this function when you require common directory +# names, file names, etc. +function _rspamd_get_envs() { + # If the variables are already set, we cannot set them again as they are declared + # with `readonly`. Checking whether one is declared suffices, because either all + # are declared at once, or none. + if [[ ! -v RSPAMD_LOCAL_D ]]; then + readonly RSPAMD_LOCAL_D='/etc/rspamd/local.d' + readonly RSPAMD_OVERRIDE_D='/etc/rspamd/override.d' + + readonly RSPAMD_DMS_D='/tmp/docker-mailserver/rspamd' + readonly RSPAMD_DMS_DKIM_D="${RSPAMD_DMS_D}/dkim" + readonly RSPAMD_DMS_OVERRIDE_D="${RSPAMD_DMS_D}/override.d" + + readonly RSPAMD_DMS_CUSTOM_COMMANDS_F="${RSPAMD_DMS_D}/custom-commands.conf" + fi +} + +# Parses `RSPAMD_DMS_CUSTOM_COMMANDS_F` and executed the directives given by the file. +# To get a detailed explanation of the commands and how the file works, visit +# https://docker-mailserver.github.io/docker-mailserver/latest/config/security/rspamd/#with-the-help-of-a-custom-file +function _rspamd_handle_user_modules_adjustments() { + # Adds an option with a corresponding value to a module, or, in case the option + # is already present, overwrites it. + # + # @param ${1} = file name in ${RSPAMD_OVERRIDE_D}/ + # @param ${2} = module name as it should appear in the log + # @param ${3} = option name in the module + # @param ${4} = value of the option + # + # ## Note + # + # While this function is currently bound to the scope of `_rspamd_handle_user_modules_adjustments`, + # it is written in a versatile way (taking 4 arguments instead of assuming `ARGUMENT2` / `ARGUMENT3` + # are set) so that it may be used elsewhere if needed. + function __add_or_replace() { + local MODULE_FILE=${1:?Module file name must be provided} + local MODULE_LOG_NAME=${2:?Module log name must be provided} + local OPTION=${3:?Option name must be provided} + local VALUE=${4:?Value belonging to an option must be provided} + # remove possible whitespace at the end (e.g., in case ${ARGUMENT3} is empty) + VALUE=${VALUE% } + local FILE="${RSPAMD_OVERRIDE_D}/${MODULE_FILE}" + + readonly MODULE_FILE MODULE_LOG_NAME OPTION VALUE FILE + + [[ -f ${FILE} ]] || touch "${FILE}" + + if grep -q -E "${OPTION}.*=.*" "${FILE}"; then + __rspamd__log 'trace' "Overwriting option '${OPTION}' with value '${VALUE}' for ${MODULE_LOG_NAME}" + sed -i -E "s|([[:space:]]*${OPTION}).*|\1 = ${VALUE};|g" "${FILE}" + else + __rspamd__log 'trace' "Setting option '${OPTION}' for ${MODULE_LOG_NAME} to '${VALUE}'" + echo "${OPTION} = ${VALUE};" >>"${FILE}" + fi + } + + if [[ -f "${RSPAMD_DMS_CUSTOM_COMMANDS_F}" ]]; then + __rspamd__log 'debug' "Found file '${RSPAMD_DMS_CUSTOM_COMMANDS_F}' - parsing and applying it" + + local COMMAND ARGUMENT1 ARGUMENT2 ARGUMENT3 + while read -r COMMAND ARGUMENT1 ARGUMENT2 ARGUMENT3; do + case "${COMMAND}" in + ('disable-module') + __rspamd__helper__enable_disable_module "${ARGUMENT1}" 'false' 'override' + ;; + + ('enable-module') + __rspamd__helper__enable_disable_module "${ARGUMENT1}" 'true' 'override' + ;; + + ('set-option-for-module') + __add_or_replace "${ARGUMENT1}.conf" "module '${ARGUMENT1}'" "${ARGUMENT2}" "${ARGUMENT3}" + ;; + + ('set-option-for-controller') + __add_or_replace 'worker-controller.inc' 'controller worker' "${ARGUMENT1}" "${ARGUMENT2} ${ARGUMENT3}" + ;; + + ('set-option-for-proxy') + __add_or_replace 'worker-proxy.inc' 'proxy worker' "${ARGUMENT1}" "${ARGUMENT2} ${ARGUMENT3}" + ;; + + ('set-common-option') + __add_or_replace 'options.inc' 'common options' "${ARGUMENT1}" "${ARGUMENT2} ${ARGUMENT3}" + ;; + + ('add-line') + __rspamd__log 'trace' "Adding complete line to '${ARGUMENT1}'" + echo "${ARGUMENT2}${ARGUMENT3+ ${ARGUMENT3}}" >>"${RSPAMD_OVERRIDE_D}/${ARGUMENT1}" + ;; + + (*) + __rspamd__log 'warn' "Command '${COMMAND}' is invalid" + continue + ;; + esac + done < <(_get_valid_lines_from_file "${RSPAMD_DMS_CUSTOM_COMMANDS_F}") + fi +} diff --git a/target/scripts/helpers/ssl.sh b/target/scripts/helpers/ssl.sh index 94d5dc59008..c7d703171c5 100644 --- a/target/scripts/helpers/ssl.sh +++ b/target/scripts/helpers/ssl.sh @@ -1,16 +1,14 @@ #!/bin/bash -function _setup_dhparam -{ +function _setup_dhparam() { local DH_SERVICE=$1 local DH_DEST=$2 local DH_CUSTOM='/tmp/docker-mailserver/dhparams.pem' _log 'debug' "Setting up ${DH_SERVICE} dhparam" - if [[ -f ${DH_CUSTOM} ]] - then # use custom supplied dh params (assumes they're probably insecure) - _log 'trace' "${DH_SERVICE} will use custom provided DH paramters" + if [[ -f ${DH_CUSTOM} ]]; then # use custom supplied dh params (assumes they're probably insecure) + _log 'trace' "${DH_SERVICE} will use custom provided DH parameters" _log 'warn' "Using self-generated dhparams is considered insecure - unless you know what you are doing, please remove '${DH_CUSTOM}'" cp -f "${DH_CUSTOM}" "${DH_DEST}" @@ -19,8 +17,7 @@ function _setup_dhparam fi } -function _setup_ssl -{ +function _setup_ssl() { _log 'debug' 'Setting up SSL' local POSTFIX_CONFIG_MAIN='/etc/postfix/main.cf' @@ -32,15 +29,13 @@ function _setup_ssl mkdir -p "${DMS_TLS_PATH}" # Primary certificate to serve for TLS - function _set_certificate - { + function _set_certificate() { local POSTFIX_KEY_WITH_FULLCHAIN=${1} local DOVECOT_KEY=${1} local DOVECOT_CERT=${1} # If a 2nd param is provided, a separate key and cert was received instead of a fullkeychain - if [[ -n ${2} ]] - then + if [[ -n ${2} ]]; then local PRIVATE_KEY=$1 local CERT_CHAIN=$2 @@ -62,8 +57,7 @@ function _setup_ssl } # Enables supporting two certificate types such as ECDSA with an RSA fallback - function _set_alt_certificate - { + function _set_alt_certificate() { local COPY_KEY_FROM_PATH=$1 local COPY_CERT_FROM_PATH=$2 local PRIVATE_KEY_ALT="${DMS_TLS_PATH}/fallback_key" @@ -77,21 +71,20 @@ function _setup_ssl # Postfix configuration # NOTE: This operation doesn't replace the line, it appends to the end of the line. # Thus this method should only be used when this line has explicitly been replaced earlier in the script. - # Otherwise without `docker-compose down` first, a `docker-compose up` may + # Otherwise without `docker compose down` first, a `docker compose up` may # persist previous container state and cause a failure in postfix configuration. sedfile -i "s|^smtpd_tls_chain_files =.*|& ${PRIVATE_KEY_ALT} ${CERT_CHAIN_ALT}|" "${POSTFIX_CONFIG_MAIN}" # Dovecot configuration # Conditionally checks for `#`, in the event that internal container state is accidentally persisted, - # can be caused by: `docker-compose up` run again after a `ctrl+c`, without running `docker-compose down` + # can be caused by: `docker compose up` run again after a `ctrl+c`, without running `docker compose down` sedfile -i -r \ -e "s|^#?(ssl_alt_key =).*|\1 <${PRIVATE_KEY_ALT}|" \ -e "s|^#?(ssl_alt_cert =).*|\1 <${CERT_CHAIN_ALT}|" \ "${DOVECOT_CONFIG_SSL}" } - function _apply_tls_level - { + function _apply_tls_level() { local TLS_CIPHERS_ALLOW=$1 local TLS_PROTOCOL_IGNORE=$2 local TLS_PROTOCOL_MINIMUM=$3 @@ -115,24 +108,19 @@ function _setup_ssl # Extracts files `key.pem` and `fullchain.pem`. # `_extract_certs_from_acme` is located in `helpers/ssl.sh` # NOTE: See the `SSL_TYPE=letsencrypt` case below for more details. - function _traefik_support - { - if [[ -f /etc/letsencrypt/acme.json ]] - then + function _traefik_support() { + if [[ -f /etc/letsencrypt/acme.json ]]; then # Variable only intended for troubleshooting via debug output local EXTRACTED_DOMAIN # Conditional handling depends on the success of `_extract_certs_from_acme`, # Failure tries the next fallback FQDN to try extract a certificate from. # Subshell not used in conditional to ensure extraction log output is still captured - if [[ -n ${SSL_DOMAIN} ]] && _extract_certs_from_acme "${SSL_DOMAIN}" - then + if [[ -n ${SSL_DOMAIN} ]] && _extract_certs_from_acme "${SSL_DOMAIN}"; then EXTRACTED_DOMAIN=('SSL_DOMAIN' "${SSL_DOMAIN}") - elif _extract_certs_from_acme "${HOSTNAME}" - then + elif _extract_certs_from_acme "${HOSTNAME}"; then EXTRACTED_DOMAIN=('HOSTNAME' "${HOSTNAME}") - elif _extract_certs_from_acme "${DOMAINNAME}" - then + elif _extract_certs_from_acme "${DOMAINNAME}"; then EXTRACTED_DOMAIN=('DOMAINNAME' "${DOMAINNAME}") else _log 'warn' "letsencrypt (acme.json) failed to identify a certificate to extract" @@ -220,8 +208,7 @@ function _setup_ssl local TMP_KEY_WITH_FULLCHAIN="${TMP_DMS_TLS_PATH}/${COMBINED_PEM_NAME}" local KEY_WITH_FULLCHAIN="${DMS_TLS_PATH}/${COMBINED_PEM_NAME}" - if [[ -f ${TMP_KEY_WITH_FULLCHAIN} ]] - then + if [[ -f ${TMP_KEY_WITH_FULLCHAIN} ]]; then cp "${TMP_KEY_WITH_FULLCHAIN}" "${KEY_WITH_FULLCHAIN}" chmod 600 "${KEY_WITH_FULLCHAIN}" @@ -241,8 +228,7 @@ function _setup_ssl local CERT_CHAIN="${DMS_TLS_PATH}/cert" # Fail early: - if [[ -z ${SSL_KEY_PATH} ]] && [[ -z ${SSL_CERT_PATH} ]] - then + if [[ -z ${SSL_KEY_PATH} ]] && [[ -z ${SSL_CERT_PATH} ]]; then _dms_panic__no_env 'SSL_KEY_PATH or SSL_CERT_PATH' "${SCOPE_SSL_TYPE}" fi @@ -254,8 +240,7 @@ function _setup_ssl _dms_panic__no_file "(ALT) ${SSL_ALT_KEY_PATH} or ${SSL_ALT_CERT_PATH}" "${SCOPE_SSL_TYPE}" fi - if [[ -f ${SSL_KEY_PATH} ]] && [[ -f ${SSL_CERT_PATH} ]] - then + if [[ -f ${SSL_KEY_PATH} ]] && [[ -f ${SSL_CERT_PATH} ]]; then cp "${SSL_KEY_PATH}" "${PRIVATE_KEY}" cp "${SSL_CERT_PATH}" "${CERT_CHAIN}" chmod 600 "${PRIVATE_KEY}" @@ -264,8 +249,7 @@ function _setup_ssl _set_certificate "${PRIVATE_KEY}" "${CERT_CHAIN}" # Support for a fallback certificate, useful for hybrid/dual ECDSA + RSA certs - if [[ -n ${SSL_ALT_KEY_PATH} ]] && [[ -n ${SSL_ALT_CERT_PATH} ]] - then + if [[ -n ${SSL_ALT_KEY_PATH} ]] && [[ -n ${SSL_ALT_CERT_PATH} ]]; then _log 'trace' "Configuring fallback certificates using key ${SSL_ALT_KEY_PATH} and cert ${SSL_ALT_CERT_PATH}" _set_alt_certificate "${SSL_ALT_KEY_PATH}" "${SSL_ALT_CERT_PATH}" @@ -325,7 +309,15 @@ function _setup_ssl _log 'trace' "SSL configured with 'self-signed' certificates" else - _dms_panic__no_file "${SS_KEY} or ${SS_CERT} or ${SS_CA_CERT}" "${SCOPE_SSL_TYPE}" + local MISSING_FILES=() + [[ ! -f ${SS_KEY} ]] && MISSING_FILES+=("${SS_KEY}") + [[ ! -f ${SS_CERT} ]] && MISSING_FILES+=("${SS_CERT}") + [[ ! -f ${SS_CA_CERT} ]] && MISSING_FILES+=("${SS_CA_CERT}") + + # Concatenate each element and delimit with ` + `: + local ERROR_CONTEXT + ERROR_CONTEXT=$(printf "'%s' + " "${MISSING_FILES[@]}" | sed 's/ + $//') + _dms_panic__no_file "${ERROR_CONTEXT}" "${SCOPE_SSL_TYPE}" fi ;; @@ -342,7 +334,7 @@ function _setup_ssl # | http://www.postfix.org/postconf.5.html#smtpd_tls_auth_only | http://www.postfix.org/TLS_README.html#server_tls_auth # # smtp_tls_wrappermode (default: not applied, 'no') | http://www.postfix.org/postconf.5.html#smtp_tls_wrappermode - # smtpd_tls_wrappermode (default: 'yes' for service port 'smtps') | http://www.postfix.org/postconf.5.html#smtpd_tls_wrappermode + # smtpd_tls_wrappermode (default: 'yes' for service port 'submissions') | http://www.postfix.org/postconf.5.html#smtpd_tls_wrappermode # NOTE: Enabling wrappermode requires a security_level of 'encrypt' or stronger. Port 465 presently does not meet this condition. # # Postfix main.cf (base config): @@ -353,7 +345,7 @@ function _setup_ssl # # Postfix master.cf (per connection overrides): # Disables implicit TLS on port 465 for inbound (smtpd) and outbound (smtp) traffic. Treats it as equivalent to port 25 SMTP with explicit STARTTLS. - # Inbound 465 (aka service port aliases: submissions / smtps) for Postfix to receive over implicit TLS (eg from MUA or functioning as a relay host). + # Inbound 465 (aka service port aliases: submissions) for Postfix to receive over implicit TLS (eg from MUA or functioning as a relay host). # Outbound 465 as alternative to port 587 when sending to another MTA (with authentication), such as a relay service (eg SendGrid). sedfile -i -r \ -e "/smtpd?_tls_security_level/s|=.*|=none|" \ @@ -389,18 +381,14 @@ function _setup_ssl # Identify a valid letsencrypt FQDN folder to use. -function _find_letsencrypt_domain -{ +function _find_letsencrypt_domain() { local LETSENCRYPT_DOMAIN - if [[ -n ${SSL_DOMAIN} ]] && [[ -e /etc/letsencrypt/live/$(_strip_wildcard_prefix "${SSL_DOMAIN}")/fullchain.pem ]] - then + if [[ -n ${SSL_DOMAIN} ]] && [[ -e /etc/letsencrypt/live/$(_strip_wildcard_prefix "${SSL_DOMAIN}")/fullchain.pem ]]; then LETSENCRYPT_DOMAIN=$(_strip_wildcard_prefix "${SSL_DOMAIN}") - elif [[ -e /etc/letsencrypt/live/${HOSTNAME}/fullchain.pem ]] - then + elif [[ -e /etc/letsencrypt/live/${HOSTNAME}/fullchain.pem ]]; then LETSENCRYPT_DOMAIN=${HOSTNAME} - elif [[ -e /etc/letsencrypt/live/${DOMAINNAME}/fullchain.pem ]] - then + elif [[ -e /etc/letsencrypt/live/${DOMAINNAME}/fullchain.pem ]]; then LETSENCRYPT_DOMAIN=${DOMAINNAME} else _log 'error' "Cannot find a valid DOMAIN for '/etc/letsencrypt/live//', tried: '${SSL_DOMAIN}', '${HOSTNAME}', '${DOMAINNAME}'" @@ -411,21 +399,17 @@ function _find_letsencrypt_domain } # Verify the FQDN folder also includes a valid private key (`privkey.pem` for Certbot, `key.pem` for extraction by Traefik) -function _find_letsencrypt_key -{ +function _find_letsencrypt_key() { local LETSENCRYPT_KEY local LETSENCRYPT_DOMAIN=${1} - if [[ -z ${LETSENCRYPT_DOMAIN} ]] - then + if [[ -z ${LETSENCRYPT_DOMAIN} ]]; then _dms_panic__misconfigured 'LETSENCRYPT_DOMAIN' '_find_letsencrypt_key' fi - if [[ -e /etc/letsencrypt/live/${LETSENCRYPT_DOMAIN}/privkey.pem ]] - then + if [[ -e /etc/letsencrypt/live/${LETSENCRYPT_DOMAIN}/privkey.pem ]]; then LETSENCRYPT_KEY='privkey' - elif [[ -e /etc/letsencrypt/live/${LETSENCRYPT_DOMAIN}/key.pem ]] - then + elif [[ -e /etc/letsencrypt/live/${LETSENCRYPT_DOMAIN}/key.pem ]]; then LETSENCRYPT_KEY='key' else _log 'error' "Cannot find key file ('privkey.pem' or 'key.pem') in '/etc/letsencrypt/live/${LETSENCRYPT_DOMAIN}/'" @@ -435,11 +419,9 @@ function _find_letsencrypt_key echo "${LETSENCRYPT_KEY}" } -function _extract_certs_from_acme -{ +function _extract_certs_from_acme() { local CERT_DOMAIN=${1} - if [[ -z ${CERT_DOMAIN} ]] - then + if [[ -z ${CERT_DOMAIN} ]]; then _log 'warn' "_extract_certs_from_acme | CERT_DOMAIN is empty" return 1 fi @@ -448,16 +430,14 @@ function _extract_certs_from_acme KEY=$(acme_extract.py /etc/letsencrypt/acme.json "${CERT_DOMAIN}" --key) CERT=$(acme_extract.py /etc/letsencrypt/acme.json "${CERT_DOMAIN}" --cert) - if [[ -z ${KEY} ]] || [[ -z ${CERT} ]] - then + if [[ -z ${KEY} ]] || [[ -z ${CERT} ]]; then _log 'warn' "_extract_certs_from_acme | Unable to find key and/or cert for '${CERT_DOMAIN}' in '/etc/letsencrypt/acme.json'" return 1 fi # Currently we advise SSL_DOMAIN for wildcard support using a `*.example.com` value, # The filepath however should be `example.com`, avoiding the wildcard part: - if [[ ${SSL_DOMAIN} == "${CERT_DOMAIN}" ]] - then + if [[ ${SSL_DOMAIN} == "${CERT_DOMAIN}" ]]; then CERT_DOMAIN=$(_strip_wildcard_prefix "${SSL_DOMAIN}") fi diff --git a/target/scripts/helpers/utils.sh b/target/scripts/helpers/utils.sh index bd4d8e3c984..d11c152284e 100644 --- a/target/scripts/helpers/utils.sh +++ b/target/scripts/helpers/utils.sh @@ -1,22 +1,69 @@ #!/bin/bash -function _escape -{ +function _escape() { echo "${1//./\\.}" } +# TODO: Not in use currently. Maybe in the future: https://github.com/docker-mailserver/docker-mailserver/pull/3484/files#r1299410851 +# Replaces a string so that it can be used inside +# `sed` safely. +# +# @param ${1} = string to escape +# @output = prints the escaped string +function _escape_for_sed() { + sed -E 's/[]\/$*.^[]/\\&/g' <<< "${1:?String to escape for sed is required}" +} + # Returns input after filtering out lines that are: # empty, white-space, comments (`#` as the first non-whitespace character) -function _get_valid_lines_from_file -{ +function _get_valid_lines_from_file() { + _convert_crlf_to_lf_if_necessary "${1}" + _append_final_newline_if_missing "${1}" + grep --extended-regexp --invert-match "^\s*$|^\s*#" "${1}" || true } +# This is to sanitize configs from users that unknowingly introduced CRLF: +function _convert_crlf_to_lf_if_necessary() { + if [[ $(file "${1}") =~ 'CRLF' ]]; then + _log 'warn' "File '${1}' contains CRLF line-endings" + + if [[ -w ${1} ]]; then + _log 'debug' 'Converting CRLF to LF' + sed -i 's|\r||g' "${1}" + else + _log 'warn' "File '${1}' is not writable - cannot change CRLF to LF" + fi + fi +} + +# This is to sanitize configs from users that unknowingly removed the end-of-file LF: +function _append_final_newline_if_missing() { + # Correctly detect a missing final newline and fix it: + # https://stackoverflow.com/questions/38746/how-to-detect-file-ends-in-newline#comment82380232_25749716 + # https://unix.stackexchange.com/questions/31947/how-to-add-a-newline-to-the-end-of-a-file/441200#441200 + # https://unix.stackexchange.com/questions/159557/how-to-non-invasively-test-for-write-access-to-a-file + if [[ $(tail -c1 "${1}" | wc -l) -eq 0 ]]; then + # Avoid fixing when the destination is read-only: + if [[ -w ${1} ]]; then + printf '\n' >> "${1}" + + _log 'info' "File '${1}' was missing a final newline - this has been fixed" + else + _log 'warn' "File '${1}' is missing a final newline - it is not writable, hence it was not fixed - the last line will not be processed!" + fi + fi +} + # Provide the name of an environment variable to this function # and it will return its value stored in /etc/dms-settings -function _get_dms_env_value -{ - grep "^${1}=" /etc/dms-settings | cut -d "'" -f 2 +function _get_dms_env_value() { + if [[ -f /etc/dms-settings ]]; then + grep "^${1}=" /etc/dms-settings | cut -d "'" -f 2 + else + _log 'warn' "Call to '_get_dms_env_value' but '/etc/dms-settings' is not present" + return 1 + fi } # TODO: `chown -R 5000:5000 /var/mail` has existed since the projects first commit. @@ -25,24 +72,22 @@ function _get_dms_env_value # # `helpers/accounts.sh:_create_accounts` (mkdir, cp) appears to be the only writer to # /var/mail folders (used during startup and change detection handling). -function _chown_var_mail_if_necessary -{ +function _chown_var_mail_if_necessary() { # fix permissions, but skip this if 3 levels deep the user id is already set - if find /var/mail -maxdepth 3 -a \( \! -user 5000 -o \! -group 5000 \) | read -r - then + if find /var/mail -maxdepth 3 -a \( \! -user "${DMS_VMAIL_UID}" -o \! -group "${DMS_VMAIL_GID}" \) | read -r; then _log 'trace' 'Fixing /var/mail permissions' - chown -R 5000:5000 /var/mail || return 1 + chown -R "${DMS_VMAIL_UID}:${DMS_VMAIL_GID}" /var/mail || return 1 fi } -function _require_n_parameters_or_print_usage -{ +function _require_n_parameters_or_print_usage() { local COUNT COUNT=${1} shift [[ ${1:-} == 'help' ]] && { __usage ; exit 0 ; } [[ ${#} -lt ${COUNT} ]] && { __usage ; exit 1 ; } + return 0 } # NOTE: Postfix commands that read `main.cf` will stall execution, @@ -50,16 +95,13 @@ function _require_n_parameters_or_print_usage # After we modify the config explicitly, we can safely assume (reasonably) # that the write stream has completed, and it is safe to read the config. # https://github.com/docker-mailserver/docker-mailserver/issues/2985 -function _adjust_mtime_for_postfix_maincf -{ - if [[ $(( $(date '+%s') - $(stat -c '%Y' '/etc/postfix/main.cf') )) -lt 2 ]] - then +function _adjust_mtime_for_postfix_maincf() { + if [[ $(( $(date '+%s') - $(stat -c '%Y' '/etc/postfix/main.cf') )) -lt 2 ]]; then touch -d '2 seconds ago' /etc/postfix/main.cf fi } -function _reload_postfix -{ +function _reload_postfix() { _adjust_mtime_for_postfix_maincf postfix reload } @@ -80,7 +122,7 @@ function _reload_postfix # you can set the environment variable `POSTFIX_README_DIRECTORY='/new/dir/'` # (`POSTFIX_` is an arbitrary prefix, you can choose the one you like), # and then call this function: -# `_replace_by_env_in_file 'POSTFIX_' 'PATH TO POSTFIX's main.cf>` +# `_replace_by_env_in_file 'POSTFIX_' '` # # ## Panics # @@ -88,24 +130,19 @@ function _reload_postfix # # 1. No first and second argument is supplied # 2. The second argument is a path to a file that does not exist -function _replace_by_env_in_file -{ - if [[ -z ${1+set} ]] - then - _dms_panic__invalid_value 'first argument unset' 'utils.sh:_replace_by_env_in_file' 'immediate' - elif [[ -z ${2+set} ]] - then - _dms_panic__invalid_value 'second argument unset' 'utils.sh:_replace_by_env_in_file' 'immediate' - elif [[ ! -f ${2} ]] - then - _dms_panic__invalid_value "file '${2}' does not exist" 'utils.sh:_replace_by_env_in_file' 'immediate' +function _replace_by_env_in_file() { + if [[ -z ${1:-} ]]; then + _dms_panic__invalid_value 'first argument unset' 'utils.sh:_replace_by_env_in_file' + elif [[ -z ${2:-} ]]; then + _dms_panic__invalid_value 'second argument unset' 'utils.sh:_replace_by_env_in_file' + elif [[ ! -f ${2} ]]; then + _dms_panic__invalid_value "file '${2}' does not exist" 'utils.sh:_replace_by_env_in_file' fi local ENV_PREFIX=${1} CONFIG_FILE=${2} local ESCAPED_VALUE ESCAPED_KEY - while IFS='=' read -r KEY VALUE - do + while IFS='=' read -r KEY VALUE; do KEY=${KEY#"${ENV_PREFIX}"} # strip prefix ESCAPED_KEY=$(sed -E 's#([\=\&\|\$\.\*\/\[\\^]|\])#\\\1#g' <<< "${KEY,,}") ESCAPED_VALUE=$(sed -E 's#([\=\&\|\$\.\*\/\[\\^]|\])#\\\1#g' <<< "${VALUE}") @@ -114,3 +151,45 @@ function _replace_by_env_in_file sed -i -E "s#^${ESCAPED_KEY}[[:space:]]*=.*#${ESCAPED_KEY} =${ESCAPED_VALUE}#g" "${CONFIG_FILE}" done < <(env | grep "^${ENV_PREFIX}") } + +# Check if an environment variable's value is zero or one. This aids in checking variables +# that act as "booleans" for enabling or disabling a service, configuration option, etc. +# +# This function will log a warning and return with exit code 1 in case the variable's value +# is not zero or one. +# +# @param ${1} = name of the ENV variable to check +function _env_var_expect_zero_or_one() { + local ENV_VAR_NAME=${1:?ENV var name must be provided to _env_var_expect_zero_or_one} + + if [[ ! -v ${ENV_VAR_NAME} ]]; then + _log 'warn' "'${ENV_VAR_NAME}' is not set, but was expected to be" + return 1 + fi + + if [[ ! ${!ENV_VAR_NAME} =~ ^(0|1)$ ]]; then + _log 'warn' "The value of '${ENV_VAR_NAME}' (= '${!ENV_VAR_NAME}') is not 0 or 1, but was expected to be" + return 1 + fi + + return 0 +} + +# Check if an environment variable's value is an integer. +# +# This function will log a warning and return with exit code 1 in case the variable's value +# is not an integer. +# +# @param ${1} = name of the ENV variable to check +function _env_var_expect_integer() { + local ENV_VAR_NAME=${1:?ENV var name must be provided to _env_var_expect_integer} + + [[ ${!ENV_VAR_NAME} =~ ^-?[0-9][0-9]*$ ]] && return 0 + _log 'warn' "The value of '${ENV_VAR_NAME}' is not an integer ('${!ENV_VAR_NAME}'), but was expected to be" + return 1 +} + +function _reload_rspamd() { + _log 'debug' "Reloading configuration for Rspamd via sending 'SIGHUP'" + supervisorctl signal SIGHUP rspamd +} diff --git a/target/scripts/start-mailserver.sh b/target/scripts/start-mailserver.sh index c98639b8e8c..6fd65ade005 100755 --- a/target/scripts/start-mailserver.sh +++ b/target/scripts/start-mailserver.sh @@ -1,5 +1,10 @@ #!/bin/bash +# When 'pipefail' is enabled, the exit status of the pipeline reflects the exit status of the last command that fails. +# Without 'pipefail', the exit status of a pipeline is determined by the exit status of the last command in the pipeline. +set -o pipefail + +# Allows the usage of '**' in patterns, e.g. ls **/* shopt -s globstar # ------------------------------------------------------------ @@ -27,26 +32,27 @@ source /usr/local/bin/daemons-stack.sh # ? >> Registering functions # ------------------------------------------------------------ -function _register_functions -{ +function _register_functions() { _log 'debug' 'Registering functions' # ? >> Checks - _register_check_function '_check_improper_restart' _register_check_function '_check_hostname' - _register_check_function '_check_log_level' + _register_check_function '_check_spam_prefix' # ? >> Setup - _register_setup_function '_setup_logs_general' + _register_setup_function '_setup_vmail_id' _register_setup_function '_setup_timezone' - if [[ ${SMTP_ONLY} -ne 1 ]] - then + if [[ ${SMTP_ONLY} -ne 1 ]]; then _register_setup_function '_setup_dovecot' + _register_setup_function '_setup_dovecot_sieve' _register_setup_function '_setup_dovecot_dhparam' _register_setup_function '_setup_dovecot_quota' + _register_setup_function '_setup_spam_subject' + _register_setup_function '_setup_spam_to_junk' + _register_setup_function '_setup_spam_mark_as_read' fi case "${ACCOUNT_PROVISIONER}" in @@ -55,30 +61,26 @@ function _register_functions ;; ( 'LDAP' ) - _environment_variables_ldap _register_setup_function '_setup_ldap' ;; ( 'OIDC' ) - _dms_panic__fail_init 'OIDC user account provisioning - it is not yet implemented' '' 'immediate' + _dms_panic__fail_init 'OIDC user account provisioning - it is not yet implemented' ;; ( * ) - _dms_panic__invalid_value "'${ACCOUNT_PROVISIONER}' is not a valid value for ACCOUNT_PROVISIONER" '' 'immediate' + _dms_panic__invalid_value "'${ACCOUNT_PROVISIONER}' is not a valid value for ACCOUNT_PROVISIONER" ;; esac - if [[ ${ENABLE_SASLAUTHD} -eq 1 ]] - then - _environment_variables_saslauthd - _register_setup_function '_setup_saslauthd' - fi + [[ ${ENABLE_OAUTH2} -eq 1 ]] && _register_setup_function '_setup_oauth2' + [[ ${ENABLE_SASLAUTHD} -eq 1 ]] && _register_setup_function '_setup_saslauthd' - _register_setup_function '_setup_postfix_inet_protocols' _register_setup_function '_setup_dovecot_inet_protocols' _register_setup_function '_setup_opendkim' _register_setup_function '_setup_opendmarc' # must come after `_setup_opendkim` + _register_setup_function '_setup_policyd_spf' _register_setup_function '_setup_security_stack' _register_setup_function '_setup_rspamd' @@ -86,50 +88,58 @@ function _register_functions _register_setup_function '_setup_ssl' _register_setup_function '_setup_docker_permit' _register_setup_function '_setup_mailname' - _register_setup_function '_setup_dovecot_hostname' - - _register_setup_function '_setup_postfix_hostname' - _register_setup_function '_setup_postfix_smtputf8' - _register_setup_function '_setup_postfix_sasl' - _register_setup_function '_setup_postfix_aliases' - _register_setup_function '_setup_postfix_vhost' - _register_setup_function '_setup_postfix_dhparam' - _register_setup_function '_setup_postfix_sizelimits' - _register_setup_function '_setup_fetchmail' - _register_setup_function '_setup_fetchmail_parallel' - # needs to come after _setup_postfix_aliases + _register_setup_function '_setup_postfix_early' + + # Dependent upon _setup_postfix_early first calling _create_aliases + # Due to conditional check for /etc/postfix/regexp _register_setup_function '_setup_spoof_protection' - if [[ ${ENABLE_SRS} -eq 1 ]] - then + _register_setup_function '_setup_postfix_late' + + if [[ ${ENABLE_SRS} -eq 1 ]]; then _register_setup_function '_setup_SRS' _register_start_daemon '_start_daemon_postsrsd' fi - _register_setup_function '_setup_postfix_access_control' - _register_setup_function '_setup_postfix_relay_hosts' - _register_setup_function '_setup_postfix_virtual_transport' - _register_setup_function '_setup_postfix_override_configuration' + _register_setup_function '_setup_fetchmail' + _register_setup_function '_setup_fetchmail_parallel' + _register_setup_function '_setup_getmail' + _register_setup_function '_setup_logrotate' _register_setup_function '_setup_mail_summary' _register_setup_function '_setup_logwatch' _register_setup_function '_setup_save_states' - _register_setup_function '_setup_apply_fixes_after_configuration' - _register_setup_function '_environment_variables_export' + _register_setup_function '_setup_adjust_state_permissions' + + if [[ ${ENABLE_MTA_STS} -eq 1 ]]; then + _register_setup_function '_setup_mta_sts' + _register_start_daemon '_start_daemon_mta_sts_daemon' + fi + + # ! The following functions must be executed after all other setup functions + _register_setup_function '_setup_directory_and_file_permissions' + _register_setup_function '_setup_run_user_patches' # ? >> Daemons _register_start_daemon '_start_daemon_cron' _register_start_daemon '_start_daemon_rsyslog' - [[ ${SMTP_ONLY} -ne 1 ]] && _register_start_daemon '_start_daemon_dovecot' + [[ ${SMTP_ONLY} -ne 1 ]] && _register_start_daemon '_start_daemon_dovecot' - [[ ${ENABLE_UPDATE_CHECK} -eq 1 ]] && _register_start_daemon '_start_daemon_update_check' - [[ ${ENABLE_RSPAMD} -eq 1 ]] && _register_start_daemon '_start_daemon_rspamd' + if [[ ${ENABLE_UPDATE_CHECK} -eq 1 ]]; then + if [[ ${DMS_RELEASE} != 'edge' ]]; then + _register_start_daemon '_start_daemon_update_check' + else + _log 'warn' "ENABLE_UPDATE_CHECK=1 is configured, but image is not a stable release. Update-Check is disabled." + fi + fi + + # The order here matters: Since Rspamd is using Redis, Redis should be started before Rspamd. [[ ${ENABLE_RSPAMD_REDIS} -eq 1 ]] && _register_start_daemon '_start_daemon_rspamd_redis' - [[ ${ENABLE_UPDATE_CHECK} -eq 1 ]] && _register_start_daemon '_start_daemon_update_check' + [[ ${ENABLE_RSPAMD} -eq 1 ]] && _register_start_daemon '_start_daemon_rspamd' # needs to be started before SASLauthd [[ ${ENABLE_OPENDKIM} -eq 1 ]] && _register_start_daemon '_start_daemon_opendkim' @@ -146,7 +156,8 @@ function _register_functions [[ ${ENABLE_FETCHMAIL} -eq 1 ]] && _register_start_daemon '_start_daemon_fetchmail' [[ ${ENABLE_CLAMAV} -eq 1 ]] && _register_start_daemon '_start_daemon_clamav' [[ ${ENABLE_AMAVIS} -eq 1 ]] && _register_start_daemon '_start_daemon_amavis' - [[ ${ACCOUNT_PROVISIONER} == 'FILE' ]] && _register_start_daemon '_start_daemon_changedetector' + [[ ${ENABLE_GETMAIL} -eq 1 ]] && _register_start_daemon '_start_daemon_getmail' + _register_start_daemon '_start_daemon_changedetector' } # ------------------------------------------------------------ @@ -158,21 +169,39 @@ function _register_functions _early_supervisor_setup _early_variables_setup -_log 'info' "Welcome to docker-mailserver $(/CONTAINER_START -_log 'info' "${HOSTNAME} is up and running" +# Container logs will receive updates from this log file: +MAIN_LOGFILE=/var/log/mail/mail.log +# NOTE: rsyslogd would usually create this later during `_start_daemons`, however it would already exist if the container was restarted. +touch "${MAIN_LOGFILE}" +# Ensure `tail` follows the correct position of the log file for this container start (new logs begin once `_start_daemons` is called) +TAIL_START=$(( $(wc -l < "${MAIN_LOGFILE}") + 1 )) -touch /var/log/mail/mail.log -tail -Fn 0 /var/log/mail/mail.log +[[ ${LOG_LEVEL} =~ (debug|trace) ]] && print-environment +_start_daemons -exit 0 +# Container start-up scripts completed. `tail` will now pipe the log updates to stdout: +_log 'info' "${HOSTNAME} is up and running" +exec tail -Fn "+${TAIL_START}" "${MAIN_LOGFILE}" diff --git a/target/scripts/startup/check-stack.sh b/target/scripts/startup/check-stack.sh index 8fb7073fc4d..52d83ad49c8 100644 --- a/target/scripts/startup/check-stack.sh +++ b/target/scripts/startup/check-stack.sh @@ -2,61 +2,34 @@ declare -a FUNCS_CHECK -function _register_check_function -{ +function _register_check_function() { FUNCS_CHECK+=("${1}") _log 'trace' "${1}() registered" } -function _check -{ +function _check() { _log 'info' 'Checking configuration' - for FUNC in "${FUNCS_CHECK[@]}" - do + for FUNC in "${FUNCS_CHECK[@]}"; do ${FUNC} done } -function _check_improper_restart -{ - _log 'debug' 'Checking for improper restart' - - if [[ -f /CONTAINER_START ]] - then - _log 'warn' 'This container was (likely) improperly restarted which can result in undefined behavior' - _log 'warn' 'Please destroy the container properly and then start DMS again' - fi -} - -function _check_hostname -{ +function _check_hostname() { _log 'debug' 'Checking that hostname/domainname is provided or overridden' _log 'debug' "Domain has been set to ${DOMAINNAME}" _log 'debug' "Hostname has been set to ${HOSTNAME}" # HOSTNAME should be an FQDN (eg: hostname.domain) - if ! grep -q -E '^(\S+[.]\S+)$' <<< "${HOSTNAME}" - then - _dms_panic__general 'Setting hostname/domainname is required' '' 'immediate' + if ! grep -q -E '^(\S+[.]\S+)$' <<< "${HOSTNAME}"; then + _dms_panic__general 'Setting hostname/domainname is required' fi } -function _check_log_level -{ - if [[ ${LOG_LEVEL} == 'trace' ]] \ - || [[ ${LOG_LEVEL} == 'debug' ]] \ - || [[ ${LOG_LEVEL} == 'info' ]] \ - || [[ ${LOG_LEVEL} == 'warn' ]] \ - || [[ ${LOG_LEVEL} == 'error' ]] - then - return 0 - else - local DEFAULT_LOG_LEVEL='info' - _log 'warn' "Log level '${LOG_LEVEL}' is invalid (falling back to default '${DEFAULT_LOG_LEVEL}')" - - # shellcheck disable=SC2034 - VARS[LOG_LEVEL]="${DEFAULT_LOG_LEVEL}" - LOG_LEVEL="${DEFAULT_LOG_LEVEL}" +function _check_spam_prefix() { + # This check should be independent of ENABLE_POP3 and ENABLE_IMAP + if [[ ${MOVE_SPAM_TO_JUNK} -eq 0 ]] \ + && [[ -z ${SPAM_SUBJECT} ]]; then + _log 'warn' "'MOVE_SPAM_TO_JUNK=0' and 'SPAM_SUBJECT' is empty - make sure this is intended: spam e-mails might not be immediately recognizable in this configuration" fi } diff --git a/target/scripts/startup/daemons-stack.sh b/target/scripts/startup/daemons-stack.sh index ac2a85c2d63..4cc9b2af067 100644 --- a/target/scripts/startup/daemons-stack.sh +++ b/target/scripts/startup/daemons-stack.sh @@ -2,34 +2,29 @@ declare -a DAEMONS_START -function _register_start_daemon -{ +function _register_start_daemon() { DAEMONS_START+=("${1}") _log 'trace' "${1}() registered" } -function _start_daemons -{ +function _start_daemons() { _log 'info' 'Starting daemons' - for FUNCTION in "${DAEMONS_START[@]}" - do + for FUNCTION in "${DAEMONS_START[@]}"; do ${FUNCTION} done } -function _default_start_daemon -{ +function _default_start_daemon() { _log 'debug' "Starting ${1:?}" local RESULT RESULT=$(supervisorctl start "${1}" 2>&1) # shellcheck disable=SC2181 - if [[ ${?} -ne 0 ]] - then + if [[ ${?} -ne 0 ]]; then _log 'error' "${RESULT}" - _dms_panic__fail_init "${1}" '' 'immediate' + _dms_panic__fail_init "${1}" fi } @@ -39,33 +34,30 @@ function _start_daemon_clamav { _default_start_daemon 'clamav' ; function _start_daemon_cron { _default_start_daemon 'cron' ; } function _start_daemon_dovecot { _default_start_daemon 'dovecot' ; } function _start_daemon_fail2ban { _default_start_daemon 'fail2ban' ; } +function _start_daemon_getmail { _default_start_daemon 'getmail' ; } function _start_daemon_opendkim { _default_start_daemon 'opendkim' ; } function _start_daemon_opendmarc { _default_start_daemon 'opendmarc' ; } function _start_daemon_postgrey { _default_start_daemon 'postgrey' ; } function _start_daemon_postsrsd { _default_start_daemon 'postsrsd' ; } +function _start_daemon_mta_sts_daemon { _default_start_daemon 'mta-sts-daemon' ; } function _start_daemon_rspamd { _default_start_daemon 'rspamd' ; } function _start_daemon_rspamd_redis { _default_start_daemon 'rspamd-redis' ; } function _start_daemon_rsyslog { _default_start_daemon 'rsyslog' ; } function _start_daemon_update_check { _default_start_daemon 'update-check' ; } -function _start_daemon_saslauthd -{ +function _start_daemon_saslauthd() { _default_start_daemon "saslauthd_${SASLAUTHD_MECHANISMS}" } -function _start_daemon_postfix -{ +function _start_daemon_postfix() { _adjust_mtime_for_postfix_maincf _default_start_daemon 'postfix' } -function _start_daemon_fetchmail -{ - if [[ ${FETCHMAIL_PARALLEL} -eq 1 ]] - then +function _start_daemon_fetchmail() { + if [[ ${FETCHMAIL_PARALLEL} -eq 1 ]]; then local COUNTER=0 - for _ in /etc/fetchmailrc.d/fetchmail-*.rc - do + for _ in /etc/fetchmailrc.d/fetchmail-*.rc; do COUNTER=$(( COUNTER + 1 )) _default_start_daemon "fetchmail-${COUNTER}" done diff --git a/target/scripts/startup/setup-stack.sh b/target/scripts/startup/setup-stack.sh index 0cd357277c3..a75770c533b 100644 --- a/target/scripts/startup/setup-stack.sh +++ b/target/scripts/startup/setup-stack.sh @@ -2,41 +2,45 @@ declare -a FUNCS_SETUP -function _register_setup_function -{ +function _register_setup_function() { FUNCS_SETUP+=("${1}") _log 'trace' "${1}() registered" } -function _setup -{ +function _setup() { # Requires `shopt -s globstar` because of `**` which in - # turn is required as we're decending through directories - for FILE in /usr/local/bin/setup.d/**/*.sh - do + # turn is required as we're descending through directories + for FILE in /usr/local/bin/setup.d/**/*.sh; do # shellcheck source=/dev/null source "${FILE}" done _log 'info' 'Configuring mail server' - for FUNC in "${FUNCS_SETUP[@]}" - do + for FUNC in "${FUNCS_SETUP[@]}"; do ${FUNC} done + _setup_post +} + +function _setup_post() { + # Dovecot `.svbin` files must have a newer mtime than their `.sieve` source files, + # Modifications during setup to these files sometimes results in a common mtime value. + # Handled during post-setup as setup of Dovecot Sieve scripts is not centralized. + find /usr/lib/dovecot/ -iname '*.sieve' -exec touch -d '2 seconds ago' {} + + find /usr/lib/dovecot/ -iname '*.svbin' -exec touch -d '1 seconds ago' {} + + # All startup modifications to configs should have taken place before calling this: _prepare_for_change_detection } -function _early_supervisor_setup -{ +function _early_supervisor_setup() { SUPERVISOR_LOGLEVEL="${SUPERVISOR_LOGLEVEL:-warn}" - if ! grep -q "loglevel = ${SUPERVISOR_LOGLEVEL}" /etc/supervisor/supervisord.conf - then + if ! grep -q "loglevel = ${SUPERVISOR_LOGLEVEL}" /etc/supervisor/supervisord.conf; then case "${SUPERVISOR_LOGLEVEL}" in ( 'critical' | 'error' | 'info' | 'debug' ) - sed -i -E \ + sedfile -i -E \ "s|(loglevel).*|\1 = ${SUPERVISOR_LOGLEVEL}|g" \ /etc/supervisor/supervisord.conf @@ -57,15 +61,13 @@ function _early_supervisor_setup return 0 } -function _setup_timezone -{ +function _setup_timezone() { [[ -n ${TZ} ]] || return 0 _log 'debug' "Setting timezone to '${TZ}'" local ZONEINFO_FILE="/usr/share/zoneinfo/${TZ}" - if [[ ! -e ${ZONEINFO_FILE} ]] - then + if [[ ! -e ${ZONEINFO_FILE} ]]; then _log 'warn' "Cannot find timezone '${TZ}'" return 1 fi @@ -80,31 +82,73 @@ function _setup_timezone fi } -function _setup_apply_fixes_after_configuration -{ +# Misc checks and fixes migrated here until next refactor: +# NOTE: `start-mailserver.sh` runs this along with `mail-state.sh` during container restarts +function _setup_directory_and_file_permissions() { _log 'trace' 'Removing leftover PID files from a stop/start' find /var/run/ -not -name 'supervisord.pid' -name '*.pid' -delete touch /dev/shm/supervisor.sock _log 'debug' 'Checking /var/mail permissions' - if ! _chown_var_mail_if_necessary - then - _dms_panic__general 'Failed to fix /var/mail permissions' '' 'immediate' + if ! _chown_var_mail_if_necessary; then + _dms_panic__general 'Failed to fix /var/mail permissions' fi _log 'debug' 'Removing files and directories from older versions' rm -rf /var/mail-state/spool-postfix/{dev,etc,lib,pid,usr,private/auth} + + _rspamd_get_envs + # /tmp/docker-mailserver/rspamd/dkim + if [[ -d ${RSPAMD_DMS_DKIM_D} ]]; then + _log 'debug' "Ensuring '${RSPAMD_DMS_DKIM_D}' is owned by '_rspamd:_rspamd'" + chown -R _rspamd:_rspamd "${RSPAMD_DMS_DKIM_D}" + fi + + # Parent directories must have the executable bit set to descend the file tree for access, + # as each service in the container running as a non-root user requires this to access any subpath, + # `/tmp/docker-mailserver` must allow all users `+x` (notably required for `_rspamd` user read access): + local DMS_CONFIG_DIR=/tmp/docker-mailserver + chmod +x "${DMS_CONFIG_DIR}" + + __log_fixes } -function _run_user_patches -{ +function _setup_run_user_patches() { local USER_PATCHES='/tmp/docker-mailserver/user-patches.sh' - if [[ -f ${USER_PATCHES} ]] - then + if [[ -f ${USER_PATCHES} ]]; then _log 'debug' 'Applying user patches' /bin/bash "${USER_PATCHES}" else _log 'trace' "No optional '${USER_PATCHES}' provided" fi } + +function __log_fixes() { + _log 'debug' 'Ensuring /var/log/mail ownership + permissions are correct' + + # File/folder permissions are fine when using docker volumes, but may be wrong + # when file system folders are mounted into the container. + # Set the expected values and create missing folders/files just in case. + mkdir -p /var/log/{mail,supervisor} + + # TODO: Remove these lines in a future release once concerns are resolved: + # https://github.com/docker-mailserver/docker-mailserver/pull/4370#issuecomment-2661762043 + chown syslog:root /var/log/mail + + if [[ ${ENABLE_CLAMAV} -eq 1 ]]; then + # TODO: Consider assigning /var/log/mail a writable non-root group for other processes like ClamAV? + # - Check if ClamAV is capable of creating files itself when they're missing? + # - Alternatively a symlink to /var/log/mail from the original intended location would allow write access + # as a user to the symlink location, while keeping ownership as root at /var/log/mail + # - `LogSyslog false` for clamd.conf + freshclam.conf could possibly be enabled instead of log files? + # However without better filtering in place (once Vector is adopted), this should be avoided. + touch /var/log/mail/{clamav,freshclam}.log + chown clamav:adm /var/log/mail/{clamav,freshclam}.log + fi + + # Volume permissions should be corrected: + # https://github.com/docker-mailserver/docker-mailserver-helm/issues/137 + chmod 755 /var/log/mail/ + find /var/log/mail/ -type f -exec chmod 640 {} + +} diff --git a/target/scripts/startup/setup.d/dmarc_dkim_spf.sh b/target/scripts/startup/setup.d/dmarc_dkim_spf.sh index f541eeaa832..7b26f86b6ad 100644 --- a/target/scripts/startup/setup.d/dmarc_dkim_spf.sh +++ b/target/scripts/startup/setup.d/dmarc_dkim_spf.sh @@ -6,10 +6,8 @@ # # The OpenDKIM milter must come before the OpenDMARC milter in Postfix's # `smtpd_milters` milters options. -function _setup_opendkim -{ - if [[ ${ENABLE_OPENDKIM} -eq 1 ]] - then +function _setup_opendkim() { + if [[ ${ENABLE_OPENDKIM} -eq 1 ]]; then _log 'debug' 'Configuring DKIM' mkdir -p /etc/opendkim/keys/ @@ -18,16 +16,18 @@ function _setup_opendkim _log 'trace' "Adding OpenDKIM to Postfix's milters" postconf 'dkim_milter = inet:localhost:8891' # shellcheck disable=SC2016 - sed -i -E \ - -e 's|^(smtpd_milters =.*)|\1 \$dkim_milter|g' \ - -e 's|^(non_smtpd_milters =.*)|\1 \$dkim_milter|g' \ - /etc/postfix/main.cf + _add_to_or_update_postfix_main 'smtpd_milters' '$dkim_milter' + # shellcheck disable=SC2016 + _add_to_or_update_postfix_main 'non_smtpd_milters' '$dkim_milter' # check if any keys are available - if [[ -e /tmp/docker-mailserver/opendkim/KeyTable ]] - then + if [[ -e /tmp/docker-mailserver/opendkim/KeyTable ]]; then cp -a /tmp/docker-mailserver/opendkim/* /etc/opendkim/ - _log 'trace' "DKIM keys added for: $(find /etc/opendkim/keys/ -maxdepth 1 -type f -printf '%f ')" + + local DKIM_DOMAINS + DKIM_DOMAINS=$(find /etc/opendkim/keys/ -maxdepth 1 -type f -printf '%f ') + _log 'trace' "DKIM keys added for: ${DKIM_DOMAINS}" + chown -R opendkim:opendkim /etc/opendkim/ chmod -R 0700 /etc/opendkim/keys/ else @@ -35,8 +35,7 @@ function _setup_opendkim fi # setup nameservers parameter from /etc/resolv.conf if not defined - if ! grep -q '^Nameservers' /etc/opendkim.conf - then + if ! grep -q '^Nameservers' /etc/opendkim.conf; then local NAMESERVER_IPS NAMESERVER_IPS=$(grep '^nameserver' /etc/resolv.conf | awk -F " " '{print $2}' | paste -sd ',' -) echo "Nameservers ${NAMESERVER_IPS}" >>/etc/opendkim.conf @@ -51,16 +50,14 @@ function _setup_opendkim fi } -# Set up OpenDKIM +# Set up OpenDMARC # # ## Attention # # The OpenDMARC milter must come after the OpenDKIM milter in Postfix's # `smtpd_milters` milters options. -function _setup_opendmarc -{ - if [[ ${ENABLE_OPENDMARC} -eq 1 ]] - then +function _setup_opendmarc() { + if [[ ${ENABLE_OPENDMARC} -eq 1 ]]; then # TODO When disabling SPF is possible, add a check whether DKIM and SPF is disabled # for DMARC to work, you should have at least one enabled # (see RFC 7489 https://www.rfc-editor.org/rfc/rfc7489#page-24) @@ -70,7 +67,7 @@ function _setup_opendmarc postconf 'dmarc_milter = inet:localhost:8893' # Make sure to append the OpenDMARC milter _after_ the OpenDKIM milter! # shellcheck disable=SC2016 - sed -i -E 's|^(smtpd_milters =.*)|\1 \$dmarc_milter|g' /etc/postfix/main.cf + _add_to_or_update_postfix_main 'smtpd_milters' '$dmarc_milter' sed -i \ -e "s|^AuthservID.*$|AuthservID ${HOSTNAME}|g" \ @@ -84,3 +81,24 @@ function _setup_opendmarc _log 'debug' 'Disabling OpenDMARC' fi } + +# Configures the SPF check inside Postfix's configuration via policyd-spf. When +# using Rspamd, you will likely want to turn that off. +function _setup_policyd_spf() { + if [[ ${ENABLE_POLICYD_SPF} -eq 1 ]]; then + _log 'debug' 'Configuring policyd-spf' + cat >>/etc/postfix/master.cf </tmp/docker-mailserver/dovecot-quotas.cf fi # enable quota policy check in postfix - sed -i -E \ + sedfile -i -E \ "s|(reject_unknown_recipient_domain)|\1, check_policy_service inet:localhost:65265|g" \ /etc/postfix/main.cf fi } -function _setup_dovecot_local_user -{ +function _setup_dovecot_local_user() { [[ ${SMTP_ONLY} -eq 1 ]] && return 0 [[ ${ACCOUNT_PROVISIONER} == 'FILE' ]] || return 0 _log 'debug' 'Setting up Dovecot Local User' - if [[ ! -f /tmp/docker-mailserver/postfix-accounts.cf ]] - then + if [[ ! -f /tmp/docker-mailserver/postfix-accounts.cf ]]; then _log 'trace' "No mail accounts to create - '/tmp/docker-mailserver/postfix-accounts.cf' is missing" fi - function __wait_until_an_account_is_added_or_shutdown - { + function __wait_until_an_account_is_added_or_shutdown() { local SLEEP_PERIOD='10' - for (( COUNTER = 11 ; COUNTER >= 0 ; COUNTER-- )) - do - if [[ $(grep -cE '.+@.+\|' /tmp/docker-mailserver/postfix-accounts.cf 2>/dev/null || printf '%s' '0') -ge 1 ]] - then + for (( COUNTER = 11 ; COUNTER >= 0 ; COUNTER-- )); do + if [[ $(grep -cE '.+@.+\|' /tmp/docker-mailserver/postfix-accounts.cf 2>/dev/null || printf '%s' '0') -ge 1 ]]; then return 0 else _log 'warn' "You need at least one mail account to start Dovecot ($(( ( COUNTER + 1 ) * SLEEP_PERIOD ))s left for account creation before shutdown)" @@ -182,7 +210,7 @@ function _setup_dovecot_local_user fi done - _dms_panic__fail_init 'accounts provisioning because no accounts were provided - Dovecot could not be started' '' 'immediate' + _dms_panic__fail_init 'accounts provisioning because no accounts were provided - Dovecot could not be started' } __wait_until_an_account_is_added_or_shutdown @@ -190,35 +218,25 @@ function _setup_dovecot_local_user _create_accounts } -function _setup_dovecot_inet_protocols -{ +function _setup_dovecot_inet_protocols() { [[ ${DOVECOT_INET_PROTOCOLS} == 'all' ]] && return 0 _log 'trace' 'Setting up DOVECOT_INET_PROTOCOLS option' local PROTOCOL # https://dovecot.org/doc/dovecot-example.conf - if [[ ${DOVECOT_INET_PROTOCOLS} == "ipv4" ]] - then + if [[ ${DOVECOT_INET_PROTOCOLS} == "ipv4" ]]; then PROTOCOL='*' # IPv4 only - elif [[ ${DOVECOT_INET_PROTOCOLS} == "ipv6" ]] - then + elif [[ ${DOVECOT_INET_PROTOCOLS} == "ipv6" ]]; then PROTOCOL='[::]' # IPv6 only else # Unknown value, panic. - _dms_panic__invalid_value 'DOVECOT_INET_PROTOCOLS' "${DOVECOT_INET_PROTOCOLS}" 'immediate' + _dms_panic__invalid_value 'DOVECOT_INET_PROTOCOLS' "${DOVECOT_INET_PROTOCOLS}" fi sedfile -i "s|^#listen =.*|listen = ${PROTOCOL}|g" /etc/dovecot/dovecot.conf } -function _setup_dovecot_dhparam -{ +function _setup_dovecot_dhparam() { _setup_dhparam 'Dovecot' '/etc/dovecot/dh.pem' } - -function _setup_dovecot_hostname -{ - _log 'debug' 'Applying hostname to Dovecot' - sed -i "s|^#hostname =.*$|hostname = '${HOSTNAME}'|g" /etc/dovecot/conf.d/15-lda.conf -} diff --git a/target/scripts/startup/setup.d/fetchmail.sh b/target/scripts/startup/setup.d/fetchmail.sh index 97b4aa993f8..033571db11c 100644 --- a/target/scripts/startup/setup.d/fetchmail.sh +++ b/target/scripts/startup/setup.d/fetchmail.sh @@ -1,9 +1,13 @@ #!/bin/bash -function _setup_fetchmail -{ - if [[ ${ENABLE_FETCHMAIL} -eq 1 ]] - then +# Docs - Config: +# https://www.fetchmail.info/fetchmail-man.html#the-run-control-file +# Docs - CLI: +# https://www.fetchmail.info/fetchmail-man.html#general-operation +# https://www.fetchmail.info/fetchmail-man.html#daemon-mode + +function _setup_fetchmail() { + if [[ ${ENABLE_FETCHMAIL} -eq 1 ]]; then _log 'trace' 'Enabling and configuring Fetchmail' local CONFIGURATION FETCHMAILRC @@ -11,8 +15,8 @@ function _setup_fetchmail CONFIGURATION='/tmp/docker-mailserver/fetchmail.cf' FETCHMAILRC='/etc/fetchmailrc' - if [[ -f ${CONFIGURATION} ]] - then + # Create `/etc/fetchmailrc` with default global config, optionally appending user-provided config: + if [[ -f ${CONFIGURATION} ]]; then cat /etc/fetchmailrc_general "${CONFIGURATION}" >"${FETCHMAILRC}" else cat /etc/fetchmailrc_general >"${FETCHMAILRC}" @@ -25,59 +29,54 @@ function _setup_fetchmail fi } -function _setup_fetchmail_parallel -{ - if [[ ${FETCHMAIL_PARALLEL} -eq 1 ]] - then +# NOTE: This feature is only actually relevant for entries polling via IMAP (to support leveraging the "IMAP IDLE" extension): +# - With either the `--idle` CLI or `idle` config option present +# - With a constraint on one fetchmail instance per server polled (and only for a single mailbox folder to monitor from that poll entry) +# - Reference: https://otremba.net/wiki/Fetchmail_(Debian)#Immediate_Download_via_IMAP_IDLE +function _setup_fetchmail_parallel() { + if [[ ${FETCHMAIL_PARALLEL} -eq 1 ]]; then _log 'trace' 'Enabling and configuring Fetchmail parallel' mkdir /etc/fetchmailrc.d/ - # Split the content of /etc/fetchmailrc into - # smaller fetchmailrc files per server [poll] entries. Each - # separate fetchmailrc file is stored in /etc/fetchmailrc.d - # - # The sole purpose for this is to work around what is known - # as the Fetchmail IMAP idle issue. - function _fetchmailrc_split - { + # Extract the content of `/etc/fetchmailrc` into: + # - Individual `/etc/fetchmailrc.d/fetchmail-*.rc` files, one per server (`poll` entries) + # - Global config options temporarily to `/etc/fetchmailrc.d/defaults`, which is prepended to each `fetchmail-*.rc` file + function _fetchmailrc_split() { local FETCHMAILRC='/etc/fetchmailrc' local FETCHMAILRCD='/etc/fetchmailrc.d' local DEFAULT_FILE="${FETCHMAILRCD}/defaults" - if [[ ! -r ${FETCHMAILRC} ]] - then + if [[ ! -r ${FETCHMAILRC} ]]; then _log 'warn' "File '${FETCHMAILRC}' not found" return 1 fi - if [[ ! -d ${FETCHMAILRCD} ]] - then - if ! mkdir "${FETCHMAILRCD}" - then + if [[ ! -d ${FETCHMAILRCD} ]]; then + if ! mkdir "${FETCHMAILRCD}"; then _log 'warn' "Unable to create folder '${FETCHMAILRCD}'" return 1 fi fi + # Scan through the config: + # 1. Extract the global fetchmail config lines (before any poll entry is configured). + # 2. Once a poll entry line is found, create a new config with the global config and append the poll entry config. + # 3. Repeat step 2 when another poll entry is found, until reaching the end of `/etc/fetchmailrc`. local COUNTER=0 SERVER=0 - while read -r LINE - do - if [[ ${LINE} =~ poll ]] - then - # If we read "poll" then we reached a new server definition - # We need to create a new file with fetchmail defaults from - # /etc/fetcmailrc - COUNTER=$(( COUNTER + 1 )) + while read -r LINE; do + if [[ ${LINE} =~ poll ]]; then + # Signal that global config has been captured (only remaining poll entry configs needs to be parsed): SERVER=1 + + # Create a new fetchmail config for this poll entry: + COUNTER=$(( COUNTER + 1 )) cat "${DEFAULT_FILE}" >"${FETCHMAILRCD}/fetchmail-${COUNTER}.rc" echo "${LINE}" >>"${FETCHMAILRCD}/fetchmail-${COUNTER}.rc" - elif [[ ${SERVER} -eq 0 ]] - then - # We have not yet found "poll". Let's assume we are still reading - # the default settings from /etc/fetchmailrc file + elif [[ ${SERVER} -eq 0 ]]; then + # Until the first poll entry is encountered, all lines are captured as global config: echo "${LINE}" >>"${DEFAULT_FILE}" else - # Just the server settings that need to be added to the specific rc.d file + # Otherwise until a new poll entry is encountered, all lines are captured for the current poll config: echo "${LINE}" >>"${FETCHMAILRCD}/fetchmail-${COUNTER}.rc" fi done < <(_get_valid_lines_from_file "${FETCHMAILRC}") @@ -87,11 +86,12 @@ function _setup_fetchmail_parallel _fetchmailrc_split + # Create supervisord service files for each instance: + # `--idfile` is intended for supporting POP3 with UIDL cache (requires either `--uidl` for CLI, or `uidl` setting in config) local COUNTER=0 - for RC in /etc/fetchmailrc.d/fetchmail-*.rc - do - COUNTER=$(( COUNTER + 1 )) - cat >"/etc/supervisor/conf.d/fetchmail-${COUNTER}.conf" << EOF + for RC in /etc/fetchmailrc.d/fetchmail-*.rc; do + COUNTER=$(( COUNTER + 1 )) + cat >"/etc/supervisor/conf.d/fetchmail-${COUNTER}.conf" << EOF [program:fetchmail-${COUNTER}] startsecs=0 autostart=false @@ -99,8 +99,9 @@ autorestart=true stdout_logfile=/var/log/supervisor/%(program_name)s.log stderr_logfile=/var/log/supervisor/%(program_name)s.log user=fetchmail -command=/usr/bin/fetchmail -f ${RC} -v --nodetach --daemon %(ENV_FETCHMAIL_POLL)s -i /var/lib/fetchmail/.fetchmail-UIDL-cache --pidfile /var/run/fetchmail/%(program_name)s.pid +command=/usr/bin/fetchmail --fetchmailrc ${RC} --verbose --nodetach --daemon %(ENV_FETCHMAIL_POLL)s --idfile /var/lib/fetchmail/.fetchmail-UIDL-cache --pidfile /var/run/fetchmail/%(program_name)s.pid EOF + chmod 700 "${RC}" chown fetchmail:root "${RC}" done diff --git a/target/scripts/startup/setup.d/getmail.sh b/target/scripts/startup/setup.d/getmail.sh new file mode 100644 index 00000000000..e63db13891e --- /dev/null +++ b/target/scripts/startup/setup.d/getmail.sh @@ -0,0 +1,50 @@ +#!/bin/bash + +function _setup_getmail() { + if [[ ${ENABLE_GETMAIL} -eq 1 ]]; then + _log 'trace' 'Preparing Getmail configuration' + + local GETMAIL_RC ID GETMAIL_DIR + + local GETMAIL_CONFIG_DIR='/tmp/docker-mailserver/getmail' + local GETMAIL_RC_DIR='/etc/getmailrc.d' + local GETMAIL_RC_GENERAL_CF="${GETMAIL_CONFIG_DIR}/getmailrc_general.cf" + local GETMAIL_RC_GENERAL='/etc/getmailrc_general' + + # Create the directory /etc/getmailrc.d to place the user config in later. + mkdir -p "${GETMAIL_RC_DIR}" + + # Check if custom getmailrc_general.cf file is present. + if [[ -f "${GETMAIL_RC_GENERAL_CF}" ]]; then + _log 'debug' "Custom 'getmailrc_general.cf' found" + cp "${GETMAIL_RC_GENERAL_CF}" "${GETMAIL_RC_GENERAL}" + fi + + # If no matching filenames are found, and the shell option nullglob is disabled, the word is left unchanged. + # If the nullglob option is set, and no matches are found, the word is removed. + shopt -s nullglob + + # Generate getmailrc configs, starting with the `/etc/getmailrc_general` base config, then appending users own config to the end. + for FILE in "${GETMAIL_CONFIG_DIR}"/*.cf; do + if [[ ${FILE} =~ /getmail/(.+)\.cf ]] && [[ ${FILE} != "${GETMAIL_RC_GENERAL_CF}" ]]; then + ID=${BASH_REMATCH[1]} + + _log 'debug' "Processing getmail config '${ID}'" + + GETMAIL_RC=${GETMAIL_RC_DIR}/${ID} + cat "${GETMAIL_RC_GENERAL}" "${FILE}" >"${GETMAIL_RC}" + fi + done + # Strip read access from non-root due to files containing secrets: + chmod -R 600 "${GETMAIL_RC_DIR}" + + # Directory, where "oldmail" files are stored. + # For more information see: https://getmail6.org/faq.html#faq-about-oldmail + # The debug command for getmail expects this location to exist. + GETMAIL_DIR=/var/lib/getmail + _log 'debug' "Creating getmail state-dir '${GETMAIL_DIR}'" + mkdir -p "${GETMAIL_DIR}" + else + _log 'debug' 'Getmail is disabled' + fi +} diff --git a/target/scripts/startup/setup.d/ldap.sh b/target/scripts/startup/setup.d/ldap.sh index fce2e309cab..1451ec32d1a 100644 --- a/target/scripts/startup/setup.d/ldap.sh +++ b/target/scripts/startup/setup.d/ldap.sh @@ -1,15 +1,12 @@ #!/bin/bash -function _setup_ldap -{ +function _setup_ldap() { _log 'debug' 'Setting up LDAP' _log 'trace' 'Checking for custom configs' - for i in 'users' 'groups' 'aliases' 'domains' - do + for i in 'users' 'groups' 'aliases' 'domains'; do local FPATH="/tmp/docker-mailserver/ldap-${i}.cf" - if [[ -f ${FPATH} ]] - then + if [[ -f ${FPATH} ]]; then cp "${FPATH}" "/etc/postfix/ldap-${i}.cf" fi done @@ -25,8 +22,7 @@ function _setup_ldap /etc/postfix/maps/sender_login_maps.ldap ) - for FILE in "${FILES[@]}" - do + for FILE in "${FILES[@]}"; do [[ ${FILE} =~ ldap-user ]] && export LDAP_QUERY_FILTER="${LDAP_QUERY_FILTER_USER}" [[ ${FILE} =~ ldap-group ]] && export LDAP_QUERY_FILTER="${LDAP_QUERY_FILTER_GROUP}" [[ ${FILE} =~ ldap-aliases ]] && export LDAP_QUERY_FILTER="${LDAP_QUERY_FILTER_ALIAS}" @@ -42,20 +38,12 @@ function _setup_ldap DOVECOT_LDAP_MAPPING['DOVECOT_BASE']="${DOVECOT_BASE:="${LDAP_SEARCH_BASE}"}" DOVECOT_LDAP_MAPPING['DOVECOT_DN']="${DOVECOT_DN:="${LDAP_BIND_DN}"}" DOVECOT_LDAP_MAPPING['DOVECOT_DNPASS']="${DOVECOT_DNPASS:="${LDAP_BIND_PW}"}" - DOVECOT_LDAP_MAPPING['DOVECOT_URIS']="${DOVECOT_URIS:="${DOVECOT_HOSTS:="${LDAP_SERVER_HOST}"}"}" - - # Add protocol to DOVECOT_URIS so that we can use dovecot's "uris" option: - # https://doc.dovecot.org/configuration_manual/authentication/ldap/ - if [[ ${DOVECOT_LDAP_MAPPING["DOVECOT_URIS"]} != *'://'* ]] - then - DOVECOT_LDAP_MAPPING['DOVECOT_URIS']="ldap://${DOVECOT_LDAP_MAPPING["DOVECOT_URIS"]}" - fi + DOVECOT_LDAP_MAPPING['DOVECOT_URIS']="${DOVECOT_URIS:="${LDAP_SERVER_HOST}"}" # Default DOVECOT_PASS_FILTER to the same value as DOVECOT_USER_FILTER DOVECOT_LDAP_MAPPING['DOVECOT_PASS_FILTER']="${DOVECOT_PASS_FILTER:="${DOVECOT_USER_FILTER}"}" - for VAR in "${!DOVECOT_LDAP_MAPPING[@]}" - do + for VAR in "${!DOVECOT_LDAP_MAPPING[@]}"; do export "${VAR}=${DOVECOT_LDAP_MAPPING[${VAR}]}" done @@ -68,22 +56,19 @@ function _setup_ldap _log 'trace' "Configuring LDAP" - if [[ -f /etc/postfix/ldap-users.cf ]] - then + if [[ -f /etc/postfix/ldap-users.cf ]]; then postconf 'virtual_mailbox_maps = ldap:/etc/postfix/ldap-users.cf' else _log 'warn' "'/etc/postfix/ldap-users.cf' not found" fi - if [[ -f /etc/postfix/ldap-domains.cf ]] - then + if [[ -f /etc/postfix/ldap-domains.cf ]]; then postconf 'virtual_mailbox_domains = /etc/postfix/vhost, ldap:/etc/postfix/ldap-domains.cf' else _log 'warn' "'/etc/postfix/ldap-domains.cf' not found" fi - if [[ -f /etc/postfix/ldap-aliases.cf ]] && [[ -f /etc/postfix/ldap-groups.cf ]] - then + if [[ -f /etc/postfix/ldap-aliases.cf ]] && [[ -f /etc/postfix/ldap-groups.cf ]]; then postconf 'virtual_alias_maps = ldap:/etc/postfix/ldap-aliases.cf, ldap:/etc/postfix/ldap-groups.cf' else _log 'warn' "'/etc/postfix/ldap-aliases.cf' and / or '/etc/postfix/ldap-groups.cf' not found" diff --git a/target/scripts/startup/setup.d/log.sh b/target/scripts/startup/setup.d/log.sh index 95c32d1400c..b76413be250 100644 --- a/target/scripts/startup/setup.d/log.sh +++ b/target/scripts/startup/setup.d/log.sh @@ -1,49 +1,33 @@ #!/bin/bash -function _setup_logs_general -{ - _log 'debug' 'Setting up general log files' - - # File/folder permissions are fine when using docker volumes, but may be wrong - # when file system folders are mounted into the container. - # Set the expected values and create missing folders/files just in case. - mkdir -p /var/log/{mail,supervisor} - chown syslog:root /var/log/mail -} - -function _setup_logrotate -{ +function _setup_logrotate() { _log 'debug' 'Setting up logrotate' - LOGROTATE='/var/log/mail/mail.log\n{\n compress\n copytruncate\n delaycompress\n' - - case "${LOGROTATE_INTERVAL}" in - ( 'daily' ) - _log 'trace' 'Setting postfix logrotate interval to daily' - LOGROTATE="${LOGROTATE} rotate 4\n daily\n" - ;; - - ( 'weekly' ) - _log 'trace' 'Setting postfix logrotate interval to weekly' - LOGROTATE="${LOGROTATE} rotate 4\n weekly\n" - ;; - - ( 'monthly' ) - _log 'trace' 'Setting postfix logrotate interval to monthly' - LOGROTATE="${LOGROTATE} rotate 4\n monthly\n" - ;; + if [[ ${LOGROTATE_INTERVAL} =~ ^(daily|weekly|monthly)$ ]]; then + _log 'trace' "Logrotate interval set to ${LOGROTATE_INTERVAL}" + else + _dms_panic__invalid_value 'LOGROTATE_INTERVAL' 'Setup -> Logrotate' + fi - ( * ) - _log 'warn' 'LOGROTATE_INTERVAL not found in _setup_logrotate' - ;; - - esac + if [[ ${LOGROTATE_COUNT} =~ ^[0-9]+$ ]]; then + _log 'trace' "Logrotate count set to ${LOGROTATE_COUNT}" + else + _dms_panic__invalid_value 'LOGROTATE_COUNT' 'Setup -> Logrotate' + fi - echo -e "${LOGROTATE}}" >/etc/logrotate.d/maillog + cat >/etc/logrotate.d/maillog << EOF +/var/log/mail/mail.log +{ + compress + copytruncate + delaycompress + rotate ${LOGROTATE_COUNT} + ${LOGROTATE_INTERVAL} +} +EOF } -function _setup_mail_summary -{ +function _setup_mail_summary() { local ENABLED_MESSAGE ENABLED_MESSAGE="Enabling Postfix log summary reports with recipient '${PFLOGSUMM_RECIPIENT}'" @@ -80,8 +64,7 @@ EOF esac } -function _setup_logwatch -{ +function _setup_logwatch() { echo 'LogFile = /var/log/mail/freshclam.log' >>/etc/logwatch/conf/logfiles/clam-update.conf echo "MailFrom = ${LOGWATCH_SENDER}" >>/etc/logwatch/conf/logwatch.conf echo "Mailer = \"sendmail -t -f ${LOGWATCH_SENDER}\"" >>/etc/logwatch/conf/logwatch.conf @@ -96,8 +79,7 @@ function _setup_logwatch LOGWATCH_FILE="/etc/cron.${LOGWATCH_INTERVAL}/logwatch" INTERVAL='--range Yesterday' - if [[ ${LOGWATCH_INTERVAL} == 'weekly' ]] - then + if [[ ${LOGWATCH_INTERVAL} == 'weekly' ]]; then INTERVAL="--range 'between -7 days and -1 days'" fi diff --git a/target/scripts/startup/setup.d/mail_state.sh b/target/scripts/startup/setup.d/mail_state.sh index 6a2d3ec6ffa..7bcd8be3f87 100644 --- a/target/scripts/startup/setup.d/mail_state.sh +++ b/target/scripts/startup/setup.d/mail_state.sh @@ -1,98 +1,138 @@ #!/bin/bash +DMS_STATE_DIR='/var/mail-state' + # Consolidate all states into a single directory # (/var/mail-state) to allow persistence using docker volumes -function _setup_save_states -{ - local STATEDIR FILE FILES - - STATEDIR='/var/mail-state' - - if [[ ${ONE_DIR} -eq 1 ]] && [[ -d ${STATEDIR} ]] - then - _log 'debug' "Consolidating all state onto ${STATEDIR}" - - # Always enabled features: - FILES=( - lib/logrotate - lib/postfix - spool/postfix - ) - - # Only consolidate state for services that are enabled - # Notably avoids copying over 200MB for the ClamAV database - [[ ${ENABLE_AMAVIS} -eq 1 ]] && FILES+=('lib/amavis') - [[ ${ENABLE_CLAMAV} -eq 1 ]] && FILES+=('lib/clamav') - [[ ${ENABLE_FAIL2BAN} -eq 1 ]] && FILES+=('lib/fail2ban') - [[ ${ENABLE_FETCHMAIL} -eq 1 ]] && FILES+=('lib/fetchmail') - [[ ${ENABLE_POSTGREY} -eq 1 ]] && FILES+=('lib/postgrey') - [[ ${ENABLE_RSPAMD} -eq 1 ]] && FILES+=('lib/rspamd') - [[ ${ENABLE_RSPAMD_REDIS} -eq 1 ]] && FILES+=('lib/redis') - [[ ${ENABLE_SPAMASSASSIN} -eq 1 ]] && FILES+=('lib/spamassassin') - [[ ${SMTP_ONLY} -ne 1 ]] && FILES+=('lib/dovecot') - - for FILE in "${FILES[@]}" - do - DEST="${STATEDIR}/${FILE//\//-}" - FILE="/var/${FILE}" - - # If relevant content is found in /var/mail-state (presumably a volume mount), - # use it instead. Otherwise copy over any missing directories checked. - if [[ -d ${DEST} ]] - then - _log 'trace' "Destination ${DEST} exists, linking ${FILE} to it" - # Original content from image no longer relevant, remove it: - rm -rf "${FILE}" - elif [[ -d ${FILE} ]] - then - _log 'trace' "Moving contents of ${FILE} to ${DEST}" - # Empty volume was mounted, or new content from enabling a feature ENV: - mv "${FILE}" "${DEST}" - fi - - # Symlink the original path in the container ($FILE) to be - # sourced from assocaiated path in /var/mail-state/ ($DEST): - ln -s "${DEST}" "${FILE}" - done - - # This ensures the user and group of the files from the external mount have their - # numeric ID values in sync. New releases where the installed packages order changes - # can change the values in the Docker image, causing an ownership mismatch. - # NOTE: More details about users and groups added during image builds are documented here: - # https://github.com/docker-mailserver/docker-mailserver/pull/3011#issuecomment-1399120252 - _log 'trace' "Fixing ${STATEDIR}/* permissions" - [[ ${ENABLE_AMAVIS} -eq 1 ]] && chown -R amavis:amavis "${STATEDIR}/lib-amavis" - [[ ${ENABLE_CLAMAV} -eq 1 ]] && chown -R clamav:clamav "${STATEDIR}/lib-clamav" - [[ ${ENABLE_FETCHMAIL} -eq 1 ]] && chown -R fetchmail:nogroup "${STATEDIR}/lib-fetchmail" - [[ ${ENABLE_POSTGREY} -eq 1 ]] && chown -R postgrey:postgrey "${STATEDIR}/lib-postgrey" - [[ ${ENABLE_RSPAMD} -eq 1 ]] && chown -R _rspamd:_rspamd "${STATEDIR}/lib-rspamd" - [[ ${ENABLE_RSPAMD_REDIS} -eq 1 ]] && chown -R redis:redis "${STATEDIR}/lib-redis" - [[ ${ENABLE_SPAMASSASSIN} -eq 1 ]] && chown -R debian-spamd:debian-spamd "${STATEDIR}/lib-spamassassin" - - chown -R root:root "${STATEDIR}/lib-logrotate" - chown -R postfix:postfix "${STATEDIR}/lib-postfix" - - # NOTE: The Postfix spool location has mixed owner/groups to take into account: - # UID = postfix(101): active, bounce, corrupt, defer, deferred, flush, hold, incoming, maildrop, private, public, saved, trace - # UID = root(0): dev, etc, lib, pid, usr - # GID = postdrop(103): maildrop, public - # GID for all other directories is root(0) - # NOTE: `spool-postfix/private/` will be set to `postfix:postfix` when Postfix starts / restarts - # Set most common ownership: - chown -R postfix:root "${STATEDIR}/spool-postfix" - chown root:root "${STATEDIR}/spool-postfix" - - # These two require the postdrop(103) group: - chgrp -R postdrop "${STATEDIR}"/spool-postfix/{maildrop,public} - - # After changing the group, special bits (set-gid, sticky) may be stripped, restore them: - # Ref: https://github.com/docker-mailserver/docker-mailserver/pull/3149#issuecomment-1454981309 - chmod 1730 "${STATEDIR}/spool-postfix/maildrop" - chmod 2710 "${STATEDIR}/spool-postfix/public" - elif [[ ${ONE_DIR} -eq 1 ]] - then - _log 'warn' "'ONE_DIR=1' but no volume was mounted to '${STATEDIR}'" - else - _log 'debug' 'Not consolidating state (because it has been disabled)' +function _setup_save_states() { + if [[ ! -d ${DMS_STATE_DIR} ]]; then + _log 'debug' "'${DMS_STATE_DIR}' is not present - not consolidating state" + return 0 fi + + _log 'debug' "Consolidating all state onto ${DMS_STATE_DIR}" + + local DEST SERVICEDIR SERVICEDIRS SERVICEFILE SERVICEFILES + + # Always enabled features: + SERVICEDIRS=( + 'lib/logrotate' + 'lib/postfix' + 'spool/postfix' + ) + + # Only consolidate state for services that are enabled + # Notably avoids copying over 200MB for the ClamAV database + [[ ${ENABLE_AMAVIS} -eq 1 ]] && SERVICEDIRS+=('lib/amavis') + [[ ${ENABLE_CLAMAV} -eq 1 ]] && SERVICEDIRS+=('lib/clamav') + [[ ${ENABLE_FAIL2BAN} -eq 1 ]] && SERVICEDIRS+=('lib/fail2ban') + [[ ${ENABLE_FETCHMAIL} -eq 1 ]] && SERVICEDIRS+=('lib/fetchmail') + [[ ${ENABLE_GETMAIL} -eq 1 ]] && SERVICEDIRS+=('lib/getmail') + [[ ${ENABLE_MTA_STS} -eq 1 ]] && SERVICEDIRS+=('lib/mta-sts') + [[ ${ENABLE_POSTGREY} -eq 1 ]] && SERVICEDIRS+=('lib/postgrey') + [[ ${ENABLE_RSPAMD} -eq 1 ]] && SERVICEDIRS+=('lib/rspamd') + [[ ${ENABLE_RSPAMD_REDIS} -eq 1 ]] && SERVICEDIRS+=('lib/redis') + [[ ${ENABLE_SPAMASSASSIN} -eq 1 ]] && SERVICEDIRS+=('lib/spamassassin') + [[ ${ENABLE_SRS} -eq 1 ]] && SERVICEDIRS+=('lib/postsrsd') + [[ ${SMTP_ONLY} -ne 1 ]] && SERVICEDIRS+=('lib/dovecot') + + # Single service files + [[ ${ENABLE_SRS} -eq 1 ]] && SERVICEFILES+=('/etc/postsrsd.secret') + + for SERVICEFILE in "${SERVICEFILES[@]}"; do + DEST="${DMS_STATE_DIR}/${SERVICEFILE}" + + # Append service parent dir(s) path to the state dir and ensure it exists: + mkdir -p "${DEST%/*}" + if [[ -f ${DEST} ]]; then + _log 'trace' "Destination ${DEST} exists, linking ${SERVICEFILE} to it" + # Original content from image no longer relevant, remove it: + rm -f "${SERVICEFILE}" + elif [[ -f "${SERVICEFILE}" ]]; then + _log 'trace' "Moving ${SERVICEFILE} to ${DEST}" + # Empty volume was mounted, or new content from enabling a feature ENV: + mv "${SERVICEFILE}" "${DEST}" + # Apply SELinux security context to match the state directory, so access + # is not restricted to the current running container: + chcon -R --reference="${DMS_STATE_DIR}" "${DEST}" 2>/dev/null || true + fi + + # Symlink the original file in the container ($SERVICEFILE) to be + # sourced from assocaiated path in /var/mail-state/ ($DEST): + ln -s "${DEST}" "${SERVICEFILE}" + done + + for SERVICEDIR in "${SERVICEDIRS[@]}"; do + DEST="${DMS_STATE_DIR}/${SERVICEDIR//\//-}" + SERVICEDIR="/var/${SERVICEDIR}" + + # If relevant content is found in /var/mail-state (presumably a volume mount), + # use it instead. Otherwise copy over any missing directories checked. + if [[ -d ${DEST} ]]; then + _log 'trace' "Destination ${DEST} exists, linking ${SERVICEDIR} to it" + # Original content from image no longer relevant, remove it: + rm -rf "${SERVICEDIR}" + elif [[ -d ${SERVICEDIR} ]]; then + _log 'trace' "Moving contents of ${SERVICEDIR} to ${DEST}" + # An empty volume was mounted, or new content dir now exists from enabling a feature ENV: + mv "${SERVICEDIR}" "${DEST}" + # Apply SELinux security context to match the state directory, so access + # is not restricted to the current running container: + # https://github.com/docker-mailserver/docker-mailserver/pull/3890 + chcon -R --reference="${DMS_STATE_DIR}" "${DEST}" 2>/dev/null || true + else + _log 'error' "${SERVICEDIR} should exist but is missing" + fi + + # Symlink the original path in the container ($SERVICEDIR) to be + # sourced from associated path in /var/mail-state/ ($DEST): + ln -s "${DEST}" "${SERVICEDIR}" + done +} + +# These corrections are to fix changes to UID/GID values between upgrades, +# or when ownership/permissions were altered externally on the host (eg: migration or system scripts) +function _setup_adjust_state_permissions() { + [[ ! -d ${DMS_STATE_DIR} ]] && return 0 + + # Parent directories must have executable bit set to descend the file tree for access, + # as each service running as a non-root user requires this to access their state directory, + # `/var/mail-state` must allow all users `+x`: + chmod +x "${DMS_STATE_DIR}" + + # This ensures the user and group of the files from the external mount have their + # numeric ID values in sync. New releases where the installed packages order changes + # can change the values in the Docker image, causing an ownership mismatch. + # NOTE: More details about users and groups added during image builds are documented here: + # https://github.com/docker-mailserver/docker-mailserver/pull/3011#issuecomment-1399120252 + _log 'trace' "Ensuring correct ownership + permissions for DMS state dir: '${DMS_STATE_DIR}'" + [[ ${ENABLE_AMAVIS} -eq 1 ]] && chown -R amavis:amavis "${DMS_STATE_DIR}/lib-amavis" + [[ ${ENABLE_CLAMAV} -eq 1 ]] && chown -R clamav:clamav "${DMS_STATE_DIR}/lib-clamav" + [[ ${ENABLE_FETCHMAIL} -eq 1 ]] && chown -R fetchmail:nogroup "${DMS_STATE_DIR}/lib-fetchmail" + [[ ${ENABLE_MTA_STS} -eq 1 ]] && chown -R _mta-sts:_mta-sts "${DMS_STATE_DIR}/lib-mta-sts" + [[ ${ENABLE_POSTGREY} -eq 1 ]] && chown -R postgrey:postgrey "${DMS_STATE_DIR}/lib-postgrey" + [[ ${ENABLE_RSPAMD} -eq 1 ]] && chown -R _rspamd:_rspamd "${DMS_STATE_DIR}/lib-rspamd" + [[ ${ENABLE_RSPAMD_REDIS} -eq 1 ]] && chown -R redis:redis "${DMS_STATE_DIR}/lib-redis" + [[ ${ENABLE_SPAMASSASSIN} -eq 1 ]] && chown -R debian-spamd:debian-spamd "${DMS_STATE_DIR}/lib-spamassassin" + + chown -R root:root "${DMS_STATE_DIR}/lib-logrotate" + chown -R postfix:postfix "${DMS_STATE_DIR}/lib-postfix" + + # NOTE: The Postfix spool location has mixed owner/groups to take into account: + # UID = postfix(101): active, bounce, corrupt, defer, deferred, flush, hold, incoming, maildrop, private, public, saved, trace + # UID = root(0): dev, etc, lib, pid, usr + # GID = postdrop(103): maildrop, public + # GID for all other directories is root(0) + # NOTE: `spool-postfix/private/` will be set to `postfix:postfix` when Postfix starts / restarts + # Set most common ownership: + chown -R postfix:root "${DMS_STATE_DIR}/spool-postfix" + chown root:root "${DMS_STATE_DIR}/spool-postfix" + + # These two require the postdrop(103) group: + chgrp -R postdrop "${DMS_STATE_DIR}"/spool-postfix/{maildrop,public} + + # These permissions rely on the `postdrop` binary having the SGID bit set. + # Ref: https://github.com/docker-mailserver/docker-mailserver/pull/3625 + chmod 730 "${DMS_STATE_DIR}/spool-postfix/maildrop" + chmod 710 "${DMS_STATE_DIR}/spool-postfix/public" } diff --git a/target/scripts/startup/setup.d/mta-sts.sh b/target/scripts/startup/setup.d/mta-sts.sh new file mode 100644 index 00000000000..7d1f88eaf1e --- /dev/null +++ b/target/scripts/startup/setup.d/mta-sts.sh @@ -0,0 +1,7 @@ +#!/bin/bash + + +function _setup_mta_sts() { + _log 'trace' 'Adding MTA-STS lookup to the Postfix TLS policy map' + _add_to_or_update_postfix_main smtp_tls_policy_maps 'socketmap:unix:/var/run/mta-sts/daemon.sock:postfix' +} diff --git a/target/scripts/startup/setup.d/networking.sh b/target/scripts/startup/setup.d/networking.sh index 396db87576f..35c33db85e9 100644 --- a/target/scripts/startup/setup.d/networking.sh +++ b/target/scripts/startup/setup.d/networking.sh @@ -1,13 +1,11 @@ #!/bin/bash -function _setup_mailname -{ +function _setup_mailname() { _log 'debug' "Setting up mailname and creating '/etc/mailname'" echo "${DOMAINNAME}" >/etc/mailname } -function _setup_docker_permit -{ +function _setup_docker_permit() { _log 'debug' 'Setting up PERMIT_DOCKER option' local CONTAINER_IP CONTAINER_NETWORK @@ -19,25 +17,21 @@ function _setup_docker_permit grep 'inet ' | sed 's|[^0-9\.\/]*||g' | cut -d '/' -f 1) CONTAINER_NETWORK=$(echo "${CONTAINER_IP}" | cut -d '.' -f1-2).0.0 - if [[ -z ${CONTAINER_IP} ]] - then + if [[ -z ${CONTAINER_IP} ]]; then _log 'error' 'Detecting the container IP address failed' - _dms_panic__misconfigured 'NETWORK_INTERFACE' 'Network Setup [docker_permit]' 'immediate' + _dms_panic__misconfigured 'NETWORK_INTERFACE' 'Network Setup [docker_permit]' fi - while read -r IP - do + while read -r IP; do CONTAINER_NETWORKS+=("${IP}") done < <(ip -o -4 addr show type veth | grep -E -o '[0-9\.]+/[0-9]+') - function __clear_postfix_mynetworks - { + function __clear_postfix_mynetworks() { _log 'trace' "Clearing Postfix's 'mynetworks'" postconf "mynetworks =" } - function __add_to_postfix_mynetworks - { + function __add_to_postfix_mynetworks() { local NETWORK_TYPE=$1 local NETWORK=$2 @@ -54,8 +48,7 @@ function _setup_docker_permit ;; ( 'connected-networks' ) - for CONTAINER_NETWORK in "${CONTAINER_NETWORKS[@]}" - do + for CONTAINER_NETWORK in "${CONTAINER_NETWORKS[@]}"; do CONTAINER_NETWORK=$(_sanitize_ipv4_to_subnet_cidr "${CONTAINER_NETWORK}") __add_to_postfix_mynetworks 'Docker Network' "${CONTAINER_NETWORK}" done diff --git a/target/scripts/startup/setup.d/oauth2.sh b/target/scripts/startup/setup.d/oauth2.sh new file mode 100644 index 00000000000..20e9ffd1366 --- /dev/null +++ b/target/scripts/startup/setup.d/oauth2.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +function _setup_oauth2() { + _log 'debug' 'Setting up OAUTH2' + + # Enable OAuth2 PassDB (Authentication): + sedfile -i -e '/\!include auth-oauth2\.conf\.ext/s/^#//' /etc/dovecot/conf.d/10-auth.conf + _replace_by_env_in_file 'OAUTH2_' '/etc/dovecot/dovecot-oauth2.conf.ext' + + return 0 +} diff --git a/target/scripts/startup/setup.d/postfix.sh b/target/scripts/startup/setup.d/postfix.sh index 4d1bbf6ffc0..ac9c23e7f66 100644 --- a/target/scripts/startup/setup.d/postfix.sh +++ b/target/scripts/startup/setup.d/postfix.sh @@ -1,141 +1,153 @@ #!/bin/bash -function _setup_postfix_sizelimits -{ - _log 'trace' "Configuring Postfix message size limit to '${POSTFIX_MESSAGE_SIZE_LIMIT}'" - postconf "message_size_limit = ${POSTFIX_MESSAGE_SIZE_LIMIT}" +# Just a helper to prepend the log messages with `(Postfix setup)` so +# users know exactly where the message originated from. +# +# @param ${1} = log level +# @param ${2} = message +function __postfix__log { _log "${1:-}" "(Postfix setup) ${2:-}" ; } - _log 'trace' "Configuring Postfix mailbox size limit to '${POSTFIX_MAILBOX_SIZE_LIMIT}'" - postconf "mailbox_size_limit = ${POSTFIX_MAILBOX_SIZE_LIMIT}" +function _setup_postfix_early() { + _log 'debug' 'Configuring Postfix (early setup)' - _log 'trace' "Configuring Postfix virtual mailbox size limit to '${POSTFIX_MAILBOX_SIZE_LIMIT}'" - postconf "virtual_mailbox_limit = ${POSTFIX_MAILBOX_SIZE_LIMIT}" -} - -function _setup_postfix_access_control -{ - _log 'trace' 'Configuring user access' - - if [[ -f /tmp/docker-mailserver/postfix-send-access.cf ]] - then - sed -i 's|smtpd_sender_restrictions =|smtpd_sender_restrictions = check_sender_access texthash:/tmp/docker-mailserver/postfix-send-access.cf,|' /etc/postfix/main.cf - fi + __postfix__log 'trace' 'Applying hostname and domainname' + postconf "myhostname = ${HOSTNAME}" + postconf "mydomain = ${DOMAINNAME}" - if [[ -f /tmp/docker-mailserver/postfix-receive-access.cf ]] - then - sed -i 's|smtpd_recipient_restrictions =|smtpd_recipient_restrictions = check_recipient_access texthash:/tmp/docker-mailserver/postfix-receive-access.cf,|' /etc/postfix/main.cf + if [[ ${POSTFIX_INET_PROTOCOLS} != 'all' ]]; then + __postfix__log 'trace' 'Setting up POSTFIX_INET_PROTOCOLS option' + postconf "inet_protocols = ${POSTFIX_INET_PROTOCOLS}" fi -} -function _setup_postfix_sasl -{ - if [[ ${ENABLE_SASLAUTHD} -eq 1 ]] && [[ ! -f /etc/postfix/sasl/smtpd.conf ]] - then + __postfix__log 'trace' "Configuring SASLauthd" + if [[ ${ENABLE_SASLAUTHD} -eq 1 ]] && [[ ! -f /etc/postfix/sasl/smtpd.conf ]]; then cat >/etc/postfix/sasl/smtpd.conf << EOF pwcheck_method: saslauthd mech_list: plain login EOF fi - if [[ ${ENABLE_SASLAUTHD} -eq 0 ]] && [[ ${SMTP_ONLY} -eq 1 ]] - then + # User has explicitly requested to disable SASL auth: + # TODO: Additive config by feature would be better. Should only enable SASL auth + # on submission(s) services in master.cf when SASLAuthd or Dovecot is enabled. + if [[ ${ENABLE_SASLAUTHD} -eq 0 ]] && [[ ${SMTP_ONLY} -eq 1 ]]; then + # Default for services (eg: Port 25); NOTE: This has since become the default: sed -i -E \ 's|^smtpd_sasl_auth_enable =.*|smtpd_sasl_auth_enable = no|g' \ /etc/postfix/main.cf + # Submission services that are explicitly enabled by default: sed -i -E \ 's|^ -o smtpd_sasl_auth_enable=.*| -o smtpd_sasl_auth_enable=no|g' \ /etc/postfix/master.cf fi -} -function _setup_postfix_aliases -{ - _log 'debug' 'Setting up Postfix aliases' + # scripts/helpers/aliases.sh:_create_aliases() + __postfix__log 'trace' 'Setting up aliases' _create_aliases -} -function _setup_postfix_vhost -{ - _log 'debug' 'Setting up Postfix vhost' + # scripts/helpers/postfix.sh:_create_postfix_vhost() + __postfix__log 'trace' 'Setting up Postfix vhost' _create_postfix_vhost -} -function _setup_postfix_inet_protocols -{ - [[ ${POSTFIX_INET_PROTOCOLS} == 'all' ]] && return 0 - - _log 'trace' 'Setting up POSTFIX_INET_PROTOCOLS option' - postconf "inet_protocols = ${POSTFIX_INET_PROTOCOLS}" -} + __postfix__log 'trace' 'Setting up DH Parameters' + _setup_dhparam 'Postfix' '/etc/postfix/dhparams.pem' -function _setup_postfix_virtual_transport -{ - [[ -z ${POSTFIX_DAGENT} ]] && return 0 + __postfix__log 'trace' "Configuring message size limit to '${POSTFIX_MESSAGE_SIZE_LIMIT}'" + postconf "message_size_limit = ${POSTFIX_MESSAGE_SIZE_LIMIT}" - _log 'trace' "Changing Postfix virtual transport to '${POSTFIX_DAGENT}'" - # Default value in main.cf should be 'lmtp:unix:/var/run/dovecot/lmtp' - postconf "virtual_transport = ${POSTFIX_DAGENT}" -} + __postfix__log 'trace' "Configuring mailbox size limit to '${POSTFIX_MAILBOX_SIZE_LIMIT}'" + postconf "mailbox_size_limit = ${POSTFIX_MAILBOX_SIZE_LIMIT}" -function _setup_postfix_override_configuration -{ - _log 'debug' 'Overriding / adjusting Postfix configuration with user-supplied values' + __postfix__log 'trace' "Configuring virtual mailbox size limit to '${POSTFIX_MAILBOX_SIZE_LIMIT}'" + postconf "virtual_mailbox_limit = ${POSTFIX_MAILBOX_SIZE_LIMIT}" - if [[ -f /tmp/docker-mailserver/postfix-main.cf ]] - then - cat /tmp/docker-mailserver/postfix-main.cf >>/etc/postfix/main.cf - _adjust_mtime_for_postfix_maincf + if [[ ${POSTFIX_REJECT_UNKNOWN_CLIENT_HOSTNAME} -eq 1 ]]; then + __postfix__log 'trace' 'Enabling reject_unknown_client_hostname to dms_smtpd_sender_restrictions' + sedfile -i -E \ + 's|^(dms_smtpd_sender_restrictions = .*)|\1, reject_unknown_client_hostname|' \ + /etc/postfix/main.cf + fi - # do not directly output to 'main.cf' as this causes a read-write-conflict - postconf -n >/tmp/postfix-main-new.cf 2>/dev/null + # Dovecot feature integration + # TODO: Alias SMTP_ONLY=0 to DOVECOT_ENABLED=1? + if [[ ${SMTP_ONLY} -ne 1 ]]; then + __postfix__log 'trace' 'Configuring Postfix with Dovecot integration' - mv /tmp/postfix-main-new.cf /etc/postfix/main.cf - _adjust_mtime_for_postfix_maincf - _log 'trace' "Adjusted '/etc/postfix/main.cf' according to '/tmp/docker-mailserver/postfix-main.cf'" - else - _log 'trace' "No extra Postfix settings loaded because optional '/tmp/docker-mailserver/postfix-main.cf' was not provided" + # /etc/postfix/vmailbox is created by: scripts/helpers/accounts.sh:_create_accounts() + # This file config is for Postfix to verify a mail account exists before accepting + # mail arriving and delivering it to Dovecot over LMTP. + if [[ ${ACCOUNT_PROVISIONER} == 'FILE' ]]; then + postconf 'virtual_mailbox_maps = texthash:/etc/postfix/vmailbox' + fi + # Historical context regarding decision to use LMTP instead of LDA (do not change this): + # https://github.com/docker-mailserver/docker-mailserver/issues/4178#issuecomment-2375489302 + postconf 'virtual_transport = lmtp:unix:/var/run/dovecot/lmtp' fi - if [[ -f /tmp/docker-mailserver/postfix-master.cf ]] - then - while read -r LINE - do - if [[ ${LINE} =~ ^[0-9a-z] ]] - then - postconf -P "${LINE}" - fi - done < /tmp/docker-mailserver/postfix-master.cf - _log 'trace' "Adjusted '/etc/postfix/master.cf' according to '/tmp/docker-mailserver/postfix-master.cf'" - else - _log 'trace' "No extra Postfix settings loaded because optional '/tmp/docker-mailserver/postfix-master.cf' was not provided" + if [[ -n ${POSTFIX_DAGENT} ]]; then + __postfix__log 'trace' "Changing virtual transport to '${POSTFIX_DAGENT}'" + postconf "virtual_transport = ${POSTFIX_DAGENT}" fi } -function _setup_postfix_relay_hosts -{ +function _setup_postfix_late() { + _log 'debug' 'Configuring Postfix (late setup)' + + # These two config files are `access` database tables managed via `setup email restrict`: + # NOTE: Prepends to existing restrictions, thus has priority over other permit/reject policies that follow. + # https://www.postfix.org/postconf.5.html#smtpd_sender_restrictions + # https://www.postfix.org/access.5.html + __postfix__log 'trace' 'Configuring user access' + if [[ -f /tmp/docker-mailserver/postfix-send-access.cf ]]; then + # Prefer to prepend to our specialized variant instead: + # https://github.com/docker-mailserver/docker-mailserver/pull/4379 + sed -i -E 's|^(dms_smtpd_sender_restrictions =)|\1 check_sender_access texthash:/tmp/docker-mailserver/postfix-send-access.cf,|' /etc/postfix/main.cf + fi + + if [[ -f /tmp/docker-mailserver/postfix-receive-access.cf ]]; then + sed -i -E 's|^(smtpd_recipient_restrictions =)|\1 check_recipient_access texthash:/tmp/docker-mailserver/postfix-receive-access.cf,|' /etc/postfix/main.cf + fi + + __postfix__log 'trace' 'Configuring relay host' _setup_relayhost -} -function _setup_postfix_dhparam -{ - _setup_dhparam 'Postfix' '/etc/postfix/dhparams.pem' + __postfix__setup_override_configuration } -function _setup_dnsbl_disable -{ - _log 'debug' 'Disabling postscreen DNS block lists' - postconf 'postscreen_dnsbl_action = ignore' - postconf 'postscreen_dnsbl_sites = ' -} +function __postfix__setup_override_configuration() { + __postfix__log 'debug' 'Overriding / adjusting configuration with user-supplied values' + + local OVERRIDE_CONFIG_POSTFIX_MASTER='/tmp/docker-mailserver/postfix-master.cf' + if [[ -f ${OVERRIDE_CONFIG_POSTFIX_MASTER} ]]; then + while read -r LINE; do + [[ ${LINE} =~ ^[0-9a-z] ]] && postconf -P "${LINE}" + done < <(_get_valid_lines_from_file "${OVERRIDE_CONFIG_POSTFIX_MASTER}") + __postfix__log 'trace' "Adjusted '/etc/postfix/master.cf' according to '${OVERRIDE_CONFIG_POSTFIX_MASTER}'" + else + __postfix__log 'trace' "No extra Postfix master settings loaded because optional '${OVERRIDE_CONFIG_POSTFIX_MASTER}' was not provided" + fi + + # NOTE: `postfix-main.cf` should be handled after `postfix-master.cf` as custom parameters require an existing reference + # in either `main.cf` or `master.cf` prior to `postconf` reading `main.cf`, otherwise it is discarded from output. + local OVERRIDE_CONFIG_POSTFIX_MAIN='/tmp/docker-mailserver/postfix-main.cf' + if [[ -f ${OVERRIDE_CONFIG_POSTFIX_MAIN} ]]; then + cat "${OVERRIDE_CONFIG_POSTFIX_MAIN}" >>/etc/postfix/main.cf + _adjust_mtime_for_postfix_maincf -function _setup_postfix_smtputf8 -{ - _log 'trace' "Disabling Postfix's smtputf8 support" - postconf 'smtputf8_enable = no' + # Do not directly output to 'main.cf' as this causes a read-write-conflict. + # `postconf` output is filtered to skip expected warnings regarding overrides: + # https://github.com/docker-mailserver/docker-mailserver/pull/3880#discussion_r1510414576 + postconf -n >/tmp/postfix-main-new.cf 2> >(grep -v 'overriding earlier entry' >&2) + + mv /tmp/postfix-main-new.cf /etc/postfix/main.cf + _adjust_mtime_for_postfix_maincf + __postfix__log 'trace' "Adjusted '/etc/postfix/main.cf' according to '${OVERRIDE_CONFIG_POSTFIX_MAIN}'" + else + __postfix__log 'trace' "No extra Postfix main settings loaded because optional '${OVERRIDE_CONFIG_POSTFIX_MAIN}' was not provided" + fi } -function _setup_SRS -{ +function _setup_SRS() { _log 'debug' 'Setting up SRS' postconf 'sender_canonical_maps = tcp:localhost:10001' @@ -143,55 +155,33 @@ function _setup_SRS postconf 'recipient_canonical_maps = tcp:localhost:10002' postconf 'recipient_canonical_classes = envelope_recipient,header_recipient' - function __generate_secret - { + function __generate_secret() { ( umask 0077 dd if=/dev/urandom bs=24 count=1 2>/dev/null | base64 -w0 >"${1}" ) } - local POSTSRSD_SECRET_FILE POSTSRSD_STATE_DIR POSTSRSD_STATE_SECRET_FILE + local POSTSRSD_SECRET_FILE sed -i "s/localdomain/${SRS_DOMAINNAME}/g" /etc/default/postsrsd POSTSRSD_SECRET_FILE='/etc/postsrsd.secret' - POSTSRSD_STATE_DIR='/var/mail-state/etc-postsrsd' - POSTSRSD_STATE_SECRET_FILE="${POSTSRSD_STATE_DIR}/postsrsd.secret" - if [[ -n ${SRS_SECRET} ]] - then + if [[ -n ${SRS_SECRET} ]]; then ( umask 0077 echo "${SRS_SECRET}" | tr ',' '\n' >"${POSTSRSD_SECRET_FILE}" ) else - if [[ ${ONE_DIR} -eq 1 ]] - then - if [[ ! -f ${POSTSRSD_STATE_SECRET_FILE} ]] - then - install -d -m 0775 "${POSTSRSD_STATE_DIR}" - __generate_secret "${POSTSRSD_STATE_SECRET_FILE}" - fi - - install -m 0400 "${POSTSRSD_STATE_SECRET_FILE}" "${POSTSRSD_SECRET_FILE}" - elif [[ ! -f ${POSTSRSD_SECRET_FILE} ]] - then + if [[ ! -f ${POSTSRSD_SECRET_FILE} ]]; then __generate_secret "${POSTSRSD_SECRET_FILE}" fi fi - if [[ -n ${SRS_EXCLUDE_DOMAINS} ]] - then - sed -i \ - "s/^#\?(SRS_EXCLUDE_DOMAINS=).*$/\1=${SRS_EXCLUDE_DOMAINS}/g" \ + if [[ -n ${SRS_EXCLUDE_DOMAINS} ]]; then + sedfile -i -E \ + "s|^#?(SRS_EXCLUDE_DOMAINS=).*|\1${SRS_EXCLUDE_DOMAINS}|" \ /etc/default/postsrsd fi } - -function _setup_postfix_hostname -{ - _log 'debug' 'Applying hostname and domainname to Postfix' - postconf "myhostname = ${HOSTNAME}" - postconf "mydomain = ${DOMAINNAME}" -} diff --git a/target/scripts/startup/setup.d/saslauthd.sh b/target/scripts/startup/setup.d/saslauthd.sh index 1a6488d0676..eb33a24329c 100644 --- a/target/scripts/startup/setup.d/saslauthd.sh +++ b/target/scripts/startup/setup.d/saslauthd.sh @@ -1,31 +1,29 @@ #!/bin/bash - -function _setup_saslauthd -{ +function _setup_saslauthd() { _log 'debug' 'Setting up SASLAUTHD' - if [[ ! -f /etc/saslauthd.conf ]] - then + # NOTE: It's unlikely this file would already exist, + # Unlike Dovecot/Postfix LDAP support, this file has no ENV replacement + # nor does it copy from the DMS config volume to this internal location. + if [[ ${ACCOUNT_PROVISIONER} == 'LDAP' ]] \ + && [[ ! -f /etc/saslauthd.conf ]]; then _log 'trace' 'Creating /etc/saslauthd.conf' - cat > /etc/saslauthd.conf << EOF -ldap_servers: ${SASLAUTHD_LDAP_SERVER} - -ldap_auth_method: ${SASLAUTHD_LDAP_AUTH_METHOD} -ldap_bind_dn: ${SASLAUTHD_LDAP_BIND_DN} -ldap_bind_pw: ${SASLAUTHD_LDAP_PASSWORD} - -ldap_search_base: ${SASLAUTHD_LDAP_SEARCH_BASE} -ldap_filter: ${SASLAUTHD_LDAP_FILTER} - -ldap_start_tls: ${SASLAUTHD_LDAP_START_TLS} -ldap_tls_check_peer: ${SASLAUTHD_LDAP_TLS_CHECK_PEER} - -${SASLAUTHD_LDAP_TLS_CACERT_FILE} -${SASLAUTHD_LDAP_TLS_CACERT_DIR} -${SASLAUTHD_LDAP_PASSWORD_ATTR} -${SASLAUTHD_LDAP_MECH} + # Create a config based on ENV + sed '/^.*: $/d'> /etc/saslauthd.conf << EOF +ldap_servers: ${SASLAUTHD_LDAP_SERVER:=${LDAP_SERVER_HOST}} +ldap_auth_method: ${SASLAUTHD_LDAP_AUTH_METHOD:=bind} +ldap_bind_dn: ${SASLAUTHD_LDAP_BIND_DN:=${LDAP_BIND_DN}} +ldap_bind_pw: ${SASLAUTHD_LDAP_PASSWORD:=${LDAP_BIND_PW}} +ldap_search_base: ${SASLAUTHD_LDAP_SEARCH_BASE:=${LDAP_SEARCH_BASE}} +ldap_filter: ${SASLAUTHD_LDAP_FILTER:=(&(uniqueIdentifier=%u)(mailEnabled=TRUE))} +ldap_start_tls: ${SASLAUTHD_LDAP_START_TLS:=no} +ldap_tls_check_peer: ${SASLAUTHD_LDAP_TLS_CHECK_PEER:=no} +ldap_tls_cacert_file: ${SASLAUTHD_LDAP_TLS_CACERT_FILE} +ldap_tls_cacert_dir: ${SASLAUTHD_LDAP_TLS_CACERT_DIR} +ldap_password_attr: ${SASLAUTHD_LDAP_PASSWORD_ATTR} +ldap_mech: ${SASLAUTHD_LDAP_MECH} ldap_referrals: yes log_level: 10 EOF @@ -44,4 +42,3 @@ EOF gpasswd -a postfix sasl >/dev/null } - diff --git a/target/scripts/startup/setup.d/security/misc.sh b/target/scripts/startup/setup.d/security/misc.sh index d441f7d136a..a56fecb56f6 100644 --- a/target/scripts/startup/setup.d/security/misc.sh +++ b/target/scripts/startup/setup.d/security/misc.sh @@ -1,7 +1,6 @@ #!/bin/bash -function _setup_security_stack -{ +function _setup_security_stack() { _log 'debug' 'Setting up Security Stack' __setup__security__postgrey @@ -23,10 +22,8 @@ function _setup_security_stack __setup__security__amavis } -function __setup__security__postgrey -{ - if [[ ${ENABLE_POSTGREY} -eq 1 ]] - then +function __setup__security__postgrey() { + if [[ ${ENABLE_POSTGREY} -eq 1 ]]; then _log 'debug' 'Enabling and configuring Postgrey' sedfile -i -E \ @@ -37,35 +34,30 @@ function __setup__security__postgrey "s|\"--inet=127.0.0.1:10023\"|\"--inet=127.0.0.1:10023 --delay=${POSTGREY_DELAY} --max-age=${POSTGREY_MAX_AGE} --auto-whitelist-clients=${POSTGREY_AUTO_WHITELIST_CLIENTS}\"|" \ /etc/default/postgrey - if ! grep -i 'POSTGREY_TEXT' /etc/default/postgrey - then + if ! grep -i 'POSTGREY_TEXT' /etc/default/postgrey; then printf 'POSTGREY_TEXT=\"%s\"\n\n' "${POSTGREY_TEXT}" >>/etc/default/postgrey fi - if [[ -f /tmp/docker-mailserver/whitelist_clients.local ]] - then + if [[ -f /tmp/docker-mailserver/whitelist_clients.local ]]; then cp -f /tmp/docker-mailserver/whitelist_clients.local /etc/postgrey/whitelist_clients.local fi - if [[ -f /tmp/docker-mailserver/whitelist_recipients ]] - then + if [[ -f /tmp/docker-mailserver/whitelist_recipients ]]; then cp -f /tmp/docker-mailserver/whitelist_recipients /etc/postgrey/whitelist_recipients fi else - _log 'debug' 'Postscreen is disabled' + _log 'debug' 'Postgrey is disabled' fi } -function __setup__security__postscreen -{ +function __setup__security__postscreen() { _log 'debug' 'Configuring Postscreen' sed -i \ -e "s|postscreen_dnsbl_action = enforce|postscreen_dnsbl_action = ${POSTSCREEN_ACTION}|" \ -e "s|postscreen_greet_action = enforce|postscreen_greet_action = ${POSTSCREEN_ACTION}|" \ -e "s|postscreen_bare_newline_action = enforce|postscreen_bare_newline_action = ${POSTSCREEN_ACTION}|" /etc/postfix/main.cf - if [[ ${ENABLE_DNSBL} -eq 0 ]] - then + if [[ ${ENABLE_DNSBL} -eq 0 ]]; then _log 'debug' 'Disabling Postscreen DNSBLs' postconf 'postscreen_dnsbl_action = ignore' postconf 'postscreen_dnsbl_sites = ' @@ -74,12 +66,17 @@ function __setup__security__postscreen fi } -function __setup__security__spamassassin -{ - if [[ ${ENABLE_SPAMASSASSIN} -eq 1 ]] - then +function __setup__security__spamassassin() { + if [[ ${ENABLE_AMAVIS} -ne 1 && ${ENABLE_SPAMASSASSIN} -eq 1 ]]; then + _log 'warn' 'Spamassassin does not work when Amavis is disabled. Enable Amavis to fix it.' + ENABLE_SPAMASSASSIN=0 + fi + + if [[ ${ENABLE_SPAMASSASSIN} -eq 1 ]]; then _log 'debug' 'Enabling and configuring SpamAssassin' + # Maintainers should take care in attempting to change these sed commands. Alternatives were already explored: + # https://github.com/docker-mailserver/docker-mailserver/pull/3767#issuecomment-1885989591 # shellcheck disable=SC2016 sed -i -r 's|^\$sa_tag_level_deflt (.*);|\$sa_tag_level_deflt = '"${SA_TAG}"';|g' /etc/amavis/conf.d/20-debian_defaults @@ -89,43 +86,30 @@ function __setup__security__spamassassin # shellcheck disable=SC2016 sed -i -r 's|^\$sa_kill_level_deflt (.*);|\$sa_kill_level_deflt = '"${SA_KILL}"';|g' /etc/amavis/conf.d/20-debian_defaults - # fix cron.daily for spamassassin - sed -i \ - 's|invoke-rc.d spamassassin reload|/etc/init\.d/spamassassin reload|g' \ - /etc/cron.daily/spamassassin - - if [[ ${SA_SPAM_SUBJECT} == 'undef' ]] - then - # shellcheck disable=SC2016 - sed -i -r 's|^\$sa_spam_subject_tag (.*);|\$sa_spam_subject_tag = undef;|g' /etc/amavis/conf.d/20-debian_defaults - else - # shellcheck disable=SC2016 - sed -i -r 's|^\$sa_spam_subject_tag (.*);|\$sa_spam_subject_tag = '"'${SA_SPAM_SUBJECT}'"';|g' /etc/amavis/conf.d/20-debian_defaults - fi + # disable rewriting the subject as this is handles by _setup_spam_subject (which uses Dovecot Sieve) + # shellcheck disable=SC2016 + sed -i -r 's|^\$sa_spam_subject_tag (.*);|\$sa_spam_subject_tag = undef;|g' /etc/amavis/conf.d/20-debian_defaults # activate short circuits when SA BAYES is certain it has spam or ham. - if [[ ${SA_SHORTCIRCUIT_BAYES_SPAM} -eq 1 ]] - then + if [[ ${SA_SHORTCIRCUIT_BAYES_SPAM} -eq 1 ]]; then # automatically activate the Shortcircuit Plugin sed -i -r 's|^# loadplugin Mail::SpamAssassin::Plugin::Shortcircuit|loadplugin Mail::SpamAssassin::Plugin::Shortcircuit|g' /etc/spamassassin/v320.pre sed -i -r 's|^# shortcircuit BAYES_99|shortcircuit BAYES_99|g' /etc/spamassassin/local.cf fi - if [[ ${SA_SHORTCIRCUIT_BAYES_HAM} -eq 1 ]] - then + if [[ ${SA_SHORTCIRCUIT_BAYES_HAM} -eq 1 ]]; then # automatically activate the Shortcircuit Plugin sed -i -r 's|^# loadplugin Mail::SpamAssassin::Plugin::Shortcircuit|loadplugin Mail::SpamAssassin::Plugin::Shortcircuit|g' /etc/spamassassin/v320.pre sed -i -r 's|^# shortcircuit BAYES_00|shortcircuit BAYES_00|g' /etc/spamassassin/local.cf fi - if [[ -e /tmp/docker-mailserver/spamassassin-rules.cf ]] - then + if [[ -e /tmp/docker-mailserver/spamassassin-rules.cf ]]; then cp /tmp/docker-mailserver/spamassassin-rules.cf /etc/spamassassin/ fi - if [[ ${SPAMASSASSIN_SPAM_TO_INBOX} -eq 1 ]] - then + if [[ ${SPAMASSASSIN_SPAM_TO_INBOX} -eq 1 ]]; then _log 'trace' 'Configuring Spamassassin/Amavis to send SPAM to inbox' + _log 'debug' "'SPAMASSASSIN_SPAM_TO_INBOX=1' is set. The 'SA_KILL' ENV will be ignored." sed -i "s|\$final_spam_destiny.*=.*$|\$final_spam_destiny = D_PASS;|g" /etc/amavis/conf.d/49-docker-mailserver sed -i "s|\$final_bad_header_destiny.*=.*$|\$final_bad_header_destiny = D_PASS;|g" /etc/amavis/conf.d/49-docker-mailserver @@ -136,8 +120,7 @@ function __setup__security__spamassassin sed -i "s|\$final_bad_header_destiny.*=.*$|\$final_bad_header_destiny = D_BOUNCE;|g" /etc/amavis/conf.d/49-docker-mailserver fi - if [[ ${ENABLE_SPAMASSASSIN_KAM} -eq 1 ]] - then + if [[ ${ENABLE_SPAMASSASSIN_KAM} -eq 1 ]]; then _log 'trace' 'Configuring Spamassassin KAM' local SPAMASSASSIN_KAM_CRON_FILE=/etc/cron.daily/spamassassin_kam @@ -150,8 +133,7 @@ RESULT=$(sa-update --gpgkey 24C063D8 --channel kam.sa-channels.mcgrail.com 2>&1) EXIT_CODE=${?} # see https://spamassassin.apache.org/full/3.1.x/doc/sa-update.html#exit_codes -if [[ ${EXIT_CODE} -ge 4 ]] -then +if [[ ${EXIT_CODE} -ge 4 ]]; then echo -e "Updating SpamAssassin KAM failed:\n${RESULT}\n" >&2 exit 1 fi @@ -169,25 +151,28 @@ EOF fi } -function __setup__security__clamav -{ - if [[ ${ENABLE_CLAMAV} -eq 1 ]] - then +function __setup__security__clamav() { + if [[ ${ENABLE_CLAMAV} -eq 1 ]]; then _log 'debug' 'Enabling and configuring ClamAV' - local FILE - for FILE in /var/log/mail/{clamav,freshclam}.log - do - touch "${FILE}" - chown clamav:adm "${FILE}" - chmod 640 "${FILE}" - done - - if [[ ${CLAMAV_MESSAGE_SIZE_LIMIT} != '25M' ]] - then + if [[ ${CLAMAV_MESSAGE_SIZE_LIMIT} != '25M' ]]; then _log 'trace' "Setting ClamAV message scan size limit to '${CLAMAV_MESSAGE_SIZE_LIMIT}'" - sedfile -i \ - "s/^MaxFileSize.*/MaxFileSize ${CLAMAV_MESSAGE_SIZE_LIMIT}/" \ + + # do a short sanity check: ClamAV does not support setting a maximum size greater than 4000M (at all) + if [[ $(numfmt --from=si "${CLAMAV_MESSAGE_SIZE_LIMIT}") -gt $(numfmt --from=si 4000M) ]]; then + _log 'warn' "You set 'CLAMAV_MESSAGE_SIZE_LIMIT' to a value larger than 4 Gigabyte, but the maximum value is 4000M for this value - you should correct your configuration" + fi + # For more details, see + # https://github.com/docker-mailserver/docker-mailserver/pull/3341 + if [[ $(numfmt --from=si "${CLAMAV_MESSAGE_SIZE_LIMIT}") -ge $(numfmt --from=iec 2G) ]]; then + _log 'warn' "You set 'CLAMAV_MESSAGE_SIZE_LIMIT' to a value larger than 2 Gibibyte but ClamAV does not scan files larger or equal to 2 Gibibyte" + fi + + sedfile -i -E \ + "s|^(MaxFileSize).*|\1 ${CLAMAV_MESSAGE_SIZE_LIMIT}|" \ + /etc/clamav/clamd.conf + sedfile -i -E \ + "s|^(MaxScanSize).*|\1 ${CLAMAV_MESSAGE_SIZE_LIMIT}|" \ /etc/clamav/clamd.conf fi else @@ -197,41 +182,40 @@ function __setup__security__clamav fi } -function __setup__security__fail2ban -{ - if [[ ${ENABLE_FAIL2BAN} -eq 1 ]] - then +function __setup__security__fail2ban() { + if [[ ${ENABLE_FAIL2BAN} -eq 1 ]]; then _log 'debug' 'Enabling and configuring Fail2Ban' - if [[ -e /tmp/docker-mailserver/fail2ban-fail2ban.cf ]] - then + if [[ -e /tmp/docker-mailserver/fail2ban-fail2ban.cf ]]; then + _log 'trace' 'Custom fail2ban-fail2ban.cf found' cp /tmp/docker-mailserver/fail2ban-fail2ban.cf /etc/fail2ban/fail2ban.local fi - if [[ -e /tmp/docker-mailserver/fail2ban-jail.cf ]] - then + if [[ -e /tmp/docker-mailserver/fail2ban-jail.cf ]]; then + _log 'trace' 'Custom fail2ban-jail.cf found' cp /tmp/docker-mailserver/fail2ban-jail.cf /etc/fail2ban/jail.d/user-jail.local fi - if [[ ${FAIL2BAN_BLOCKTYPE} != 'reject' ]] - then + if [[ ${FAIL2BAN_BLOCKTYPE} != 'reject' ]]; then + _log 'trace' "Setting fail2ban blocktype to 'drop'" echo -e '[Init]\nblocktype = drop' >/etc/fail2ban/action.d/nftables-common.local fi echo '[Definition]' >/etc/fail2ban/filter.d/custom.conf + + _log 'trace' 'Configuring fail2ban logrotate rotate count and interval' + [[ ${LOGROTATE_COUNT} -ne 4 ]] && sedfile -i "s|rotate 4$|rotate ${LOGROTATE_COUNT}|" /etc/logrotate.d/fail2ban + [[ ${LOGROTATE_INTERVAL} != "weekly" ]] && sedfile -i "s|weekly$|${LOGROTATE_INTERVAL}|" /etc/logrotate.d/fail2ban else _log 'debug' 'Fail2Ban is disabled' rm -f /etc/logrotate.d/fail2ban fi } -function __setup__security__amavis -{ - if [[ ${ENABLE_AMAVIS} -eq 1 ]] - then +function __setup__security__amavis() { + if [[ ${ENABLE_AMAVIS} -eq 1 ]]; then _log 'debug' 'Configuring Amavis' - if [[ -f /tmp/docker-mailserver/amavis.cf ]] - then + if [[ -f /tmp/docker-mailserver/amavis.cf ]]; then cp /tmp/docker-mailserver/amavis.cf /etc/amavis/conf.d/50-user fi @@ -252,14 +236,108 @@ function __setup__security__amavis mv /etc/cron.d/amavisd-new /etc/cron.d/amavisd-new.disabled chmod 0 /etc/cron.d/amavisd-new.disabled - if [[ ${ENABLE_CLAMAV} -eq 1 ]] && [[ ${ENABLE_RSPAMD} -eq 0 ]] - then + if [[ ${ENABLE_CLAMAV} -eq 1 ]] && [[ ${ENABLE_RSPAMD} -eq 0 ]]; then _log 'warn' 'ClamAV will not work when Amavis & rspamd are disabled. Enable either Amavis or rspamd to fix it.' fi + fi +} + +# If `SPAM_SUBJECT` is not empty, we create a Sieve script that alters the `Subject` +# header, in order to prepend a user-defined string. +function _setup_spam_subject() { + if [[ -z ${SPAM_SUBJECT} ]] + then + _log 'debug' 'Spam subject is not set - no prefix will be added to spam e-mails' + else + _log 'debug' "Spam subject is set - the prefix '${SPAM_SUBJECT}' will be added to spam e-mails" + + _log 'trace' "Enabling '+editheader' Sieve extension" + # check whether sieve_global_extensions is disabled (and enabled it if so) + sed -i -E 's|#(sieve_global_extensions.*)|\1|' /etc/dovecot/conf.d/90-sieve.conf + # then append the extension + sedfile -i -E 's|(sieve_global_extensions.*)|\1 +editheader|' /etc/dovecot/conf.d/90-sieve.conf + + _log 'trace' "Adding global (before) Sieve script for subject rewrite" + # This directory contains Sieve scripts that are executed before user-defined Sieve + # scripts run. + local DOVECOT_SIEVE_GLOBAL_BEFORE_DIR='/usr/lib/dovecot/sieve-global/before' + local DOVECOT_SIEVE_FILE='spam_subject' + readonly DOVECOT_SIEVE_GLOBAL_BEFORE_DIR DOVECOT_SIEVE_FILE + + mkdir -p "${DOVECOT_SIEVE_GLOBAL_BEFORE_DIR}" + # ref: https://superuser.com/a/1502589 + cat >"${DOVECOT_SIEVE_GLOBAL_BEFORE_DIR}/${DOVECOT_SIEVE_FILE}.sieve" << EOF +require ["editheader","variables"]; + +if anyof (header :contains "X-Spam-Flag" "YES", + header :contains "X-Spam" "Yes") +{ + # Match the entire subject ... + if header :matches "Subject" "*" { + # ... to get it in a match group that can then be stored in a variable: + set "subject" "\${1}"; + } + + # We can't "replace" a header, but we can delete (all instances of) it and + # re-add (a single instance of) it: + deleteheader "Subject"; + + # Note that the header is added ":last" (so it won't appear before possible + # "Received" headers). + addheader :last "Subject" "${SPAM_SUBJECT}\${subject}"; +} +EOF + + sievec "${DOVECOT_SIEVE_GLOBAL_BEFORE_DIR}/${DOVECOT_SIEVE_FILE}.sieve" + chown dovecot:root "${DOVECOT_SIEVE_GLOBAL_BEFORE_DIR}/${DOVECOT_SIEVE_FILE}."{sieve,svbin} + fi +} + +# We can use Sieve to move spam emails to the "Junk" folder. +function _setup_spam_to_junk() { + if [[ ${MOVE_SPAM_TO_JUNK} -eq 1 ]]; then + _log 'debug' 'Spam emails will be moved to the Junk folder' + mkdir -p /usr/lib/dovecot/sieve-global/after/ + cat >/usr/lib/dovecot/sieve-global/after/spam_to_junk.sieve << EOF +require ["fileinto","special-use"]; + +if anyof (header :contains "X-Spam-Flag" "YES", + header :contains "X-Spam" "Yes") { + fileinto :specialuse "\\\\Junk" "Junk"; +} +EOF + sievec /usr/lib/dovecot/sieve-global/after/spam_to_junk.sieve + chown dovecot:root /usr/lib/dovecot/sieve-global/after/spam_to_junk.{sieve,svbin} + + if [[ ${ENABLE_SPAMASSASSIN} -eq 1 ]] && [[ ${SPAMASSASSIN_SPAM_TO_INBOX} -eq 0 ]]; then + _log 'warn' "'SPAMASSASSIN_SPAM_TO_INBOX=0' but it is required to be 1 for 'MOVE_SPAM_TO_JUNK=1' to work" + fi + else + _log 'debug' 'Spam emails will not be moved to the Junk folder' + fi +} - if [[ ${ENABLE_SPAMASSASSIN} -eq 1 ]] - then - _log 'warn' 'Spamassassin will not work when Amavis is disabled. Enable Amavis to fix it.' +function _setup_spam_mark_as_read() { + if [[ ${MARK_SPAM_AS_READ} -eq 1 ]]; then + _log 'debug' 'Spam emails will be marked as read' + mkdir -p /usr/lib/dovecot/sieve-global/after/ + + # Header support: `X-Spam-Flag` (SpamAssassin), `X-Spam` (Rspamd) + cat >/usr/lib/dovecot/sieve-global/after/spam_mark_as_read.sieve << EOF +require ["mailbox","imap4flags"]; + +if anyof (header :contains "X-Spam-Flag" "YES", + header :contains "X-Spam" "Yes") { + setflag "\\\\Seen"; +} +EOF + sievec /usr/lib/dovecot/sieve-global/after/spam_mark_as_read.sieve + chown dovecot:root /usr/lib/dovecot/sieve-global/after/spam_mark_as_read.{sieve,svbin} + + if [[ ${ENABLE_SPAMASSASSIN} -eq 1 ]] && [[ ${SPAMASSASSIN_SPAM_TO_INBOX} -eq 0 ]]; then + _log 'warn' "'SPAMASSASSIN_SPAM_TO_INBOX=0' but it is required to be 1 for 'MARK_SPAM_AS_READ=1' to work" fi + else + _log 'debug' 'Spam emails will not be marked as read' fi } diff --git a/target/scripts/startup/setup.d/security/rspamd.sh b/target/scripts/startup/setup.d/security/rspamd.sh index a476bd4652c..7651b8abb63 100644 --- a/target/scripts/startup/setup.d/security/rspamd.sh +++ b/target/scripts/startup/setup.d/security/rspamd.sh @@ -1,16 +1,33 @@ #!/bin/bash -function _setup_rspamd -{ - if [[ ${ENABLE_RSPAMD} -eq 1 ]] - then - _log 'warn' 'Rspamd integration is work in progress - expect (breaking) changes at any time' - _log 'debug' 'Enabling and configuring Rspamd' +# This file is executed during startup of DMS. Hence, the `index.sh` helper has already +# been sourced, and thus, all helper functions from `rspamd.sh` are available. - __rspamd__preflight_checks - __rspamd__adjust_postfix_configuration - __rspamd__disable_default_modules - __rspamd__handle_modules_configuration +# Function called during global setup to handle the complete setup of Rspamd. Functions +# with a single `_` prefix are sourced from the `rspamd.sh` helper. +function _setup_rspamd() { + if _env_var_expect_zero_or_one 'ENABLE_RSPAMD' && [[ ${ENABLE_RSPAMD} -eq 1 ]]; then + _log 'debug' 'Enabling and configuring Rspamd' + __rspamd__log 'trace' '---------- Setup started ----------' + + _rspamd_get_envs # must run first + __rspamd__run_early_setup_and_checks # must run second + __rspamd__setup_logfile + __rspamd__setup_redis + __rspamd__setup_postfix + __rspamd__setup_clamav + __rspamd__setup_default_modules + __rspamd__setup_learning + __rspamd__setup_greylisting + __rspamd__setup_hfilter_group + __rspamd__setup_neural + __rspamd__setup_check_authenticated + _rspamd_handle_user_modules_adjustments # must run last + + # only performing checks, no further setup handled from here onwards + __rspamd__check_dkim_permissions + + __rspamd__log 'trace' '---------- Setup finished ----------' else _log 'debug' 'Rspamd is disabled' fi @@ -23,45 +40,107 @@ function _setup_rspamd # @param ${2} = message function __rspamd__log { _log "${1:-}" "(Rspamd setup) ${2:-}" ; } -# Run miscellaneous checks against the current configuration so we can -# properly handle integration into ClamAV, etc. +# Helper for explicitly enabling or disabling a specific module. # -# This will also check whether Amavis is enabled and emit a warning as -# we discourage users from running Amavis & Rspamd at the same time. -function __rspamd__preflight_checks -{ - touch /var/lib/rspamd/stats.ucl +# @param ${1} = module name +# @param ${2} = `true` when you want to enable the module (default), +# `false` when you want to disable the module [OPTIONAL] +# @param ${3} = whether to use `local` (default) or `override` [OPTIONAL] +function __rspamd__helper__enable_disable_module() { + local MODULE=${1:?Module name must be provided} + local ENABLE_MODULE=${2:-true} + local LOCAL_OR_OVERRIDE=${3:-local} + local MESSAGE='Enabling' + + readonly MODULE ENABLE_MODULE LOCAL_OR_OVERRIDE + + if [[ ! ${ENABLE_MODULE} =~ ^(true|false)$ ]]; then + __rspamd__log 'warn' "__rspamd__helper__enable_disable_module got non-boolean argument for deciding whether module should be enabled or not" + return 1 + fi + + [[ ${ENABLE_MODULE} == true ]] || MESSAGE='Disabling' + + __rspamd__log 'trace' "${MESSAGE} module '${MODULE}'" + cat >"/etc/rspamd/${LOCAL_OR_OVERRIDE}.d/${MODULE}.conf" << EOF +# documentation: https://rspamd.com/doc/modules/${MODULE}.html + +enabled = ${ENABLE_MODULE}; + +EOF +} + +# Run miscellaneous early setup tasks and checks, such as creating files needed at runtime +# or checking for other anti-spam/anti-virus software. +function __rspamd__run_early_setup_and_checks() { + mkdir -p /var/lib/rspamd/ + : >/var/lib/rspamd/stats.ucl - if [[ ${ENABLE_AMAVIS} -eq 1 ]] || [[ ${ENABLE_SPAMASSASSIN} -eq 1 ]] - then + # Copy if directory exists and is not empty + if [[ -d ${RSPAMD_DMS_OVERRIDE_D} ]] && [[ -z $(find "${RSPAMD_DMS_OVERRIDE_D}" -maxdepth 0 -empty) ]]; then + cp "${RSPAMD_DMS_OVERRIDE_D}/"* "${RSPAMD_OVERRIDE_D}" + fi + + if [[ ${ENABLE_AMAVIS} -eq 1 ]] || [[ ${ENABLE_SPAMASSASSIN} -eq 1 ]]; then __rspamd__log 'warn' 'Running Amavis/SA & Rspamd at the same time is discouraged' fi - if [[ ${ENABLE_CLAMAV} -eq 1 ]] - then - __rspamd__log 'debug' 'Enabling ClamAV integration' - sedfile -i -E 's|^(enabled).*|\1 = true;|g' /etc/rspamd/local.d/antivirus.conf - # RSpamd uses ClamAV's UNIX socket, and to be able to read it, it must be in the same group - usermod -a -G clamav _rspamd - else - __rspamd__log 'debug' 'Rspamd will not use ClamAV (which has not been enabled)' + if [[ ${ENABLE_OPENDKIM} -eq 1 ]]; then + __rspamd__log 'warn' 'Running OpenDKIM & Rspamd at the same time is discouraged - we recommend Rspamd for DKIM checks (enabled with Rspamd by default) & signing' + fi + + if [[ ${ENABLE_OPENDMARC} -eq 1 ]]; then + __rspamd__log 'warn' 'Running OpenDMARC & Rspamd at the same time is discouraged - we recommend Rspamd for DMARC checks (enabled with Rspamd by default)' + fi + + if [[ ${ENABLE_POLICYD_SPF} -eq 1 ]]; then + __rspamd__log 'warn' 'Running policyd-spf & Rspamd at the same time is discouraged - we recommend Rspamd for SPF checks (enabled with Rspamd by default)' fi - if [[ ${ENABLE_RSPAMD_REDIS} -eq 1 ]] - then - __rspamd__log 'trace' 'Internal Redis is enabled, adding configuration' - cat >/etc/rspamd/local.d/redis.conf << "EOF" + if [[ ${ENABLE_POSTGREY} -eq 1 ]] && [[ ${RSPAMD_GREYLISTING} -eq 1 ]]; then + __rspamd__log 'warn' 'Running Postgrey & Rspamd at the same time is discouraged - we recommend Rspamd for greylisting' + fi +} + +# Keep in sync with `target/scripts/startup/setup.d/log.sh:_setup_logrotate()` +function __rspamd__setup_logfile() { + cat >/etc/logrotate.d/rspamd << EOF +/var/log/mail/rspamd.log +{ + compress + copytruncate + delaycompress + rotate ${LOGROTATE_COUNT} + ${LOGROTATE_INTERVAL} +} +EOF +} + +# Sets up Redis. In case the user does not use a dedicated Redis instance, we +# supply a configuration for our local Redis instance which is started later. +function __rspamd__setup_redis() { + if _env_var_expect_zero_or_one 'ENABLE_RSPAMD_REDIS' && [[ ${ENABLE_RSPAMD_REDIS} -eq 1 ]]; then + __rspamd__log 'debug' 'Internal Redis is enabled, adding configuration' + cat >"${RSPAMD_LOCAL_D}/redis.conf" << "EOF" # documentation: https://rspamd.com/doc/configuration/redis.html servers = "127.0.0.1:6379"; -expand_keys = true; EOF - # Here we adjust the Redis default configuration that we supply to Redis - # when starting it. Note that `/var/lib/redis/` is linked to - # `/var/mail-state/redis/` (for persisting it) if `ONE_DIR=1`. - sedfile -i -E \ + # We do not use `{{HOSTNAME}}` but only `{{COMPRESS}}` to better support + # Kubernetes, see https://github.com/orgs/docker-mailserver/discussions/3922 + cat >"${RSPAMD_LOCAL_D}/history_redis.conf" << "EOF" +# documentation: https://rspamd.com/doc/modules/history_redis.html + +key_prefix = "rs_history{{COMPRESS}}"; + +EOF + + # Here we adjust the Redis default configuration that we supply to Redis when starting it. + # NOTE: `/var/lib/redis/` is symlinked to `/var/mail-state/redis/` when DMS is started + # with a volume mounted to `/var/mail-state/` for data persistence. + sedfile -i -E \ -e 's|^(bind).*|\1 127.0.0.1|g' \ -e 's|^(daemonize).*|\1 no|g' \ -e 's|^(port).*|\1 6379|g' \ @@ -75,153 +154,199 @@ EOF fi } -# Adjust Postfix's configuration files. Append Rspamd at the end of -# `smtpd_milters` in `main.cf`. -function __rspamd__adjust_postfix_configuration -{ - postconf 'rspamd_milter = inet:localhost:11332' +# Adjust Postfix's configuration files. We only need to append Rspamd at the end of +# `smtpd_milters` in `/etc/postfix/main.cf`. +function __rspamd__setup_postfix() { + __rspamd__log 'debug' "Adjusting Postfix's configuration" + postconf 'rspamd_milter = inet:localhost:11332' # shellcheck disable=SC2016 - sed -i -E 's|^(smtpd_milters =.*)|\1 \$rspamd_milter|g' /etc/postfix/main.cf + _add_to_or_update_postfix_main 'smtpd_milters' '$rspamd_milter' } -# Helper for explicitly enabling or disabling a specific module. -# -# @param ${1} = module name -# @param ${2} = `true` when you want to enable the module (default), -# `false` when you want to disable the module [OPTIONAL] -# @param ${3} = whether to use `local` (default) or `override` [OPTIONAL] -function __rspamd__enable_disable_module -{ - local MODULE=${1:?Module name must be provided} - local ENABLE_MODULE=${2:-true} - local LOCAL_OR_OVERRIDE=${3:-local} - local MESSAGE='Enabling' +# If ClamAV is enabled, we will integrate it into Rspamd. +function __rspamd__setup_clamav() { + if _env_var_expect_zero_or_one 'ENABLE_CLAMAV' && [[ ${ENABLE_CLAMAV} -eq 1 ]]; then + __rspamd__log 'debug' 'Enabling ClamAV integration' + sedfile -i -E 's|^(enabled).*|\1 = true;|g' "${RSPAMD_LOCAL_D}/antivirus.conf" + # Rspamd uses ClamAV's UNIX socket, and to be able to read it, it must be in the same group + usermod -a -G clamav _rspamd - if [[ ! ${ENABLE_MODULE} =~ ^(true|false)$ ]] - then - __rspamd__log 'warn' "__rspamd__enable_disable_module got non-boolean argument for deciding whether module should be enabled or not" - return 1 + if [[ ${CLAMAV_MESSAGE_SIZE_LIMIT} != '25M' ]]; then + local SIZE_IN_BYTES + SIZE_IN_BYTES=$(numfmt --from=si "${CLAMAV_MESSAGE_SIZE_LIMIT}") + __rspamd__log 'trace' "Adjusting maximum size for ClamAV to ${SIZE_IN_BYTES} bytes (${CLAMAV_MESSAGE_SIZE_LIMIT})" + sedfile -i -E "s|(.*max_size =).*|\1 ${SIZE_IN_BYTES};|" "${RSPAMD_LOCAL_D}/antivirus.conf" + fi + else + __rspamd__log 'debug' 'Rspamd will not use ClamAV (which has not been enabled)' fi - - [[ ${ENABLE_MODULE} == true ]] || MESSAGE='Disabling' - - __rspamd__log 'trace' "${MESSAGE} module '${MODULE}'" - cat >"/etc/rspamd/${LOCAL_OR_OVERRIDE}.d/${MODULE}.conf" << EOF -# documentation: https://rspamd.com/doc/modules/${MODULE}.html - -enabled = ${ENABLE_MODULE}; - -EOF } # Disables certain modules by default. This can be overwritten by the user later. # We disable the modules listed in `DISABLE_MODULES` as we believe these modules # are not commonly used and the average user does not need them. As a consequence, # disabling them saves resources. -function __rspamd__disable_default_modules -{ +function __rspamd__setup_default_modules() { + __rspamd__log 'debug' 'Disabling default modules' + + # This array contains all the modules we disable by default. They + # can be re-enabled later (in `__rspamd__handle_user_modules_adjustments`) + # with `rspamd-modules.conf`. local DISABLE_MODULES=( clickhouse elastic - greylist - neural reputation spamassassin url_redirector metric_exporter ) - for MODULE in "${DISABLE_MODULES[@]}" - do - __rspamd__enable_disable_module "${MODULE}" 'false' + readonly -a DISABLE_MODULES + local MODULE + for MODULE in "${DISABLE_MODULES[@]}"; do + __rspamd__helper__enable_disable_module "${MODULE}" 'false' done } -# Parses `RSPAMD_CUSTOM_COMMANDS_FILE` and executed the directives given by the file. -# To get a detailed explanation of the commands and how the file works, visit -# https://docker-mailserver.github.io/docker-mailserver/edge/config/security/rspamd/#with-the-help-of-a-custom-file -function __rspamd__handle_modules_configuration -{ - # Adds an option with a corresponding value to a module, or, in case the option - # is already present, overwrites it. - # - # @param ${1} = file name in /etc/rspamd/override.d/ - # @param ${2} = module name as it should appear in the log - # @patam ${3} = option name in the module - # @param ${4} = value of the option - # - # ## Note - # - # While this function is currently bound to the scope of `__rspamd__handle_modules_configuration`, - # it is written in a versatile way (taking 4 arguments instead of assuming `ARGUMENT2` / `ARGUMENT3` - # are set) so that it may be used elsewhere if needed. - function __add_or_replace - { - local MODULE_FILE=${1:?Module file name must be provided} - local MODULE_LOG_NAME=${2:?Module log name must be provided} - local OPTION=${3:?Option name must be provided} - local VALUE=${4:?Value belonging to an option must be provided} - # remove possible whitespace at the end (e.g., in case ${ARGUMENT3} is empty) - VALUE=${VALUE% } - - local FILE="/etc/rspamd/override.d/${MODULE_FILE}" - [[ -f ${FILE} ]] || touch "${FILE}" - - if grep -q -E "${OPTION}.*=.*" "${FILE}" - then - __rspamd__log 'trace' "Overwriting option '${OPTION}' with value '${VALUE}' for ${MODULE_LOG_NAME}" - sed -i -E "s|([[:space:]]*${OPTION}).*|\1 = ${VALUE};|g" "${FILE}" - else - __rspamd__log 'trace' "Setting option '${OPTION}' for ${MODULE_LOG_NAME} to '${VALUE}'" - echo "${OPTION} = ${VALUE};" >>"${FILE}" - fi - } - - local RSPAMD_CUSTOM_COMMANDS_FILE='/tmp/docker-mailserver/rspamd-modules.conf' - if [[ -f "${RSPAMD_CUSTOM_COMMANDS_FILE}" ]] - then - __rspamd__log 'debug' "Found file 'rspamd-modules.conf' - parsing and applying it" - - while read -r COMMAND ARGUMENT1 ARGUMENT2 ARGUMENT3 - do - case "${COMMAND}" in +# This function sets up intelligent learning of Junk, by +# +# 1. enabling auto-learn for the classifier-bayes module +# 2. setting up sieve scripts that detect when a user is moving e-mail +# from or to the "Junk" folder, and learning them as ham or spam. +function __rspamd__setup_learning() { + if _env_var_expect_zero_or_one 'RSPAMD_LEARN' && [[ ${RSPAMD_LEARN} -eq 1 ]]; then + __rspamd__log 'debug' 'Setting up intelligent learning of spam and ham' + + local SIEVE_PIPE_BIN_DIR='/usr/lib/dovecot/sieve-pipe' + readonly SIEVE_PIPE_BIN_DIR + ln -s "$(type -f -P rspamc)" "${SIEVE_PIPE_BIN_DIR}/rspamc" + + sedfile -i -E 's|(mail_plugins =.*)|\1 imap_sieve|' /etc/dovecot/conf.d/20-imap.conf + sedfile -i -E '/^}/d' /etc/dovecot/conf.d/90-sieve.conf + cat >>/etc/dovecot/conf.d/90-sieve.conf << EOF + + # From anywhere to Junk + imapsieve_mailbox1_name = Junk + imapsieve_mailbox1_causes = COPY APPEND + imapsieve_mailbox1_before = file:${SIEVE_PIPE_BIN_DIR}/learn-spam.sieve + + # From Junk to Inbox + imapsieve_mailbox2_name = INBOX + imapsieve_mailbox2_from = Junk + imapsieve_mailbox2_causes = COPY APPEND + imapsieve_mailbox2_before = file:${SIEVE_PIPE_BIN_DIR}/learn-ham.sieve +} +EOF - ('disable-module') - __rspamd__enable_disable_module "${ARGUMENT1}" 'false' 'override' - ;; + cat >"${SIEVE_PIPE_BIN_DIR}/learn-spam.sieve" << EOF +require ["vnd.dovecot.pipe", "copy", "imapsieve"]; +pipe :copy "rspamc" ["-h", "127.0.0.1:11334", "learn_spam"]; +EOF - ('enable-module') - __rspamd__enable_disable_module "${ARGUMENT1}" 'true' 'override' - ;; + cat >"${SIEVE_PIPE_BIN_DIR}/learn-ham.sieve" << EOF +require ["vnd.dovecot.pipe", "copy", "imapsieve"]; +pipe :copy "rspamc" ["-h", "127.0.0.1:11334", "learn_ham"]; +EOF - ('set-option-for-module') - __add_or_replace "${ARGUMENT1}.conf" "module '${ARGUMENT1}'" "${ARGUMENT2}" "${ARGUMENT3}" - ;; + sievec "${SIEVE_PIPE_BIN_DIR}/learn-spam.sieve" + sievec "${SIEVE_PIPE_BIN_DIR}/learn-ham.sieve" + else + __rspamd__log 'debug' 'Intelligent learning of spam and ham is disabled' + fi +} - ('set-option-for-controller') - __add_or_replace 'worker-controller.inc' 'controller worker' "${ARGUMENT1}" "${ARGUMENT2} ${ARGUMENT3}" - ;; +# Sets up greylisting with the greylisting module (see +# https://rspamd.com/doc/modules/greylisting.html). +function __rspamd__setup_greylisting() { + if _env_var_expect_zero_or_one 'RSPAMD_GREYLISTING' && [[ ${RSPAMD_GREYLISTING} -eq 1 ]]; then + __rspamd__log 'debug' 'Enabling greylisting' + sedfile -i -E "s|(enabled =).*|\1 true;|g" "${RSPAMD_LOCAL_D}/greylist.conf" + else + __rspamd__log 'debug' 'Greylisting is disabled' + fi +} - ('set-option-for-proxy') - __add_or_replace 'worker-proxy.inc' 'proxy worker' "${ARGUMENT1}" "${ARGUMENT2} ${ARGUMENT3}" - ;; +# This function handles setup of the Hfilter module (see +# https://www.rspamd.com/doc/modules/hfilter.html). This module is mainly +# used for hostname checks, and whether or not a reverse-DNS check +# succeeds. +function __rspamd__setup_hfilter_group() { + local MODULE_FILE="${RSPAMD_LOCAL_D}/hfilter_group.conf" + readonly MODULE_FILE + if _env_var_expect_zero_or_one 'RSPAMD_HFILTER' && [[ ${RSPAMD_HFILTER} -eq 1 ]]; then + __rspamd__log 'debug' 'Hfilter (group) module is enabled' + # Check if we received a number first + if _env_var_expect_integer 'RSPAMD_HFILTER_HOSTNAME_UNKNOWN_SCORE' \ + && [[ ${RSPAMD_HFILTER_HOSTNAME_UNKNOWN_SCORE} -ne 6 ]]; then + __rspamd__log 'trace' "Adjusting score for 'HFILTER_HOSTNAME_UNKNOWN' in Hfilter group module to ${RSPAMD_HFILTER_HOSTNAME_UNKNOWN_SCORE}" + sed -i -E \ + "s|(.*score =).*(# __TAG__HFILTER_HOSTNAME_UNKNOWN)|\1 ${RSPAMD_HFILTER_HOSTNAME_UNKNOWN_SCORE}; \2|g" \ + "${MODULE_FILE}" + else + __rspamd__log 'trace' "Not adjusting score for 'HFILTER_HOSTNAME_UNKNOWN' in Hfilter group module" + fi + else + __rspamd__log 'debug' 'Disabling Hfilter (group) module' + rm -f "${MODULE_FILE}" + fi +} - ('set-common-option') - __add_or_replace 'options.inc' 'common options' "${ARGUMENT1}" "${ARGUMENT2} ${ARGUMENT3}" - ;; - ('add-line') - __rspamd__log 'trace' "Adding complete line to '${ARGUMENT1}'" - echo "${ARGUMENT2} ${ARGUMENT3:-}" >>"/etc/rspamd/override.d/${ARGUMENT1}" - ;; +# This function handles setup of the neural module (see +# https://www.rspamd.com/doc/modules/neural.html). This module is experimental +# but can enhance anti-spam scoring possibly. +function __rspamd__setup_neural() { + if _env_var_expect_zero_or_one 'RSPAMD_NEURAL' && [[ ${RSPAMD_NEURAL} -eq 1 ]]; then + __rspamd__log 'debug' 'Enabling Neural module' + __rspamd__log 'warn' 'The Neural module is still experimental (in Rspamd) and hence not tested in DMS' + else + __rspamd__log 'debug' 'Neural module is disabled' + rm -f "${RSPAMD_LOCAL_D}/neural.conf" + rm -f "${RSPAMD_LOCAL_D}/neural_group.conf" + __rspamd__helper__enable_disable_module 'neural' 'false' + fi +} - (*) - __rspamd__log 'warn' "Command '${COMMAND}' is invalid" - continue - ;; - esac - done < <(_get_valid_lines_from_file "${RSPAMD_CUSTOM_COMMANDS_FILE}") +# If 'RSPAMD_CHECK_AUTHENTICATED' is enabled, then content checks for all users, i.e. +# also for authenticated users, are performed. +# +# The default that DMS ships does not check authenticated users. In case the checks are +# enabled, this function will remove the part of the Rspamd configuration that disables +# checks for authenticated users. +function __rspamd__setup_check_authenticated() { + local MODULE_FILE="${RSPAMD_LOCAL_D}/settings.conf" + readonly MODULE_FILE + if _env_var_expect_zero_or_one 'RSPAMD_CHECK_AUTHENTICATED' \ + && [[ ${RSPAMD_CHECK_AUTHENTICATED} -eq 0 ]]; then + __rspamd__log 'debug' 'Content checks for authenticated users are disabled' + else + __rspamd__log 'debug' 'Enabling content checks for authenticated users' + sed -i -E \ + '/DMS::SED_TAG::1::START/{:a;N;/DMS::SED_TAG::1::END/!ba};/authenticated/d' \ + "${MODULE_FILE}" fi } + +# This function performs a simple check on the queried rspamd DKIM configuration: +# - Acquire all private key file locations and check whether they exist and can be accessed by Rspamd. +# - We are not checking paths that contain the '$' symbol. +function __rspamd__check_dkim_permissions() { + local KEY_FILE + while read -r KEY_FILE; do + if [[ -f ${KEY_FILE} ]]; then + __rspamd__log 'trace' "Checking DKIM file '${KEY_FILE}'" + # See https://serverfault.com/a/829314 for an explanation on `-exec false {} +` + # We additionally resolve symbolic links to check the permissions of the actual files + if find "$(realpath -L "${KEY_FILE}")" \( -user _rspamd -or -group _rspamd -or -perm -o=r \) \ + -exec false {} +; then + __rspamd__log 'warn' "Rspamd DKIM private key file '${KEY_FILE}' does not appear to have correct permissions/ownership for Rspamd to use it" + else + __rspamd__log 'trace' "DKIM file '${KEY_FILE}' permissions and ownership appear correct" + fi + else + __rspamd__log 'warn' "Rspamd DKIM private key file '${KEY_FILE}' is configured for usage, but does not appear to exist" + fi + done < <(rspamadm configdump dkim_signing | grep 'path =' | grep -v -F '$' | awk '{print $3}' | tr -d ';"') +} diff --git a/target/scripts/startup/setup.d/security/spoofing.sh b/target/scripts/startup/setup.d/security/spoofing.sh index b6b4f63f11d..ffefb2797b1 100644 --- a/target/scripts/startup/setup.d/security/spoofing.sh +++ b/target/scripts/startup/setup.d/security/spoofing.sh @@ -1,22 +1,20 @@ #!/bin/bash -function _setup_spoof_protection -{ - if [[ ${SPOOF_PROTECTION} -eq 1 ]] - then +function _setup_spoof_protection() { + if [[ ${SPOOF_PROTECTION} -eq 1 ]]; then _log 'trace' 'Enabling and configuring spoof protection' - if [[ ${ACCOUNT_PROVISIONER} == 'LDAP' ]] - then - if [[ -z ${LDAP_QUERY_FILTER_SENDERS} ]] - then + if [[ ${ACCOUNT_PROVISIONER} == 'LDAP' ]]; then + if [[ -z ${LDAP_QUERY_FILTER_SENDERS} ]]; then postconf 'smtpd_sender_login_maps = ldap:/etc/postfix/ldap-users.cf ldap:/etc/postfix/ldap-aliases.cf ldap:/etc/postfix/ldap-groups.cf' else postconf 'smtpd_sender_login_maps = ldap:/etc/postfix/ldap-senders.cf' fi else - if [[ -f /etc/postfix/regexp ]] - then + # NOTE: This file is always created at startup, it potentially has content added. + # TODO: From section: "SPOOF_PROTECTION=1 handling for smtpd_sender_login_maps" + # https://github.com/docker-mailserver/docker-mailserver/issues/2819#issue-1402114383 + if [[ -f /etc/postfix/regexp ]]; then postconf 'smtpd_sender_login_maps = unionmap:{ texthash:/etc/postfix/virtual, hash:/etc/aliases, pcre:/etc/postfix/maps/sender_login_maps.pcre, pcre:/etc/postfix/regexp }' else postconf 'smtpd_sender_login_maps = texthash:/etc/postfix/virtual, hash:/etc/aliases, pcre:/etc/postfix/maps/sender_login_maps.pcre' diff --git a/target/scripts/startup/setup.d/vmail-id.sh b/target/scripts/startup/setup.d/vmail-id.sh new file mode 100644 index 00000000000..2b692169afd --- /dev/null +++ b/target/scripts/startup/setup.d/vmail-id.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +function _setup_vmail_id() { + if [[ "${DMS_VMAIL_UID}" != "5000" ]]; then + _log 'debug' "Setting 'docker' UID to ${DMS_VMAIL_UID}" + usermod --uid "${DMS_VMAIL_UID}" docker + fi + if [[ "${DMS_VMAIL_GID}" != "5000" ]]; then + _log 'debug' "Setting 'docker' GID to ${DMS_VMAIL_GID}" + groupmod --gid "${DMS_VMAIL_GID}" docker + fi +} diff --git a/target/scripts/startup/variables-stack.sh b/target/scripts/startup/variables-stack.sh index 20a94847438..e41df37ddf6 100644 --- a/target/scripts/startup/variables-stack.sh +++ b/target/scripts/startup/variables-stack.sh @@ -3,22 +3,40 @@ # shellcheck disable=SC2034 declare -A VARS -function _early_variables_setup -{ +function _early_variables_setup() { + __ensure_valid_log_level + __environment_variables_from_files _obtain_hostname_and_domainname __environment_variables_backwards_compatibility __environment_variables_general_setup + + [[ ${ACCOUNT_PROVISIONER} == 'LDAP' ]] && __environment_variables_ldap + [[ ${ENABLE_OAUTH2} -eq 1 ]] && __environment_variables_oauth2 + [[ ${ENABLE_SASLAUTHD} -eq 1 ]] && __environment_variables_saslauthd + + __environment_variables_export } # This function handles variables that are deprecated. This allows a # smooth transition period, without the need of removing a variable # completely with a single version. -function __environment_variables_backwards_compatibility -{ - if [[ ${ENABLE_LDAP:-0} -eq 1 ]] - then - _log 'warn' "'ENABLE_LDAP=1' is deprecated (and will be removed in v13.0.0) => use 'ACCOUNT_PROVISIONER=LDAP' instead" - ACCOUNT_PROVISIONER='LDAP' +function __environment_variables_backwards_compatibility() { + if [[ ${ENABLE_LDAP:-0} -eq 1 ]]; then + _log 'error' "'ENABLE_LDAP=1' has been changed to 'ACCOUNT_PROVISIONER=LDAP' since DMS v13" + fi + + # Dovecot and SASLAuthd have applied an 'ldap://' fallback for compatibility since v10 (June 2021) + # This was silently applied, but users should be explicit: + if [[ ${LDAP_SERVER_HOST:-'://'} != *'://'* ]] \ + || [[ ${DOVECOT_URIS:-'://'} != *'://'* ]] \ + || [[ ${SASLAUTHD_LDAP_SERVER:-'://'} != *'://'* ]]; then + _log 'error' "The ENV for which LDAP host to connect to must include the URI scheme ('ldap://', 'ldaps://', 'ldapi://')" + fi + + if [[ -n ${SA_SPAM_SUBJECT:-} ]]; then + _log 'warn' "'SA_SPAM_SUBJECT' has been renamed to 'SPAM_SUBJECT' - this warning will block startup on v15.0.0" + _log 'info' "Copying value of 'SA_SPAM_SUBJECT' into 'SPAM_SUBJECT' if 'SPAM_SUBJECT' has not been set explicitly" + SPAM_SUBJECT=${SPAM_SUBJECT:-${SA_SPAM_SUBJECT}} fi # TODO this can be uncommented in a PR handling the HOSTNAME/DOMAINNAME issue @@ -33,8 +51,7 @@ function __environment_variables_backwards_compatibility # This function sets almost all environment variables. This involves setting # a default if no value was provided and writing the variable and its value # to the VARS map. -function __environment_variables_general_setup -{ +function __environment_variables_general_setup() { _log 'debug' 'Handling general environment variable setup' # these variables must be defined first @@ -43,6 +60,10 @@ function __environment_variables_general_setup VARS[POSTMASTER_ADDRESS]="${POSTMASTER_ADDRESS:=postmaster@${DOMAINNAME}}" VARS[REPORT_RECIPIENT]="${REPORT_RECIPIENT:=${POSTMASTER_ADDRESS}}" VARS[REPORT_SENDER]="${REPORT_SENDER:=mailserver-report@${HOSTNAME}}" + VARS[DMS_VMAIL_UID]="${DMS_VMAIL_UID:=5000}" + VARS[DMS_VMAIL_GID]="${DMS_VMAIL_GID:=5000}" + + # user-customizable are next _log 'trace' 'Setting anti-spam & anti-virus environment variables' @@ -50,13 +71,20 @@ function __environment_variables_general_setup VARS[CLAMAV_MESSAGE_SIZE_LIMIT]="${CLAMAV_MESSAGE_SIZE_LIMIT:=25M}" VARS[FAIL2BAN_BLOCKTYPE]="${FAIL2BAN_BLOCKTYPE:=drop}" VARS[MOVE_SPAM_TO_JUNK]="${MOVE_SPAM_TO_JUNK:=1}" + VARS[MARK_SPAM_AS_READ]="${MARK_SPAM_AS_READ:=0}" VARS[POSTGREY_AUTO_WHITELIST_CLIENTS]="${POSTGREY_AUTO_WHITELIST_CLIENTS:=5}" VARS[POSTGREY_DELAY]="${POSTGREY_DELAY:=300}" VARS[POSTGREY_MAX_AGE]="${POSTGREY_MAX_AGE:=35}" VARS[POSTGREY_TEXT]="${POSTGREY_TEXT:=Delayed by Postgrey}" VARS[POSTSCREEN_ACTION]="${POSTSCREEN_ACTION:=enforce}" - VARS[SA_KILL]=${SA_KILL:="6.31"} - VARS[SA_SPAM_SUBJECT]=${SA_SPAM_SUBJECT:="***SPAM*** "} + VARS[RSPAMD_CHECK_AUTHENTICATED]="${RSPAMD_CHECK_AUTHENTICATED:=0}" + VARS[RSPAMD_GREYLISTING]="${RSPAMD_GREYLISTING:=0}" + VARS[RSPAMD_HFILTER]="${RSPAMD_HFILTER:=1}" + VARS[RSPAMD_HFILTER_HOSTNAME_UNKNOWN_SCORE]="${RSPAMD_HFILTER_HOSTNAME_UNKNOWN_SCORE:=6}" + VARS[RSPAMD_NEURAL]="${RSPAMD_NEURAL:=0}" + VARS[RSPAMD_LEARN]="${RSPAMD_LEARN:=0}" + VARS[SA_KILL]=${SA_KILL:="10.0"} + VARS[SPAM_SUBJECT]=${SPAM_SUBJECT:=} VARS[SA_TAG]=${SA_TAG:="2.0"} VARS[SA_TAG2]=${SA_TAG2:="6.31"} VARS[SPAMASSASSIN_SPAM_TO_INBOX]="${SPAMASSASSIN_SPAM_TO_INBOX:=1}" @@ -70,12 +98,15 @@ function __environment_variables_general_setup VARS[ENABLE_DNSBL]="${ENABLE_DNSBL:=0}" VARS[ENABLE_FAIL2BAN]="${ENABLE_FAIL2BAN:=0}" VARS[ENABLE_FETCHMAIL]="${ENABLE_FETCHMAIL:=0}" + VARS[ENABLE_GETMAIL]="${ENABLE_GETMAIL:=0}" VARS[ENABLE_MANAGESIEVE]="${ENABLE_MANAGESIEVE:=0}" + VARS[ENABLE_OAUTH2]="${ENABLE_OAUTH2:=0}" VARS[ENABLE_OPENDKIM]="${ENABLE_OPENDKIM:=1}" VARS[ENABLE_OPENDMARC]="${ENABLE_OPENDMARC:=1}" + VARS[ENABLE_POLICYD_SPF]="${ENABLE_POLICYD_SPF:=1}" VARS[ENABLE_POP3]="${ENABLE_POP3:=0}" + VARS[ENABLE_IMAP]="${ENABLE_IMAP:=1}" VARS[ENABLE_POSTGREY]="${ENABLE_POSTGREY:=0}" - VARS[ENABLE_QUOTAS]="${ENABLE_QUOTAS:=1}" VARS[ENABLE_RSPAMD]="${ENABLE_RSPAMD:=0}" VARS[ENABLE_RSPAMD_REDIS]="${ENABLE_RSPAMD_REDIS:=${ENABLE_RSPAMD}}" VARS[ENABLE_SASLAUTHD]="${ENABLE_SASLAUTHD:=0}" @@ -102,10 +133,11 @@ function __environment_variables_general_setup VARS[DOVECOT_MAILBOX_FORMAT]="${DOVECOT_MAILBOX_FORMAT:=maildir}" VARS[DOVECOT_TLS]="${DOVECOT_TLS:=no}" + VARS[POSTFIX_DAGENT]="${POSTFIX_DAGENT:=}" VARS[POSTFIX_INET_PROTOCOLS]="${POSTFIX_INET_PROTOCOLS:=all}" VARS[POSTFIX_MAILBOX_SIZE_LIMIT]="${POSTFIX_MAILBOX_SIZE_LIMIT:=0}" VARS[POSTFIX_MESSAGE_SIZE_LIMIT]="${POSTFIX_MESSAGE_SIZE_LIMIT:=10240000}" # ~10 MB - VARS[POSTFIX_DAGENT]="${POSTFIX_DAGENT:=}" + VARS[POSTFIX_REJECT_UNKNOWN_CLIENT_HOSTNAME]="${POSTFIX_REJECT_UNKNOWN_CLIENT_HOSTNAME:=0}" _log 'trace' 'Setting SRS specific environment variables' @@ -117,14 +149,16 @@ function __environment_variables_general_setup _log 'trace' 'Setting miscellaneous environment variables' VARS[ACCOUNT_PROVISIONER]="${ACCOUNT_PROVISIONER:=FILE}" + VARS[DMS_CONFIG_POLL]="${DMS_CONFIG_POLL:=2}" VARS[FETCHMAIL_PARALLEL]="${FETCHMAIL_PARALLEL:=0}" VARS[FETCHMAIL_POLL]="${FETCHMAIL_POLL:=300}" + VARS[GETMAIL_POLL]="${GETMAIL_POLL:=5}" VARS[LOG_LEVEL]="${LOG_LEVEL:=info}" VARS[LOGROTATE_INTERVAL]="${LOGROTATE_INTERVAL:=weekly}" + VARS[LOGROTATE_COUNT]="${LOGROTATE_COUNT:=4}" VARS[LOGWATCH_INTERVAL]="${LOGWATCH_INTERVAL:=none}" VARS[LOGWATCH_RECIPIENT]="${LOGWATCH_RECIPIENT:=${REPORT_RECIPIENT}}" VARS[LOGWATCH_SENDER]="${LOGWATCH_SENDER:=${REPORT_SENDER}}" - VARS[ONE_DIR]="${ONE_DIR:=1}" VARS[PERMIT_DOCKER]="${PERMIT_DOCKER:=none}" VARS[PFLOGSUMM_RECIPIENT]="${PFLOGSUMM_RECIPIENT:=${REPORT_RECIPIENT}}" VARS[PFLOGSUMM_SENDER]="${PFLOGSUMM_SENDER:=${REPORT_SENDER}}" @@ -133,11 +167,31 @@ function __environment_variables_general_setup VARS[SUPERVISOR_LOGLEVEL]="${SUPERVISOR_LOGLEVEL:=warn}" VARS[TZ]="${TZ:=}" VARS[UPDATE_CHECK_INTERVAL]="${UPDATE_CHECK_INTERVAL:=1d}" + + _log 'trace' 'Setting environment variables that require other variables to be set first' + + # The Dovecot Quotas feature is presently only supported with the default FILE account provisioner, + # Enforce disabling the feature, unless it's been explicitly set via ENV (to avoid mismatch between + # explicit ENV and sourcing from /etc/dms-settings) + if [[ ${ACCOUNT_PROVISIONER} != 'FILE' || ${SMTP_ONLY} -eq 1 ]] && [[ ${ENABLE_QUOTAS:-1} -eq 1 ]]; then + _log 'debug' "The 'ENABLE_QUOTAS' feature is enabled (by default) but is not compatible with your config. Disabling" + VARS[ENABLE_QUOTAS]="${ENABLE_QUOTAS:=0}" + else + VARS[ENABLE_QUOTAS]="${ENABLE_QUOTAS:=1}" + fi +} + +# `LOG_LEVEL` must be set early to correctly filter calls to `scripts/helpers/log.sh:_log()` +function __ensure_valid_log_level() { + if [[ ! ${LOG_LEVEL:-info} =~ ^(trace|debug|info|warn|error)$ ]]; then + _log 'warn' "Log level '${LOG_LEVEL}' is invalid (falling back to default: 'info')" + LOG_LEVEL='info' + fi } # This function handles environment variables related to LDAP. -function _environment_variables_ldap -{ +# NOTE: SASLAuthd and Dovecot LDAP support inherit these common ENV. +function __environment_variables_ldap() { _log 'debug' 'Setting LDAP-related environment variables now' VARS[LDAP_BIND_DN]="${LDAP_BIND_DN:=}" @@ -147,77 +201,33 @@ function _environment_variables_ldap VARS[LDAP_START_TLS]="${LDAP_START_TLS:=no}" } -# This function handles environment variables related to SASLAUTHD -# and, if activated, variables related to SASLAUTHD and LDAP. -function _environment_variables_saslauthd -{ - _log 'debug' 'Setting SASLAUTHD-related environment variables now' - - VARS[SASLAUTHD_MECHANISMS]="${SASLAUTHD_MECHANISMS:=pam}" - - # SASL ENV for configuring an LDAP specific - # `saslauthd.conf` via `setup-stack.sh:_setup_sasulauthd()` - if [[ ${ACCOUNT_PROVISIONER} == 'LDAP' ]] - then - _log 'trace' 'Setting SASLSAUTH-LDAP variables nnow' - - VARS[SASLAUTHD_LDAP_AUTH_METHOD]="${SASLAUTHD_LDAP_AUTH_METHOD:=bind}" - VARS[SASLAUTHD_LDAP_BIND_DN]="${SASLAUTHD_LDAP_BIND_DN:=${LDAP_BIND_DN}}" - VARS[SASLAUTHD_LDAP_FILTER]="${SASLAUTHD_LDAP_FILTER:=(&(uniqueIdentifier=%u)(mailEnabled=TRUE))}" - VARS[SASLAUTHD_LDAP_PASSWORD]="${SASLAUTHD_LDAP_PASSWORD:=${LDAP_BIND_PW}}" - VARS[SASLAUTHD_LDAP_SEARCH_BASE]="${SASLAUTHD_LDAP_SEARCH_BASE:=${LDAP_SEARCH_BASE}}" - VARS[SASLAUTHD_LDAP_SERVER]="${SASLAUTHD_LDAP_SERVER:=${LDAP_SERVER_HOST}}" - [[ ${SASLAUTHD_LDAP_SERVER} != *'://'* ]] && SASLAUTHD_LDAP_SERVER="ldap://${SASLAUTHD_LDAP_SERVER}" - VARS[SASLAUTHD_LDAP_START_TLS]="${SASLAUTHD_LDAP_START_TLS:=no}" - VARS[SASLAUTHD_LDAP_TLS_CHECK_PEER]="${SASLAUTHD_LDAP_TLS_CHECK_PEER:=no}" - - if [[ -z ${SASLAUTHD_LDAP_TLS_CACERT_FILE} ]] - then - SASLAUTHD_LDAP_TLS_CACERT_FILE='' - else - SASLAUTHD_LDAP_TLS_CACERT_FILE="ldap_tls_cacert_file: ${SASLAUTHD_LDAP_TLS_CACERT_FILE}" - fi - VARS[SASLAUTHD_LDAP_TLS_CACERT_FILE]="${SASLAUTHD_LDAP_TLS_CACERT_FILE}" +function __environment_variables_oauth2() { + _log 'debug' 'Setting OAUTH2-related environment variables now' - if [[ -z ${SASLAUTHD_LDAP_TLS_CACERT_DIR} ]] - then - SASLAUTHD_LDAP_TLS_CACERT_DIR='' - else - SASLAUTHD_LDAP_TLS_CACERT_DIR="ldap_tls_cacert_dir: ${SASLAUTHD_LDAP_TLS_CACERT_DIR}" - fi - VARS[SASLAUTHD_LDAP_TLS_CACERT_DIR]="${SASLAUTHD_LDAP_TLS_CACERT_DIR}" + VARS[OAUTH2_INTROSPECTION_URL]="${OAUTH2_INTROSPECTION_URL:=}" +} - if [[ -z ${SASLAUTHD_LDAP_PASSWORD_ATTR} ]] - then - SASLAUTHD_LDAP_PASSWORD_ATTR='' - else - SASLAUTHD_LDAP_PASSWORD_ATTR="ldap_password_attr: ${SASLAUTHD_LDAP_PASSWORD_ATTR}" - fi - VARS[SASLAUTHD_LDAP_PASSWORD_ATTR]="${SASLAUTHD_LDAP_PASSWORD_ATTR}" +# This function handles environment variables related to SASLAUTHD +# LDAP specific ENV handled in: `startup/setup.d/saslauthd.sh:_setup_saslauthd()` +function __environment_variables_saslauthd() { + _log 'debug' 'Setting SASLAUTHD-related environment variables now' - if [[ -z ${SASLAUTHD_LDAP_MECH} ]] - then - SASLAUTHD_LDAP_MECH='' - else - SASLAUTHD_LDAP_MECH="ldap_mech: ${SASLAUTHD_LDAP_MECH}" - fi - VARS[SASLAUTHD_LDAP_MECH]="${SASLAUTHD_LDAP_MECH}" - fi + # This ENV is only used by the supervisor service config `saslauth.conf`: + # NOTE: `pam` is set as the upstream default in `/etc/default/saslauthd` + VARS[SASLAUTHD_MECHANISMS]="${SASLAUTHD_MECHANISMS:=ldap}" } # This function Writes the contents of the `VARS` map (associative array) # to locations where they can be sourced from (e.g. `/etc/dms-settings`) # or where they can be used by Bash directly (e.g. `/root/.bashrc`). -function _environment_variables_export -{ +function __environment_variables_export() { _log 'debug' "Exporting environment variables now (creating '/etc/dms-settings')" : >/root/.bashrc # make DMS variables available in login shells and their subprocesses : >/etc/dms-settings # this file can be sourced by other scripts local VAR - for VAR in "${!VARS[@]}" - do + for VAR in "${!VARS[@]}"; do echo "export ${VAR}='${VARS[${VAR}]}'" >>/root/.bashrc echo "${VAR}='${VARS[${VAR}]}'" >>/etc/dms-settings done @@ -225,3 +235,34 @@ function _environment_variables_export sort -o /root/.bashrc /root/.bashrc sort -o /etc/dms-settings /etc/dms-settings } + +# This function sets any environment variable with a value from a referenced file +# when an equivalent ENV with a `__FILE` suffix exists with a valid file path as the value. +function __environment_variables_from_files() { + # Iterate through all ENV found with a `__FILE` suffix: + while read -r ENV_WITH_FILE_REF; do + # Store the value of the `__FILE` ENV: + local FILE_PATH="${!ENV_WITH_FILE_REF}" + # Store the ENV name without the `__FILE` suffix: + local TARGET_ENV_NAME="${ENV_WITH_FILE_REF/__FILE/}" + # Assign a value representing a variable name, + # `-n` will alias `TARGET_ENV` so that it is treated as if it were the referenced variable: + local -n TARGET_ENV="${TARGET_ENV_NAME}" + + # Skip if the target ENV is already set: + if [[ -v TARGET_ENV ]]; then + _log 'warn' "ENV value will not be sourced from '${ENV_WITH_FILE_REF}' since '${TARGET_ENV_NAME}' is already set" + continue + fi + + # Skip if the file path provided is invalid: + if [[ ! -f ${FILE_PATH} ]]; then + _log 'warn' "File defined for secret '${TARGET_ENV_NAME}' with path '${FILE_PATH}' does not exist" + continue + fi + + # Read the value from a file and assign it to the intended ENV: + _log 'info' "Getting secret '${TARGET_ENV_NAME}' from '${FILE_PATH}'" + TARGET_ENV="$(< "${FILE_PATH}")" + done < <(env | grep -Po '^.+?__FILE') +} diff --git a/target/scripts/update-check.sh b/target/scripts/update-check.sh index bdcc51b2977..f00bfcb3d14 100755 --- a/target/scripts/update-check.sh +++ b/target/scripts/update-check.sh @@ -3,34 +3,31 @@ # shellcheck source=./helpers/log.sh source /usr/local/bin/helpers/log.sh -VERSION=$( ${LATEST} ]" + _log 'info' "Update available [ ${VERSION} --> ${LATEST} ]" # only notify once echo "${MAIL}" | mail -s "Mailserver update available! [ ${VERSION} --> ${LATEST} ]" "${POSTMASTER_ADDRESS}" && exit 0 else - _log_with_date 'info' 'No update available' + _log 'info' 'No update available' fi else - _log_with_date 'warn' 'Update check failed' + _log 'warn' 'Update check failed' fi # check again in 'UPDATE_CHECK_INTERVAL' time diff --git a/target/supervisor/conf.d/supervisor-app.conf b/target/supervisor/conf.d/dms-services.conf similarity index 82% rename from target/supervisor/conf.d/supervisor-app.conf rename to target/supervisor/conf.d/dms-services.conf index 4c6c10d1d24..f9db838cc5b 100644 --- a/target/supervisor/conf.d/supervisor-app.conf +++ b/target/supervisor/conf.d/dms-services.conf @@ -83,8 +83,8 @@ startsecs=0 stopwaitsecs=55 autostart=false autorestart=true -stdout_logfile=/var/log/mail/mail.log -stderr_logfile=/var/log/mail/mail.log +stdout_logfile=/var/log/supervisor/%(program_name)s.log +stderr_logfile=/var/log/supervisor/%(program_name)s.log command=/usr/sbin/postgrey --inet=127.0.0.1:10023 --syslog-facility=mail --delay="%(ENV_POSTGREY_DELAY)s" --max-age="%(ENV_POSTGREY_MAX_AGE)s" --auto-whitelist-clients="%(ENV_POSTGREY_AUTO_WHITELIST_CLIENTS)s" --greylist-text="%(ENV_POSTGREY_TEXT)s" [program:amavis] @@ -94,15 +94,15 @@ autostart=false autorestart=true stdout_logfile=/var/log/supervisor/%(program_name)s.log stderr_logfile=/var/log/supervisor/%(program_name)s.log -command=/usr/sbin/amavisd-new foreground +command=/usr/sbin/amavisd foreground [program:rspamd] startsecs=0 stopwaitsecs=55 autostart=false autorestart=true -stdout_logfile=/var/log/supervisor/%(program_name)s.log -stderr_logfile=/var/log/supervisor/%(program_name)s.log +stdout_logfile=/var/log/mail/%(program_name)s.log +stderr_logfile=/var/log/mail/%(program_name)s.log command=/usr/bin/rspamd --no-fork --user=_rspamd --group=_rspamd [program:rspamd-redis] @@ -122,6 +122,7 @@ autorestart=true stdout_logfile=/var/log/supervisor/%(program_name)s.log stderr_logfile=/var/log/supervisor/%(program_name)s.log user=fetchmail +environment=HOME="/var/lib/fetchmail",USER="fetchmail" command=/usr/bin/fetchmail -f /etc/fetchmailrc --nodetach --daemon "%(ENV_FETCHMAIL_POLL)s" -i /var/lib/fetchmail/.fetchmail-UIDL-cache --pidfile /var/run/fetchmail/fetchmail.pid [program:postfix] @@ -148,7 +149,7 @@ autostart=false autorestart=true stdout_logfile=/var/log/supervisor/%(program_name)s.log stderr_logfile=/var/log/supervisor/%(program_name)s.log -command=/etc/init.d/postsrsd start +command=/bin/bash -c 'env $(grep -vE "^(#.*|\s*)$" /etc/default/postsrsd) postsrsd -e -p /var/run/postsrsd.pid' [program:update-check] startsecs=0 @@ -157,3 +158,24 @@ autostart=false stdout_logfile=/var/log/supervisor/%(program_name)s.log stderr_logfile=/var/log/supervisor/%(program_name)s.log command=/bin/bash -l -c /usr/local/bin/update-check.sh + +# Docs: https://github.com/Snawoot/postfix-mta-sts-resolver/blob/master/man/mta-sts-daemon.1.adoc +[program:mta-sts-daemon] +startsecs=0 +stopwaitsecs=55 +autostart=false +autorestart=true +stdout_logfile=/var/log/supervisor/%(program_name)s.log +stderr_logfile=/var/log/supervisor/%(program_name)s.log +command=/usr/bin/mta-sts-daemon --config /etc/mta-sts-daemon.yml +user=_mta-sts +environment=HOME=/var/lib/mta-sts + +[program:getmail] +startsecs=0 +stopwaitsecs=55 +autostart=false +autorestart=true +stdout_logfile=/var/log/supervisor/%(program_name)s.log +stderr_logfile=/var/log/supervisor/%(program_name)s.log +command=/bin/bash -l -c /usr/local/bin/getmail-service.sh diff --git a/target/supervisor/conf.d/saslauth.conf b/target/supervisor/conf.d/saslauth.conf index 508ff83c4fa..e42aa198eb2 100644 --- a/target/supervisor/conf.d/saslauth.conf +++ b/target/supervisor/conf.d/saslauth.conf @@ -7,24 +7,6 @@ stderr_logfile=/var/log/supervisor/%(program_name)s.log command=/usr/sbin/saslauthd -d -a ldap -O /etc/saslauthd.conf pidfile=/var/run/saslauthd/saslauthd.pid -[program:saslauthd_mysql] -startsecs=0 -autostart=false -autorestart=true -stdout_logfile=/var/log/supervisor/%(program_name)s.log -stderr_logfile=/var/log/supervisor/%(program_name)s.log -command=/usr/sbin/saslauthd -d -a mysql -O "%(ENV_SASLAUTHD_MECH_OPTIONS)s" -pidfile=/var/run/saslauthd/saslauthd.pid - -[program:saslauthd_pam] -startsecs=0 -autostart=false -autorestart=true -stdout_logfile=/var/log/supervisor/%(program_name)s.log -stderr_logfile=/var/log/supervisor/%(program_name)s.log -command=/usr/sbin/saslauthd -d -a pam -O "%(ENV_SASLAUTHD_MECH_OPTIONS)s" -pidfile=/var/run/saslauthd/saslauthd.pid - [program:saslauthd_rimap] startsecs=0 autostart=false @@ -33,13 +15,3 @@ stdout_logfile=/var/log/supervisor/%(program_name)s.log stderr_logfile=/var/log/supervisor/%(program_name)s.log command=/usr/sbin/saslauthd -d -a rimap -r -O "%(ENV_SASLAUTHD_MECH_OPTIONS)s" pidfile=/var/run/saslauthd/saslauthd.pid - -[program:saslauthd_shadow] -startsecs=0 -autostart=false -autorestart=true -stdout_logfile=/var/log/supervisor/%(program_name)s.log -stderr_logfile=/var/log/supervisor/%(program_name)s.log -command=/usr/sbin/saslauthd -d -a shadow -O "%(ENV_SASLAUTHD_MECH_OPTIONS)s" -pidfile=/var/run/saslauthd/saslauthd.pid - diff --git a/test/config/dsn/postfix-main.cf b/test/config/dsn/postfix-main.cf new file mode 100644 index 00000000000..1cb0db1e598 --- /dev/null +++ b/test/config/dsn/postfix-main.cf @@ -0,0 +1 @@ +smtpd_discard_ehlo_keywords = diff --git a/test/config/dsn/postfix-master.cf b/test/config/dsn/postfix-master.cf new file mode 100644 index 00000000000..bb6aad153f8 --- /dev/null +++ b/test/config/dsn/postfix-master.cf @@ -0,0 +1,2 @@ +submission/inet/smtpd_discard_ehlo_keywords=silent-discard,dsn +submissions/inet/smtpd_discard_ehlo_keywords=silent-discard,dsn diff --git a/test/config/example-opendkim/keys/localhost.localdomain/mail.txt b/test/config/example-opendkim/keys/localhost.localdomain/mail.txt index ccc08dc0195..e9c8cd1a173 100644 --- a/test/config/example-opendkim/keys/localhost.localdomain/mail.txt +++ b/test/config/example-opendkim/keys/localhost.localdomain/mail.txt @@ -1,2 +1,2 @@ mail._domainkey IN TXT ( "v=DKIM1; k=rsa; " - "p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCzUJyyhq+TeT1wlIth5Z0yr7Ohd62n4rL5X3vRJO4EDyOEicJ73cjuaU4JLTYhbqmbNalOyXE9btS9I55Gv3RyomVBD1JpVTKdjVBUQug2L/ggw2dtt1FAn99svQWMs1XxmxiTR+sCEVkgKMmLSkCJuDCIfY/Bc9nlcng9+juB8wIDAQAB" ) ; ----- DKIM key mail for localhost.localdomain + "p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCzUJyyhq+TeT1wlIth5Z0yr7Ohd62n4rL5X3vRJO4EDyOEicJ73cjuaU4JLTYhbqmbNalOyXE9btS9I55Gv3RyomVBD1JpVTKdjVBUQug2L/ggw2dtt1FAn99svQWMs1XxmxiTR+sCEVkgKMmLSkCJuDCIfY/Bc9nlcng9+juB8wIDAQAB" ) ; ----- DKIM key mail for localhost.localdomain diff --git a/test/config/example-opendkim/keys/otherdomain.tld/mail.txt b/test/config/example-opendkim/keys/otherdomain.tld/mail.txt index d132a31c1cd..9d1079f4eb3 100644 --- a/test/config/example-opendkim/keys/otherdomain.tld/mail.txt +++ b/test/config/example-opendkim/keys/otherdomain.tld/mail.txt @@ -1,2 +1,2 @@ mail._domainkey IN TXT ( "v=DKIM1; k=rsa; " - "p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCurRsOh4NyTOqDnpPlPLGlQDuoQl32Gdkfzw7BBRKDcelIZBmQf0uhXKSZVKe5Q596w/3ESJ9WOlB03SISnHy8lq/ZJ1+vhSZQfHvp0cHQl4BgNzktRCARdPY+5nVerF8aUSsT3bG2O+2r09AY4okLCVfkiwg6Nz2Eo7j4Z7mqNwIDAQAB" ) ; ----- DKIM key mail for otherdomain.tld + "p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCurRsOh4NyTOqDnpPlPLGlQDuoQl32Gdkfzw7BBRKDcelIZBmQf0uhXKSZVKe5Q596w/3ESJ9WOlB03SISnHy8lq/ZJ1+vhSZQfHvp0cHQl4BgNzktRCARdPY+5nVerF8aUSsT3bG2O+2r09AY4okLCVfkiwg6Nz2Eo7j4Z7mqNwIDAQAB" ) ; ----- DKIM key mail for otherdomain.tld diff --git a/test/config/fetchmail/fetchmail.cf b/test/config/fetchmail/fetchmail.cf index aead698cd13..11168505046 100644 --- a/test/config/fetchmail/fetchmail.cf +++ b/test/config/fetchmail/fetchmail.cf @@ -1,11 +1,11 @@ poll pop3.third-party.test. with proto POP3 - user 'remote_username' there with - password 'secret' - is 'local_username' here - options keep ssl + user 'remote_username' there with + password 'secret' + is 'local_username' here + options keep ssl poll imap.remote-service.test. with proto IMAP - user 'user3' there with - password 'secret' - is 'user3@example.test' here - options keep ssl + user 'user3' there with + password 'secret' + is 'user3@example.test' here + options keep ssl diff --git a/test/config/getmail/user3.cf b/test/config/getmail/user3.cf new file mode 100644 index 00000000000..cad73c9827a --- /dev/null +++ b/test/config/getmail/user3.cf @@ -0,0 +1,11 @@ +[retriever] +type = SimpleIMAPSSLRetriever +server = imap.remote-service.test +username = user3 +password=secret + +[destination] +type = MDA_external +path = /usr/lib/dovecot/deliver +allow_root_commands = true +arguments =("-d","user3@example.test") diff --git a/test/config/junk-mailbox/user-patches.sh b/test/config/junk-mailbox/user-patches.sh new file mode 100755 index 00000000000..0a16a21fca4 --- /dev/null +++ b/test/config/junk-mailbox/user-patches.sh @@ -0,0 +1,21 @@ +#!/bin/bash +## +# This user script will be executed between configuration and starting daemons +# To enable it you must save it in your config directory as "user-patches.sh" +## + +echo "[user-patches.sh] Adjusting 'Junk' mailbox name to verify delivery to Junk mailbox based on special-use flag instead of mailbox's name" + +sed -i -e 's/mailbox Junk/mailbox Spam/' /etc/dovecot/conf.d/15-mailboxes.conf + +### Before / After ### + +# mailbox Junk { +# auto = subscribe +# special_use = \Junk +# } + +# mailbox Spam { +# auto = subscribe +# special_use = \Junk +# } diff --git a/test/config/ldap-groups.cf b/test/config/ldap-groups.cf deleted file mode 100644 index 6712db9eb32..00000000000 --- a/test/config/ldap-groups.cf +++ /dev/null @@ -1,10 +0,0 @@ -# Testconfig for ldap integration -bind = yes -bind_dn = cn=admin,dc=domain,dc=com -bind_pw = admin -query_filter = (&(mailGroupMember=%s)(mailEnabled=TRUE)) -result_attribute = mail -search_base = ou=people,dc=domain,dc=com -server_host = mail.domain.com -start_tls = no -version = 3 diff --git a/test/config/ldap/openldap/ldifs/01_mail-tree.ldif b/test/config/ldap/openldap/ldifs/01_mail-tree.ldif new file mode 100644 index 00000000000..a6587f89b5c --- /dev/null +++ b/test/config/ldap/openldap/ldifs/01_mail-tree.ldif @@ -0,0 +1,16 @@ +# The root object of the tree, all entries will branch off this one: +dn: dc=example,dc=test +# DN is formed from `example.test` DNS labels: +# NOTE: This is just a common convention (not dependent on hostname or any external config) +objectClass: dcObject +# Must reference left most component: +dc: example +# It's required to use an `objectClass` that implements a "Structural Class": +objectClass: organization +# Value is purely descriptive, not important to tests: +o: DMS Test + +# User accounts will belong to this subtree: +dn: ou=users,dc=example,dc=test +objectClass: organizationalUnit +ou: users diff --git a/test/docker-openldap/bootstrap/ldif/02_user-email.ldif b/test/config/ldap/openldap/ldifs/02_user-email.ldif similarity index 51% rename from test/docker-openldap/bootstrap/ldif/02_user-email.ldif rename to test/config/ldap/openldap/ldifs/02_user-email.ldif index 993a4e70d2b..67696a81119 100644 --- a/test/docker-openldap/bootstrap/ldif/02_user-email.ldif +++ b/test/config/ldap/openldap/ldifs/02_user-email.ldif @@ -1,25 +1,21 @@ -# -------------------------------------------------------------------- -# Create mail accounts -# -------------------------------------------------------------------- -# Some User -dn: uniqueIdentifier=some.user,ou=people,dc=localhost,dc=localdomain -changetype: add -objectClass: organizationalPerson -objectClass: person -objectClass: top +# NOTE: A standard user account to test against +dn: uid=some.user,ou=users,dc=example,dc=test +objectClass: inetOrgPerson objectClass: PostfixBookMailAccount -objectClass: extensibleObject cn: Some User -givenName: User +givenName: Some +surname: User +userID: some.user +# Password is: secret +userPassword: {SSHA}eLtqGpid+hkSVhxvsdTPztv4uapRofGx mail: some.user@localhost.localdomain +# postfix-book.schema: mailAlias: postmaster@localhost.localdomain mailGroupMember: employees@localhost.localdomain -mailEnabled: TRUE -mailGidNumber: 5000 mailHomeDirectory: /var/mail/localhost.localdomain/some.user/ -mailQuota: 10240 mailStorageDirectory: maildir:/var/mail/localhost.localdomain/some.user/ +# postfix-book.schema generic options: +mailEnabled: TRUE mailUidNumber: 5000 -sn: Some -uniqueIdentifier: some.user -userPassword: {SSHA}eLtqGpid+hkSVhxvsdTPztv4uapRofGx +mailGidNumber: 5000 +mailQuota: 10240 diff --git a/test/docker-openldap/bootstrap/ldif/03_user-email-other-primary-domain.ldif b/test/config/ldap/openldap/ldifs/03_user-email-other-primary-domain.ldif similarity index 51% rename from test/docker-openldap/bootstrap/ldif/03_user-email-other-primary-domain.ldif rename to test/config/ldap/openldap/ldifs/03_user-email-other-primary-domain.ldif index f949349cc0c..66a84343035 100644 --- a/test/docker-openldap/bootstrap/ldif/03_user-email-other-primary-domain.ldif +++ b/test/config/ldap/openldap/ldifs/03_user-email-other-primary-domain.ldif @@ -1,25 +1,22 @@ -# -------------------------------------------------------------------- -# Create mail accounts -# -------------------------------------------------------------------- -# Some User -dn: uniqueIdentifier=some.other.user,ou=people,dc=localhost,dc=localdomain -changetype: add -objectClass: organizationalPerson -objectClass: person -objectClass: top +# NOTE: This user differs via the domain-part of their mail address +# They also have their mail directory attributes using the primary domain, not their domain-part +dn: uid=some.other.user,ou=users,dc=example,dc=test +objectClass: inetOrgPerson objectClass: PostfixBookMailAccount -objectClass: extensibleObject cn: Some Other User -givenName: Other User +givenName: Some +surname: Other User +userID: some.other.user +# Password is: secret +userPassword: {SSHA}eLtqGpid+hkSVhxvsdTPztv4uapRofGx mail: some.other.user@localhost.otherdomain +# postfix-book.schema: mailAlias: postmaster@localhost.otherdomain mailGroupMember: employees@localhost.otherdomain -mailEnabled: TRUE -mailGidNumber: 5000 mailHomeDirectory: /var/mail/localhost.localdomain/some.other.user/ -mailQuota: 10240 mailStorageDirectory: maildir:/var/mail/localhost.localdomain/some.other.user/ +# postfix-book.schema generic options: +mailEnabled: TRUE mailUidNumber: 5000 -sn: Some -uniqueIdentifier: some.other.user -userPassword: {SSHA}eLtqGpid+hkSVhxvsdTPztv4uapRofGx +mailGidNumber: 5000 +mailQuota: 10240 diff --git a/test/config/ldap/openldap/ldifs/04_user-email-different-uid.ldif b/test/config/ldap/openldap/ldifs/04_user-email-different-uid.ldif new file mode 100644 index 00000000000..e405c7b202e --- /dev/null +++ b/test/config/ldap/openldap/ldifs/04_user-email-different-uid.ldif @@ -0,0 +1,20 @@ +# NOTE: This user differs by local-part of mail address not matching their uniqueIdentifier attribute +# They also do not have any alias or groups configured +dn: uid=some.user.id,ou=users,dc=example,dc=test +objectClass: inetOrgPerson +objectClass: PostfixBookMailAccount +cn: Some User +givenName: Some +surname: User +userID: some.user.id +# Password is: secret +userPassword: {SSHA}eLtqGpid+hkSVhxvsdTPztv4uapRofGx +mail: some.user.email@localhost.localdomain +# postfix-book.schema: +mailHomeDirectory: /var/mail/localhost.localdomain/some.user.id/ +mailStorageDirectory: maildir:/var/mail/localhost.localdomain/some.user.id/ +# postfix-book.schema generic options: +mailEnabled: TRUE +mailUidNumber: 5000 +mailGidNumber: 5000 +mailQuota: 10240 diff --git a/test/config/ldap/openldap/schemas/postfix-book.ldif b/test/config/ldap/openldap/schemas/postfix-book.ldif new file mode 100644 index 00000000000..543dc61a051 --- /dev/null +++ b/test/config/ldap/openldap/schemas/postfix-book.ldif @@ -0,0 +1,14 @@ +dn: cn=postfix-book,cn=schema,cn=config +objectClass: olcSchemaConfig +cn: postfix-book +olcAttributeTypes: {0}( 1.3.6.1.4.1.29426.1.10.1 NAME 'mailHomeDirectory' DESC 'The absolute path to the mail user home directory' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE ) +olcAttributeTypes: {1}( 1.3.6.1.4.1.29426.1.10.2 NAME 'mailAlias' DESC 'RFC822 Mailbox - mail alias' EQUALITY caseIgnoreIA5Match SUBSTR caseIgnoreIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} ) +olcAttributeTypes: {2}( 1.3.6.1.4.1.29426.1.10.3 NAME 'mailUidNumber' DESC 'UID required to access the mailbox' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ) +olcAttributeTypes: {3}( 1.3.6.1.4.1.29426.1.10.4 NAME 'mailGidNumber' DESC 'GID required to access the mailbox' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ) +olcAttributeTypes: {4}( 1.3.6.1.4.1.29426.1.10.5 NAME 'mailEnabled' DESC 'TRUE to enable, FALSE to disable account' EQUALITY booleanMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE ) +olcAttributeTypes: {5}( 1.3.6.1.4.1.29426.1.10.6 NAME 'mailGroupMember' DESC 'Name of a mail distribution list' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 ) +olcAttributeTypes: {6}( 1.3.6.1.4.1.29426.1.10.7 NAME 'mailQuota' DESC 'Mail quota limit in kilobytes' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 ) +olcAttributeTypes: {7}( 1.3.6.1.4.1.29426.1.10.8 NAME 'mailStorageDirectory' DESC 'The absolute path to the mail users mailbox' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE ) +# PostfixBook object classes: +olcObjectClasses: {0}( 1.3.6.1.4.1.29426.1.2.2.1 NAME 'PostfixBookMailAccount' DESC 'Mail account used in Postfix Book' SUP top AUXILIARY MUST mail MAY ( mailHomeDirectory $ mailAlias $ mailGroupMember $ mailUidNumber $ mailGidNumber $ mailEnabled $ mailQuota $ mailStorageDirectory ) ) +olcObjectClasses: {1}( 1.3.6.1.4.1.29426.1.2.2.2 NAME 'PostfixBookMailForward' DESC 'Mail forward used in Postfix Book' SUP top AUXILIARY MUST ( mail $ mailAlias ) ) diff --git a/test/config/ldap-aliases.cf b/test/config/ldap/overrides/ldap-aliases.cf similarity index 60% rename from test/config/ldap-aliases.cf rename to test/config/ldap/overrides/ldap-aliases.cf index a4579393cae..2436aa41aa7 100644 --- a/test/config/ldap-aliases.cf +++ b/test/config/ldap/overrides/ldap-aliases.cf @@ -1,10 +1,10 @@ # Testconfig for ldap integration bind = yes -bind_dn = cn=admin,dc=domain,dc=com +bind_dn = cn=admin,dc=example,dc=test bind_pw = admin query_filter = (&(mailAlias=%s)(mailEnabled=TRUE)) result_attribute = mail -search_base = ou=people,dc=domain,dc=com -server_host = mail.domain.com +search_base = ou=users,dc=example,dc=test +server_host = mail.example.test start_tls = no version = 3 diff --git a/test/config/ldap/overrides/ldap-groups.cf b/test/config/ldap/overrides/ldap-groups.cf new file mode 100644 index 00000000000..b0b8da211ef --- /dev/null +++ b/test/config/ldap/overrides/ldap-groups.cf @@ -0,0 +1,10 @@ +# Testconfig for ldap integration +bind = yes +bind_dn = cn=admin,dc=example,dc=test +bind_pw = admin +query_filter = (&(mailGroupMember=%s)(mailEnabled=TRUE)) +result_attribute = mail +search_base = ou=users,dc=example,dc=test +server_host = mail.example.test +start_tls = no +version = 3 diff --git a/test/config/ldap-users.cf b/test/config/ldap/overrides/ldap-users.cf similarity index 60% rename from test/config/ldap-users.cf rename to test/config/ldap/overrides/ldap-users.cf index 92cd2ed5048..b32db9fd70c 100644 --- a/test/config/ldap-users.cf +++ b/test/config/ldap/overrides/ldap-users.cf @@ -1,10 +1,10 @@ # Testconfig for ldap integration bind = yes -bind_dn = cn=admin,dc=domain,dc=com +bind_dn = cn=admin,dc=example,dc=test bind_pw = admin query_filter = (&(mail=%s)(mailEnabled=TRUE)) result_attribute = mail -search_base = ou=people,dc=domain,dc=com -server_host = mail.domain.com +search_base = ou=users,dc=example,dc=test +server_host = mail.example.test start_tls = no version = 3 diff --git a/test/config/oauth2/Caddyfile b/test/config/oauth2/Caddyfile new file mode 100644 index 00000000000..f87ffc80c86 --- /dev/null +++ b/test/config/oauth2/Caddyfile @@ -0,0 +1,91 @@ +# Mocked OAuth2 /userinfo endpoint normally provided via an Authorization Server (AS) / Identity Provider (IdP) +# +# Dovecot will query the mocked `/userinfo` endpoint with the OAuth2 bearer token it was provided during login. +# If the session for the token is valid, a response returns an attribute to perform a UserDB lookup on (default: email). + +# `DMS_YWNjZXNzX3Rva2Vu` is the access token our OAuth2 tests expect for an authorization request to be successful. +# - The token was created by base64 encoding the string `access_token`, followed by adding `DMS_` as a prefix. +# - Normally an access token is a short-lived value associated to a login session. The value does not encode any real data. +# It is an opaque token: https://oauth.net/2/bearer-tokens/ + +# NOTE: The main server config is at the end within the `:80 { ... }` block. +# This is because the endpoints are extracted out into Caddy snippets, which must be defined before they're referenced. + +# /userinfo +(route-userinfo) { + vars token "DMS_YWNjZXNzX3Rva2Vu" + + # Expects to match an authorization header with a specific bearer token: + # https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#authentication_schemes + @auth header Authorization "Bearer {vars.token}" + + # If the provided authorization header has the expected value (bearer token), respond with this JSON payload: + handle @auth { + # JSON inlined via HereDoc string feature: + # Dovecot OAuth2 defaults to `username_attribute = email`, which must be returned in the response to match + # with the `user` credentials field that Dovecot received via base64 encoded IMAP `AUTHENTICATE` value. + respond <>/etc/rspamd/local.d/options.inc + +# We want Dovecot to be very detailed about what it is doing, +# specifically for Sieve because we need to check whether the +# Sieve scripts are executed so Rspamd is trained when using +# `RSPAMD_LEARN=1`. +echo 'mail_debug = yes' >>/etc/dovecot/dovecot.conf +sed -i -E '/^}/d' /etc/dovecot/conf.d/90-sieve.conf +echo -e '\n sieve_trace_debug = yes\n}' >>/etc/dovecot/conf.d/90-sieve.conf diff --git a/test/config/templates/dovecot-masters.cf b/test/config/templates/dovecot-masters.cf index e519ec75ec8..8d3f8977af2 100644 --- a/test/config/templates/dovecot-masters.cf +++ b/test/config/templates/dovecot-masters.cf @@ -1 +1 @@ -masterusername|{SHA512-CRYPT}$6$IOybywiyl1nuDno0$gRW625qH7ThmbRaByNVpuAGgDOkMd7tc3yuVmwVRuk7IXgiN8KDwcqtMcU0LyvS5RGAskbplavjPpCmFjbKEt1 +masterusername|{SHA512-CRYPT}$6$IOybywiyl1nuDno0$gRW625qH7ThmbRaByNVpuAGgDOkMd7tc3yuVmwVRuk7IXgiN8KDwcqtMcU0LyvS5RGAskbplavjPpCmFjbKEt1 diff --git a/test/config/templates/postfix-accounts.cf b/test/config/templates/postfix-accounts.cf index 03f6ef0b46b..9d538ad35a8 100644 --- a/test/config/templates/postfix-accounts.cf +++ b/test/config/templates/postfix-accounts.cf @@ -1,5 +1,5 @@ -user1@localhost.localdomain|{SHA512-CRYPT}$6$DBEbjh4I9P7aROk8$XosqE.YI2Z4bUkWD1/bedrSNpw79nsO60yiAKk04jARhPVX5VD/SaVM5HWFDQyzftESVDjbVdhzn/d4TJxFwg0 -user2@otherdomain.tld|{SHA512-CRYPT}$6$PQRkR3RRzpYP4WET$NKLJk3PkwTRRSxryqFhQloBR7qSAYjoQH/IbD1ZQKX2UJJ3jmdbOMQPfMRGXBZv3JGhDUPmAiWzoJL6/NJN5d/ -user3@localhost.localdomain|{SHA512-CRYPT}$6$lZwv0IoijHyEjDtM$vGsAS7KM5O5Q1NdWjard1LbJyGiHcqHhKAXBKDIMudjB/CuVvOvXKVy2yKeeRvKxVtkCdYac738VQPL.kpSVB.|userdb_mail=mbox:~/mail:INBOX=~/inbox +user1@localhost.localdomain|{SHA512-CRYPT}$6$DBEbjh4I9P7aROk8$XosqE.YI2Z4bUkWD1/bedrSNpw79nsO60yiAKk04jARhPVX5VD/SaVM5HWFDQyzftESVDjbVdhzn/d4TJxFwg0 +user2@otherdomain.tld|{SHA512-CRYPT}$6$PQRkR3RRzpYP4WET$NKLJk3PkwTRRSxryqFhQloBR7qSAYjoQH/IbD1ZQKX2UJJ3jmdbOMQPfMRGXBZv3JGhDUPmAiWzoJL6/NJN5d/ +user3@localhost.localdomain|{SHA512-CRYPT}$6$lZwv0IoijHyEjDtM$vGsAS7KM5O5Q1NdWjard1LbJyGiHcqHhKAXBKDIMudjB/CuVvOvXKVy2yKeeRvKxVtkCdYac738VQPL.kpSVB.|userdb_mail=mbox:~/mail:INBOX=~/inbox # this is a test comment, please don't delete me :'( - # this is also a test comment, :O + # this is also a test comment, :O diff --git a/test/docker-openldap/Dockerfile b/test/docker-openldap/Dockerfile deleted file mode 100644 index 934c498fd8c..00000000000 --- a/test/docker-openldap/Dockerfile +++ /dev/null @@ -1,5 +0,0 @@ -FROM osixia/openldap:1.1.6 -LABEL maintainer="Dennis Stumm " - -COPY bootstrap /container/service/slapd/assets/config/bootstrap -RUN rm /container/service/slapd/assets/config/bootstrap/schema/mmc/mail.schema diff --git a/test/docker-openldap/bootstrap/ldif/01_mail-tree.ldif b/test/docker-openldap/bootstrap/ldif/01_mail-tree.ldif deleted file mode 100644 index 940fef243fb..00000000000 --- a/test/docker-openldap/bootstrap/ldif/01_mail-tree.ldif +++ /dev/null @@ -1,5 +0,0 @@ -dn: ou=people,dc=localhost,dc=localdomain -changetype: add -objectClass: organizationalUnit -objectClass: top -ou: people diff --git a/test/docker-openldap/bootstrap/ldif/04_user-email-different-uid.ldif b/test/docker-openldap/bootstrap/ldif/04_user-email-different-uid.ldif deleted file mode 100644 index b991993faa9..00000000000 --- a/test/docker-openldap/bootstrap/ldif/04_user-email-different-uid.ldif +++ /dev/null @@ -1,23 +0,0 @@ -# -------------------------------------------------------------------- -# Create mail accounts -# -------------------------------------------------------------------- -# Some User -dn: uniqueIdentifier=some.user.id,ou=people,dc=localhost,dc=localdomain -changetype: add -objectClass: organizationalPerson -objectClass: person -objectClass: top -objectClass: PostfixBookMailAccount -objectClass: extensibleObject -cn: Some User -givenName: User -mail: some.user.email@localhost.localdomain -mailEnabled: TRUE -mailGidNumber: 5000 -mailHomeDirectory: /var/mail/localhost.localdomain/some.user.id/ -mailQuota: 10240 -mailStorageDirectory: maildir:/var/mail/localhost.localdomain/some.user.id/ -mailUidNumber: 5000 -sn: Some -uniqueIdentifier: some.user.id -userPassword: {SSHA}eLtqGpid+hkSVhxvsdTPztv4uapRofGx diff --git a/test/docker-openldap/bootstrap/schema/mmc/postfix-book.schema b/test/docker-openldap/bootstrap/schema/mmc/postfix-book.schema deleted file mode 100644 index 9f0d7e53479..00000000000 --- a/test/docker-openldap/bootstrap/schema/mmc/postfix-book.schema +++ /dev/null @@ -1,70 +0,0 @@ -# $Id$ -# -# State of Mind -# Private Enterprise Number: 29426 -# -# OID prefix: 1.3.6.1.4.1.29426 -# -# Attributes: 1.3.6.1.4.1.29426.1.10.x -# - - -attributetype ( 1.3.6.1.4.1.29426.1.10.1 NAME 'mailHomeDirectory' - DESC 'The absolute path to the mail user home directory' - EQUALITY caseExactIA5Match - SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE ) - -attributetype ( 1.3.6.1.4.1.29426.1.10.2 NAME 'mailAlias' - DESC 'RFC822 Mailbox - mail alias' - EQUALITY caseIgnoreIA5Match - SUBSTR caseIgnoreIA5SubstringsMatch - SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} ) - -attributetype ( 1.3.6.1.4.1.29426.1.10.3 NAME 'mailUidNumber' - DESC 'UID required to access the mailbox' - EQUALITY integerMatch - SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ) - -attributetype ( 1.3.6.1.4.1.29426.1.10.4 NAME 'mailGidNumber' - DESC 'GID required to access the mailbox' - EQUALITY integerMatch - SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ) - -attributetype ( 1.3.6.1.4.1.29426.1.10.5 NAME 'mailEnabled' - DESC 'TRUE to enable, FALSE to disable account' - EQUALITY booleanMatch - SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE ) - -attributetype ( 1.3.6.1.4.1.29426.1.10.6 NAME 'mailGroupMember' - DESC 'Name of a mail distribution list' - EQUALITY caseExactIA5Match - SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 ) - -attributetype ( 1.3.6.1.4.1.29426.1.10.7 NAME 'mailQuota' - DESC 'Mail quota limit in kilobytes' - EQUALITY caseExactIA5Match - SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 ) - -attributetype ( 1.3.6.1.4.1.29426.1.10.8 NAME 'mailStorageDirectory' - DESC 'The absolute path to the mail users mailbox' - EQUALITY caseExactIA5Match - SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE ) - - -# -# Objects: 1.3.6.1.4.1.29426.1.2.2.x -# - -objectclass ( 1.3.6.1.4.1.29426.1.2.2.1 NAME 'PostfixBookMailAccount' - SUP top AUXILIARY - DESC 'Mail account used in Postfix Book' - MUST ( mail ) - MAY ( mailHomeDirectory $ mailAlias $ mailGroupMember - $ mailUidNumber $ mailGidNumber $ mailEnabled - $ mailQuota $mailStorageDirectory ) ) - -objectclass ( 1.3.6.1.4.1.29426.1.2.2.2 NAME 'PostfixBookMailForward' - SUP top AUXILIARY - DESC 'Mail forward used in Postfix Book' - MUST ( mail $ mailAlias )) - diff --git a/test/test-files/auth/added-imap-auth.txt b/test/files/auth/added-imap-auth.txt similarity index 100% rename from test/test-files/auth/added-imap-auth.txt rename to test/files/auth/added-imap-auth.txt diff --git a/test/test-files/auth/added-pop3-auth.txt b/test/files/auth/added-pop3-auth.txt similarity index 100% rename from test/test-files/auth/added-pop3-auth.txt rename to test/files/auth/added-pop3-auth.txt diff --git a/test/test-files/auth/imap-auth.txt b/test/files/auth/imap-auth.txt similarity index 100% rename from test/test-files/auth/imap-auth.txt rename to test/files/auth/imap-auth.txt diff --git a/test/test-files/auth/imap-ldap-auth.txt b/test/files/auth/imap-ldap-auth.txt similarity index 100% rename from test/test-files/auth/imap-ldap-auth.txt rename to test/files/auth/imap-ldap-auth.txt diff --git a/test/test-files/auth/pop3-auth.txt b/test/files/auth/pop3-auth.txt similarity index 100% rename from test/test-files/auth/pop3-auth.txt rename to test/files/auth/pop3-auth.txt diff --git a/test/test-files/email-templates/amavis-virus.txt b/test/files/emails/amavis/virus.txt similarity index 83% rename from test/test-files/email-templates/amavis-virus.txt rename to test/files/emails/amavis/virus.txt index 1343a07ca8f..2c47dcad23b 100644 --- a/test/test-files/email-templates/amavis-virus.txt +++ b/test/files/emails/amavis/virus.txt @@ -1,11 +1,7 @@ -HELO mail.external.tld -MAIL FROM: virus@external.tld -RCPT TO: user1@localhost.localdomain -DATA From: Docker Mail Server To: Existing Local User Date: Sat, 22 May 2010 07:43:25 -0400 -Subject: Test Message amavis-virus.txt +Subject: Test Message amavis/virus.txt Content-type: multipart/mixed; boundary="emailboundary" MIME-version: 1.0 @@ -27,6 +23,3 @@ ACAA/4EAAAAAZWljYXIuY29tUEsFBgAAAAABAAEANwAAAGsAAAAAAA== --emailboundary-- - -. -QUIT diff --git a/test/files/emails/auth/added-smtp-auth-spoofed-alias.txt b/test/files/emails/auth/added-smtp-auth-spoofed-alias.txt new file mode 100644 index 00000000000..eeb68ac801c --- /dev/null +++ b/test/files/emails/auth/added-smtp-auth-spoofed-alias.txt @@ -0,0 +1,5 @@ +From: user1_alias +To: Existing Local User +Date: Sat, 22 May 2010 07:43:25 -0400 +Subject: Test Message +This is a test mail. diff --git a/test/test-files/auth/added-smtp-auth-spoofed.txt b/test/files/emails/auth/added-smtp-auth-spoofed.txt similarity index 53% rename from test/test-files/auth/added-smtp-auth-spoofed.txt rename to test/files/emails/auth/added-smtp-auth-spoofed.txt index 279b6c0eb3b..fd96d40132a 100644 --- a/test/test-files/auth/added-smtp-auth-spoofed.txt +++ b/test/files/emails/auth/added-smtp-auth-spoofed.txt @@ -1,14 +1,5 @@ -EHLO mail -AUTH LOGIN YWRkZWRAbG9jYWxob3N0LmxvY2FsZG9tYWlu -bXlwYXNzd29yZA== -MAIL FROM: user2@localhost.localdomain -RCPT TO: user1@localhost.localdomain -DATA From: Not_My_Business To: Existing Local User Date: Sat, 22 May 2010 07:43:25 -0400 Subject: Test Message This is a test mail. - -. -QUIT diff --git a/test/test-files/auth/ldap-smtp-auth-spoofed-alias.txt b/test/files/emails/auth/ldap-smtp-auth-spoofed-alias.txt similarity index 57% rename from test/test-files/auth/ldap-smtp-auth-spoofed-alias.txt rename to test/files/emails/auth/ldap-smtp-auth-spoofed-alias.txt index 007b0f99b32..7453675ce59 100644 --- a/test/test-files/auth/ldap-smtp-auth-spoofed-alias.txt +++ b/test/files/emails/auth/ldap-smtp-auth-spoofed-alias.txt @@ -1,15 +1,5 @@ -EHLO mail -AUTH LOGIN -c29tZS51c2VyQGxvY2FsaG9zdC5sb2NhbGRvbWFpbg== -c2VjcmV0 -MAIL FROM: postmaster@localhost.localdomain -RCPT TO: some.user@localhost.localdomain -DATA From: alias_address To: Existing Local User Date: Sat, 22 May 2010 07:43:25 -0400 Subject: Test Message This is a test mail from ldap-smtp-auth-spoofed-alias.txt - -. -QUIT diff --git a/test/test-files/auth/ldap-smtp-auth-spoofed-sender-with-filter-exception.txt b/test/files/emails/auth/ldap-smtp-auth-spoofed-sender-with-filter-exception.txt similarity index 58% rename from test/test-files/auth/ldap-smtp-auth-spoofed-sender-with-filter-exception.txt rename to test/files/emails/auth/ldap-smtp-auth-spoofed-sender-with-filter-exception.txt index bc0447afb74..3b500bf6727 100644 --- a/test/test-files/auth/ldap-smtp-auth-spoofed-sender-with-filter-exception.txt +++ b/test/files/emails/auth/ldap-smtp-auth-spoofed-sender-with-filter-exception.txt @@ -1,15 +1,5 @@ -EHLO mail -AUTH LOGIN -c29tZS51c2VyLmVtYWlsQGxvY2FsaG9zdC5sb2NhbGRvbWFpbgo= -c2VjcmV0 -MAIL FROM: randomspoofedaddress@localhost.localdomain -RCPT TO: some.user@localhost.localdomain -DATA From: spoofed_address To: Existing Local User Date: Sat, 22 May 2010 07:43:25 -0400 Subject: Test Message This is a test mail from ldap-smtp-auth-spoofed-sender-with-filter-exception.txt - -. -QUIT diff --git a/test/test-files/auth/ldap-smtp-auth-spoofed.txt b/test/files/emails/auth/ldap-smtp-auth-spoofed.txt similarity index 53% rename from test/test-files/auth/ldap-smtp-auth-spoofed.txt rename to test/files/emails/auth/ldap-smtp-auth-spoofed.txt index cc0b164dc66..83193e17e73 100644 --- a/test/test-files/auth/ldap-smtp-auth-spoofed.txt +++ b/test/files/emails/auth/ldap-smtp-auth-spoofed.txt @@ -1,15 +1,5 @@ -EHLO mail -AUTH LOGIN -c29tZS51c2VyQGxvY2FsaG9zdC5sb2NhbGRvbWFpbg== -c2VjcmV0 -MAIL FROM: ldap@localhost.localdomain -RCPT TO: user1@localhost.localdomain -DATA From: forged_address To: Existing Local User Date: Sat, 22 May 2010 07:43:25 -0400 Subject: Test Message This is a test mail. - -. -QUIT diff --git a/test/test-files/auth/added-smtp-auth-spoofed-alias.txt b/test/files/emails/nc_raw/dsn/authenticated.txt similarity index 58% rename from test/test-files/auth/added-smtp-auth-spoofed-alias.txt rename to test/files/emails/nc_raw/dsn/authenticated.txt index 4814518376d..c187bd67b60 100644 --- a/test/test-files/auth/added-smtp-auth-spoofed-alias.txt +++ b/test/files/emails/nc_raw/dsn/authenticated.txt @@ -1,10 +1,10 @@ EHLO mail AUTH LOGIN dXNlcjFAbG9jYWxob3N0LmxvY2FsZG9tYWlu bXlwYXNzd29yZA== -MAIL FROM: alias1@localhost.localdomain -RCPT TO: user1@localhost.localdomain +MAIL FROM: user1@localhost.localdomain +RCPT TO: user1@localhost.localdomain NOTIFY=success,failure DATA -From: user1_alias +From: Existing Local User To: Existing Local User Date: Sat, 22 May 2010 07:43:25 -0400 Subject: Test Message diff --git a/test/test-files/email-templates/existing-user1.txt b/test/files/emails/nc_raw/dsn/unauthenticated.txt similarity index 74% rename from test/test-files/email-templates/existing-user1.txt rename to test/files/emails/nc_raw/dsn/unauthenticated.txt index 5ab0333f1d0..8232ea68ece 100644 --- a/test/test-files/email-templates/existing-user1.txt +++ b/test/files/emails/nc_raw/dsn/unauthenticated.txt @@ -1,11 +1,11 @@ HELO mail.external.tld MAIL FROM: user@external.tld -RCPT TO: user1@localhost.localdomain +RCPT TO: user1@localhost.localdomain NOTIFY=success,failure DATA From: Docker Mail Server To: Existing Local User Date: Sat, 22 May 2010 07:43:25 -0400 -Subject: Test Message existing-user1.txt +Subject: Test Message This is a test mail. . diff --git a/test/test-files/email-templates/postscreen.txt b/test/files/emails/nc_raw/postscreen.txt similarity index 100% rename from test/test-files/email-templates/postscreen.txt rename to test/files/emails/nc_raw/postscreen.txt diff --git a/test/test-files/email-templates/smtp-only.txt b/test/files/emails/nc_raw/smtp-only.txt similarity index 100% rename from test/test-files/email-templates/smtp-only.txt rename to test/files/emails/nc_raw/smtp-only.txt diff --git a/test/test-files/email-templates/postgrey.txt b/test/files/emails/postgrey.txt similarity index 66% rename from test/test-files/email-templates/postgrey.txt rename to test/files/emails/postgrey.txt index 33a3b153723..cdfe8f937bf 100644 --- a/test/test-files/email-templates/postgrey.txt +++ b/test/files/emails/postgrey.txt @@ -1,12 +1,5 @@ -HELO mail.external.tld -MAIL FROM: user@external.tld -RCPT TO: user1@localhost.localdomain -DATA From: Docker Mail Server To: Existing Local User Date: Sat, 22 May 2010 07:43:25 -0400 Subject: Postgrey Test Message This is a test mail. - -. -QUIT diff --git a/test/files/emails/postscreen.txt b/test/files/emails/postscreen.txt new file mode 100644 index 00000000000..732ac897abd --- /dev/null +++ b/test/files/emails/postscreen.txt @@ -0,0 +1,5 @@ +From: Docker Mail Server +To: Existing Local User +Date: Sat, 22 May 2010 07:43:25 -0400 +Subject: Test Message postscreen.txt +This is a test mail for postscreen. diff --git a/test/test-files/email-templates/send-privacy-email.txt b/test/files/emails/privacy.txt similarity index 52% rename from test/test-files/email-templates/send-privacy-email.txt rename to test/files/emails/privacy.txt index 2273a46cc4f..1d3a1b96cef 100644 --- a/test/test-files/email-templates/send-privacy-email.txt +++ b/test/files/emails/privacy.txt @@ -1,15 +1,6 @@ -EHLO mail -AUTH LOGIN dXNlcjFAbG9jYWxob3N0LmxvY2FsZG9tYWlu -bXlwYXNzd29yZA== -mail from: -rcpt to: -data From: Some User To: Some User User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:52.0) - Gecko/20100101 Thunderbird/52.2.1 + Gecko/20100101 Thunderbird/52.2.1 Subject: Test ESMTP Auth LOGIN and remove privacy This is a test mail. - -. -QUIT diff --git a/test/test-files/email-templates/quota-exceeded.txt b/test/files/emails/quota-exceeded.txt similarity index 98% rename from test/test-files/email-templates/quota-exceeded.txt rename to test/files/emails/quota-exceeded.txt index 71d221a15f5..c5281637bab 100644 --- a/test/test-files/email-templates/quota-exceeded.txt +++ b/test/files/emails/quota-exceeded.txt @@ -1,7 +1,3 @@ -HELO mail.external.tld -MAIL FROM: user@external.tld -RCPT TO: quotauser@otherdomain.tld -DATA From: Docker Mail Server To: Existing Local User Date: Sat, 22 May 2010 07:43:25 -0400 @@ -20,6 +16,3 @@ Et voluptatum nobis ut odio voluptatem et quibusdam fugit ut libero sapiente vel Sit sint obcaecati et reiciendis tenetur aut dolorum culpa. Ab veritatis maxime qui necessitatibus facilis eum voluptate asperiores non totam omnis. Nam modi officia in reiciendis odit sit rerum laudantium est rerum voluptatem ut fugit cupiditate! Sit atque sint aut delectus omnis ut asperiores enim quo reprehenderit quae! In quasi nemo ut error totam ut quia harum ut commodi tenetur? Non quod dolorum eum explicabo labore vel asperiores quas est perferendis nulla eum nemo tenetur. Ut libero blanditiis ex voluptatibus repudiandae ab reiciendis nemo id debitis impedit hic quia incidunt sed quam excepturi ut magnam odit. Qui dolor deleniti aut sunt voluptas aut blanditiis distinctio nam omnis deleniti hic omnis rerum eum magni voluptatem. Nam labore facere eum molestiae dolorum ea consectetur praesentium ut cupiditate iste ad magnam aut neque maiores! Et excepturi ducimus ut nemo voluptas eum voluptas nihil hic perferendis quos vel quasi nesciunt est praesentium dolore hic quia quis. Et maxime ducimus ea cupiditate voluptatem ad quia dolores! Sed quos quaerat vel aperiam minus non sapiente quia ut ratione dolore eum officiis rerum. Non dolor vitae qui facilis dignissimos aut voluptate odit et ullam consequuntur. Et laudantium perspiciatis sit nisi temporibus a temporibus itaque ut iure dolor a voluptatum mollitia eos officia nobis et quibusdam voluptas. Amet eligendi eos nulla corporis et blanditiis nihil vel eveniet veritatis et sunt perferendis id molestiae eius! Quo harum quod aut nemo autem ut adipisci sint sed quia sunt. Aut voluptas error ut quae perferendis eos adipisci internos. Nam rerum fugiat aut minima nostrum quo repellendus quas exercitationem tenetur. Et molestiae architecto id quibusdam reprehenderit et magnam aliquam! Quo tempora veritatis At dolorem sint ex nulla blanditiis At voluptas laudantium est molestiae exercitationem et sequi voluptates aut ipsa atque. Et animi ipsum aut atque recusandae ea nemo ullam non quisquam quos sit libero sint vel libero delectus. Eos labore quidem a velit obcaecati nam explicabo consequatur eos maxime blanditiis? Et ipsam molestiae non quia explicabo ex galisum repudiandae et tempora veniam. Sed optio repellendus ut consequatur temporibus et harum quas hic ipsa officia? Aut dolores ipsum sit nulla dignissimos id quia perferendis aut dolores dolor et quibusdam porro aut Quis consequatur. - -. -QUIT diff --git a/test/test-files/email-templates/root-email.txt b/test/files/emails/sendmail/root-email.txt similarity index 100% rename from test/test-files/email-templates/root-email.txt rename to test/files/emails/sendmail/root-email.txt diff --git a/test/test-files/email-templates/sieve-pipe.txt b/test/files/emails/sieve/pipe.txt similarity index 67% rename from test/test-files/email-templates/sieve-pipe.txt rename to test/files/emails/sieve/pipe.txt index f13dba8709b..4e8cfb39b33 100644 --- a/test/test-files/email-templates/sieve-pipe.txt +++ b/test/files/emails/sieve/pipe.txt @@ -1,12 +1,5 @@ -HELO mail.external.tld -MAIL FROM: user@external.tld -RCPT TO: user2@otherdomain.tld -DATA From: Sieve-pipe-test To: Existing Local User Date: Sat, 22 May 2010 07:43:25 -0400 Subject: Sieve pipe test message This is a test mail to sieve pipe. - -. -QUIT diff --git a/test/test-files/email-templates/sieve-spam-folder.txt b/test/files/emails/sieve/spam-folder.txt similarity index 64% rename from test/test-files/email-templates/sieve-spam-folder.txt rename to test/files/emails/sieve/spam-folder.txt index 8e802817f35..7ffd09a7dc6 100644 --- a/test/test-files/email-templates/sieve-spam-folder.txt +++ b/test/files/emails/sieve/spam-folder.txt @@ -1,12 +1,5 @@ -HELO mail.external.tld -MAIL FROM: user@external.tld -RCPT TO: user1@localhost.localdomain -DATA From: Spambot To: Existing Local User Date: Sat, 22 May 2010 07:43:25 -0400 Subject: Test Message sieve-spam-folder.txt This is a test mail. - -. -QUIT diff --git a/test/test-files/email-templates/test-email.txt b/test/files/emails/test-email.txt similarity index 100% rename from test/test-files/email-templates/test-email.txt rename to test/files/emails/test-email.txt diff --git a/test/test-files/nc_templates/imap_special_use_folders.txt b/test/files/nc/imap_special_use_folders.txt similarity index 100% rename from test/test-files/nc_templates/imap_special_use_folders.txt rename to test/files/nc/imap_special_use_folders.txt diff --git a/test/test-files/nc_templates/postgrey_whitelist.txt b/test/files/nc/postgrey_whitelist.txt similarity index 100% rename from test/test-files/nc_templates/postgrey_whitelist.txt rename to test/files/nc/postgrey_whitelist.txt diff --git a/test/test-files/nc_templates/postgrey_whitelist_recipients.txt b/test/files/nc/postgrey_whitelist_recipients.txt similarity index 100% rename from test/test-files/nc_templates/postgrey_whitelist_recipients.txt rename to test/files/nc/postgrey_whitelist_recipients.txt diff --git a/test/files/nc/rspamd_imap_move_to_inbox.txt b/test/files/nc/rspamd_imap_move_to_inbox.txt new file mode 100644 index 00000000000..b9191d627f1 --- /dev/null +++ b/test/files/nc/rspamd_imap_move_to_inbox.txt @@ -0,0 +1,4 @@ +A LOGIN user1@localhost.localdomain 123 +B SELECT Junk +A MOVE 1:1 INBOX +A LOGOUT diff --git a/test/files/nc/rspamd_imap_move_to_junk.txt b/test/files/nc/rspamd_imap_move_to_junk.txt new file mode 100644 index 00000000000..1e38081cf47 --- /dev/null +++ b/test/files/nc/rspamd_imap_move_to_junk.txt @@ -0,0 +1,4 @@ +A LOGIN user1@localhost.localdomain 123 +B SELECT INBOX +A MOVE 1:1 Junk +A LOGOUT diff --git a/test/test-files/ssl/custom-dhe-params.pem b/test/files/ssl/custom-dhe-params.pem similarity index 100% rename from test/test-files/ssl/custom-dhe-params.pem rename to test/files/ssl/custom-dhe-params.pem diff --git a/test/test-files/ssl/example.test/README.md b/test/files/ssl/example.test/README.md similarity index 86% rename from test/test-files/ssl/example.test/README.md rename to test/files/ssl/example.test/README.md index 274f633867e..e1f8cdaf46a 100644 --- a/test/test-files/ssl/example.test/README.md +++ b/test/files/ssl/example.test/README.md @@ -71,21 +71,21 @@ Certificate: X509v3 Subject Alternative Name: DNS:example.test, DNS:mail.example.test Signature Algorithm: SHA256-RSA - 50:47:7b:59:26:9d:8d:f7:e4:dc:03:94:b0:35:e4:03:b7:94: - 16:7e:b6:79:c5:bb:e7:61:db:ca:e6:22:cc:c8:a0:9f:9d:b0: - 7c:12:43:ec:a7:f3:fe:ad:0a:44:69:69:7f:c7:31:f7:3f:e8: - 98:a7:37:43:bd:fb:5b:c6:85:85:91:dc:29:23:cb:6b:a9:aa: - f0:f0:62:79:ce:43:8c:5f:28:49:ee:a1:d4:16:67:6b:59:c3: - 15:65:e3:d3:3b:35:da:59:35:33:2a:5e:8a:59:ff:14:b9:51: - a5:8e:0b:7c:1b:a1:b1:f4:89:1a:3f:2f:d7:b1:8d:23:0a:7a: - 79:e1:c2:03:b5:2f:ee:34:16:a9:67:27:b6:10:67:5d:f4:1d: - d6:b3:e0:ab:80:3d:59:fc:bc:4b:1a:55:fb:36:75:ff:e3:88: - 73:e3:16:4d:2b:17:7b:2a:21:a3:18:14:04:19:b3:b8:11:39: - 55:3f:ce:21:b7:d3:5d:8d:78:d5:3a:e0:b2:17:41:ad:3c:8e: - a5:a2:ba:eb:3d:b6:9e:2c:ef:7d:d5:cc:71:cb:07:54:21:42: - 81:79:45:2b:93:74:93:a1:c9:f1:5e:5e:11:3d:ac:df:55:98: - 37:44:d2:55:a5:15:a9:33:79:6e:fe:49:6d:e5:7b:a0:1c:12: - c5:1b:4d:33 + 50:47:7b:59:26:9d:8d:f7:e4:dc:03:94:b0:35:e4:03:b7:94: + 16:7e:b6:79:c5:bb:e7:61:db:ca:e6:22:cc:c8:a0:9f:9d:b0: + 7c:12:43:ec:a7:f3:fe:ad:0a:44:69:69:7f:c7:31:f7:3f:e8: + 98:a7:37:43:bd:fb:5b:c6:85:85:91:dc:29:23:cb:6b:a9:aa: + f0:f0:62:79:ce:43:8c:5f:28:49:ee:a1:d4:16:67:6b:59:c3: + 15:65:e3:d3:3b:35:da:59:35:33:2a:5e:8a:59:ff:14:b9:51: + a5:8e:0b:7c:1b:a1:b1:f4:89:1a:3f:2f:d7:b1:8d:23:0a:7a: + 79:e1:c2:03:b5:2f:ee:34:16:a9:67:27:b6:10:67:5d:f4:1d: + d6:b3:e0:ab:80:3d:59:fc:bc:4b:1a:55:fb:36:75:ff:e3:88: + 73:e3:16:4d:2b:17:7b:2a:21:a3:18:14:04:19:b3:b8:11:39: + 55:3f:ce:21:b7:d3:5d:8d:78:d5:3a:e0:b2:17:41:ad:3c:8e: + a5:a2:ba:eb:3d:b6:9e:2c:ef:7d:d5:cc:71:cb:07:54:21:42: + 81:79:45:2b:93:74:93:a1:c9:f1:5e:5e:11:3d:ac:df:55:98: + 37:44:d2:55:a5:15:a9:33:79:6e:fe:49:6d:e5:7b:a0:1c:12: + c5:1b:4d:33 ``` @@ -139,10 +139,10 @@ Certificate: X509v3 Subject Alternative Name: DNS:example.test, DNS:mail.example.test Signature Algorithm: ECDSA-SHA256 - 30:46:02:21:00:f8:72:3d:90:7e:db:9e:7a:4f:6d:80:fb:fa: - dc:42:43:e2:dc:8f:6a:ec:18:c5:af:e1:ea:03:fd:66:78:a2: - 01:02:21:00:f7:86:58:81:17:f5:74:5b:14:c8:0f:93:e2:bb: - b8:e9:90:47:c0:f7:b1:60:82:d9:b4:1a:fc:fa:66:fa:48:5c + 30:46:02:21:00:f8:72:3d:90:7e:db:9e:7a:4f:6d:80:fb:fa: + dc:42:43:e2:dc:8f:6a:ec:18:c5:af:e1:ea:03:fd:66:78:a2: + 01:02:21:00:f7:86:58:81:17:f5:74:5b:14:c8:0f:93:e2:bb: + b8:e9:90:47:c0:f7:b1:60:82:d9:b4:1a:fc:fa:66:fa:48:5c ``` @@ -224,10 +224,10 @@ Certificate: X509v3 Subject Alternative Name: DNS:mail.example.test Signature Algorithm: ECDSA-SHA256 - 30:46:02:21:00:ad:08:7b:f0:82:41:2e:0e:cd:2b:f7:95:fd: - ee:73:d9:93:8d:74:7c:ef:29:4d:d5:da:33:04:f0:b6:b1:6b: - 13:02:21:00:d7:f1:95:db:be:18:b8:db:77:b9:57:07:e6:b9: - 5a:3d:00:34:d3:f5:eb:18:67:9b:ba:bf:88:62:72:e9:c9:99 + 30:46:02:21:00:ad:08:7b:f0:82:41:2e:0e:cd:2b:f7:95:fd: + ee:73:d9:93:8d:74:7c:ef:29:4d:d5:da:33:04:f0:b6:b1:6b: + 13:02:21:00:d7:f1:95:db:be:18:b8:db:77:b9:57:07:e6:b9: + 5a:3d:00:34:d3:f5:eb:18:67:9b:ba:bf:88:62:72:e9:c9:99 ``` @@ -268,10 +268,10 @@ Certificate: X509v3 Subject Key Identifier: DE:90:B3:B9:4D:C1:B3:EE:77:00:88:8B:69:EC:71:C4:30:F9:F6:7F Signature Algorithm: ECDSA-SHA256 - 30:44:02:20:3f:3b:90:e7:ca:82:70:8e:3f:2e:72:2a:b9:27: - 46:ac:e9:e2:4a:db:56:02:bc:a2:b2:99:e4:8d:10:7a:d5:73: - 02:20:72:25:64:b6:1c:aa:a6:c3:14:e1:66:35:bf:a1:db:90: - ea:49:59:f9:44:e8:63:de:a8:c0:bb:9b:21:08:59:87 + 30:44:02:20:3f:3b:90:e7:ca:82:70:8e:3f:2e:72:2a:b9:27: + 46:ac:e9:e2:4a:db:56:02:bc:a2:b2:99:e4:8d:10:7a:d5:73: + 02:20:72:25:64:b6:1c:aa:a6:c3:14:e1:66:35:bf:a1:db:90: + ea:49:59:f9:44:e8:63:de:a8:c0:bb:9b:21:08:59:87 ``` @@ -337,10 +337,10 @@ Certificate: X509v3 Subject Alternative Name: DNS:*.example.test Signature Algorithm: ECDSA-SHA256 - 30:46:02:21:00:f2:50:c0:b5:c9:24:e5:e9:36:a6:7b:35:5d: - 38:a7:7d:81:af:02:fc:9d:fd:79:f4:2d:4c:8a:04:55:44:a8: - 3a:02:21:00:b1:2d:d2:25:18:2d:35:19:20:97:78:f1:d5:18: - 9f:11:d5:97:a9:dc:64:95:2a:6c:9d:4e:78:69:c1:92:23:23 + 30:46:02:21:00:f2:50:c0:b5:c9:24:e5:e9:36:a6:7b:35:5d: + 38:a7:7d:81:af:02:fc:9d:fd:79:f4:2d:4c:8a:04:55:44:a8: + 3a:02:21:00:b1:2d:d2:25:18:2d:35:19:20:97:78:f1:d5:18: + 9f:11:d5:97:a9:dc:64:95:2a:6c:9d:4e:78:69:c1:92:23:23 ``` diff --git a/test/test-files/ssl/example.test/cert.ecdsa.pem b/test/files/ssl/example.test/cert.ecdsa.pem similarity index 100% rename from test/test-files/ssl/example.test/cert.ecdsa.pem rename to test/files/ssl/example.test/cert.ecdsa.pem diff --git a/test/test-files/ssl/example.test/cert.rsa.pem b/test/files/ssl/example.test/cert.rsa.pem similarity index 100% rename from test/test-files/ssl/example.test/cert.rsa.pem rename to test/files/ssl/example.test/cert.rsa.pem diff --git a/test/test-files/ssl/example.test/key.ecdsa.pem b/test/files/ssl/example.test/key.ecdsa.pem similarity index 100% rename from test/test-files/ssl/example.test/key.ecdsa.pem rename to test/files/ssl/example.test/key.ecdsa.pem diff --git a/test/test-files/ssl/example.test/key.rsa.pem b/test/files/ssl/example.test/key.rsa.pem similarity index 100% rename from test/test-files/ssl/example.test/key.rsa.pem rename to test/files/ssl/example.test/key.rsa.pem diff --git a/test/test-files/ssl/example.test/testssl.txt b/test/files/ssl/example.test/testssl.txt similarity index 100% rename from test/test-files/ssl/example.test/testssl.txt rename to test/files/ssl/example.test/testssl.txt diff --git a/test/test-files/ssl/example.test/traefik.md b/test/files/ssl/example.test/traefik.md similarity index 100% rename from test/test-files/ssl/example.test/traefik.md rename to test/files/ssl/example.test/traefik.md diff --git a/test/test-files/ssl/example.test/with_ca/ecdsa/ca-cert.ecdsa.pem b/test/files/ssl/example.test/with_ca/ecdsa/ca-cert.ecdsa.pem similarity index 100% rename from test/test-files/ssl/example.test/with_ca/ecdsa/ca-cert.ecdsa.pem rename to test/files/ssl/example.test/with_ca/ecdsa/ca-cert.ecdsa.pem diff --git a/test/test-files/ssl/example.test/with_ca/ecdsa/ca-key.ecdsa.pem b/test/files/ssl/example.test/with_ca/ecdsa/ca-key.ecdsa.pem similarity index 100% rename from test/test-files/ssl/example.test/with_ca/ecdsa/ca-key.ecdsa.pem rename to test/files/ssl/example.test/with_ca/ecdsa/ca-key.ecdsa.pem diff --git a/test/test-files/ssl/example.test/with_ca/ecdsa/cert.ecdsa.pem b/test/files/ssl/example.test/with_ca/ecdsa/cert.ecdsa.pem similarity index 100% rename from test/test-files/ssl/example.test/with_ca/ecdsa/cert.ecdsa.pem rename to test/files/ssl/example.test/with_ca/ecdsa/cert.ecdsa.pem diff --git a/test/test-files/ssl/example.test/with_ca/ecdsa/cert.rsa.pem b/test/files/ssl/example.test/with_ca/ecdsa/cert.rsa.pem similarity index 100% rename from test/test-files/ssl/example.test/with_ca/ecdsa/cert.rsa.pem rename to test/files/ssl/example.test/with_ca/ecdsa/cert.rsa.pem diff --git a/test/test-files/ssl/example.test/with_ca/ecdsa/ecdsa.acme.json b/test/files/ssl/example.test/with_ca/ecdsa/ecdsa.acme.json similarity index 100% rename from test/test-files/ssl/example.test/with_ca/ecdsa/ecdsa.acme.json rename to test/files/ssl/example.test/with_ca/ecdsa/ecdsa.acme.json diff --git a/test/test-files/ssl/example.test/with_ca/ecdsa/key.ecdsa.pem b/test/files/ssl/example.test/with_ca/ecdsa/key.ecdsa.pem similarity index 100% rename from test/test-files/ssl/example.test/with_ca/ecdsa/key.ecdsa.pem rename to test/files/ssl/example.test/with_ca/ecdsa/key.ecdsa.pem diff --git a/test/test-files/ssl/example.test/with_ca/ecdsa/key.rsa.pem b/test/files/ssl/example.test/with_ca/ecdsa/key.rsa.pem similarity index 100% rename from test/test-files/ssl/example.test/with_ca/ecdsa/key.rsa.pem rename to test/files/ssl/example.test/with_ca/ecdsa/key.rsa.pem diff --git a/test/test-files/ssl/example.test/with_ca/ecdsa/rsa.acme.json b/test/files/ssl/example.test/with_ca/ecdsa/rsa.acme.json similarity index 100% rename from test/test-files/ssl/example.test/with_ca/ecdsa/rsa.acme.json rename to test/files/ssl/example.test/with_ca/ecdsa/rsa.acme.json diff --git a/test/test-files/ssl/example.test/with_ca/ecdsa/wildcard/cert.ecdsa.pem b/test/files/ssl/example.test/with_ca/ecdsa/wildcard/cert.ecdsa.pem similarity index 100% rename from test/test-files/ssl/example.test/with_ca/ecdsa/wildcard/cert.ecdsa.pem rename to test/files/ssl/example.test/with_ca/ecdsa/wildcard/cert.ecdsa.pem diff --git a/test/test-files/ssl/example.test/with_ca/ecdsa/wildcard/ecdsa.acme.json b/test/files/ssl/example.test/with_ca/ecdsa/wildcard/ecdsa.acme.json similarity index 100% rename from test/test-files/ssl/example.test/with_ca/ecdsa/wildcard/ecdsa.acme.json rename to test/files/ssl/example.test/with_ca/ecdsa/wildcard/ecdsa.acme.json diff --git a/test/test-files/ssl/example.test/with_ca/ecdsa/wildcard/key.ecdsa.pem b/test/files/ssl/example.test/with_ca/ecdsa/wildcard/key.ecdsa.pem similarity index 100% rename from test/test-files/ssl/example.test/with_ca/ecdsa/wildcard/key.ecdsa.pem rename to test/files/ssl/example.test/with_ca/ecdsa/wildcard/key.ecdsa.pem diff --git a/test/test-files/ssl/example.test/with_ca/rsa/ca-cert.rsa.pem b/test/files/ssl/example.test/with_ca/rsa/ca-cert.rsa.pem similarity index 100% rename from test/test-files/ssl/example.test/with_ca/rsa/ca-cert.rsa.pem rename to test/files/ssl/example.test/with_ca/rsa/ca-cert.rsa.pem diff --git a/test/test-files/ssl/example.test/with_ca/rsa/ca-key.rsa.pem b/test/files/ssl/example.test/with_ca/rsa/ca-key.rsa.pem similarity index 100% rename from test/test-files/ssl/example.test/with_ca/rsa/ca-key.rsa.pem rename to test/files/ssl/example.test/with_ca/rsa/ca-key.rsa.pem diff --git a/test/test-files/ssl/example.test/with_ca/rsa/cert.ecdsa.pem b/test/files/ssl/example.test/with_ca/rsa/cert.ecdsa.pem similarity index 100% rename from test/test-files/ssl/example.test/with_ca/rsa/cert.ecdsa.pem rename to test/files/ssl/example.test/with_ca/rsa/cert.ecdsa.pem diff --git a/test/test-files/ssl/example.test/with_ca/rsa/cert.rsa.pem b/test/files/ssl/example.test/with_ca/rsa/cert.rsa.pem similarity index 100% rename from test/test-files/ssl/example.test/with_ca/rsa/cert.rsa.pem rename to test/files/ssl/example.test/with_ca/rsa/cert.rsa.pem diff --git a/test/test-files/ssl/example.test/with_ca/rsa/ecdsa.acme.json b/test/files/ssl/example.test/with_ca/rsa/ecdsa.acme.json similarity index 100% rename from test/test-files/ssl/example.test/with_ca/rsa/ecdsa.acme.json rename to test/files/ssl/example.test/with_ca/rsa/ecdsa.acme.json diff --git a/test/test-files/ssl/example.test/with_ca/rsa/key.ecdsa.pem b/test/files/ssl/example.test/with_ca/rsa/key.ecdsa.pem similarity index 100% rename from test/test-files/ssl/example.test/with_ca/rsa/key.ecdsa.pem rename to test/files/ssl/example.test/with_ca/rsa/key.ecdsa.pem diff --git a/test/test-files/ssl/example.test/with_ca/rsa/key.rsa.pem b/test/files/ssl/example.test/with_ca/rsa/key.rsa.pem similarity index 100% rename from test/test-files/ssl/example.test/with_ca/rsa/key.rsa.pem rename to test/files/ssl/example.test/with_ca/rsa/key.rsa.pem diff --git a/test/test-files/ssl/example.test/with_ca/rsa/rsa.acme.json b/test/files/ssl/example.test/with_ca/rsa/rsa.acme.json similarity index 100% rename from test/test-files/ssl/example.test/with_ca/rsa/rsa.acme.json rename to test/files/ssl/example.test/with_ca/rsa/rsa.acme.json diff --git a/test/test-files/ssl/example.test/with_ca/rsa/wildcard/cert.rsa.pem b/test/files/ssl/example.test/with_ca/rsa/wildcard/cert.rsa.pem similarity index 100% rename from test/test-files/ssl/example.test/with_ca/rsa/wildcard/cert.rsa.pem rename to test/files/ssl/example.test/with_ca/rsa/wildcard/cert.rsa.pem diff --git a/test/test-files/ssl/example.test/with_ca/rsa/wildcard/key.rsa.pem b/test/files/ssl/example.test/with_ca/rsa/wildcard/key.rsa.pem similarity index 100% rename from test/test-files/ssl/example.test/with_ca/rsa/wildcard/key.rsa.pem rename to test/files/ssl/example.test/with_ca/rsa/wildcard/key.rsa.pem diff --git a/test/test-files/ssl/example.test/with_ca/rsa/wildcard/rsa.acme.json b/test/files/ssl/example.test/with_ca/rsa/wildcard/rsa.acme.json similarity index 100% rename from test/test-files/ssl/example.test/with_ca/rsa/wildcard/rsa.acme.json rename to test/files/ssl/example.test/with_ca/rsa/wildcard/rsa.acme.json diff --git a/test/helper/change-detection.bash b/test/helper/change-detection.bash index c6a0894cc71..e9dd7020d16 100644 --- a/test/helper/change-detection.bash +++ b/test/helper/change-detection.bash @@ -30,7 +30,7 @@ function _wait_until_expected_count_is_matched() { } # WARNING: Keep in mind it is a '>=' comparison. - # If you provide an explict count to match, ensure it is not too low to cause a false-positive. + # If you provide an explicit count to match, ensure it is not too low to cause a false-positive. function __has_expected_count() { # shellcheck disable=SC2317 [[ $(__get_count) -ge "${EXPECTED_COUNT}" ]] diff --git a/test/helper/common.bash b/test/helper/common.bash index 6920828b09e..b00a23e4309 100644 --- a/test/helper/common.bash +++ b/test/helper/common.bash @@ -18,6 +18,7 @@ function __load_bats_helper() { load "${REPOSITORY_ROOT}/test/test_helper/bats-support/load" load "${REPOSITORY_ROOT}/test/test_helper/bats-assert/load" load "${REPOSITORY_ROOT}/test/helper/sending" + load "${REPOSITORY_ROOT}/test/helper/log_and_filtering" } __load_bats_helper @@ -52,12 +53,10 @@ __load_bats_helper # # This function is internal and should not be used in tests. function __handle_container_name() { - if [[ -n ${1:-} ]] && [[ ${1:-} =~ ^dms-test_ ]] - then + if [[ -n ${1:-} ]] && [[ ${1:-} =~ ^dms-test_ ]]; then printf '%s' "${1}" return 0 - elif [[ -n ${CONTAINER_NAME+set} ]] - then + elif [[ -n ${CONTAINER_NAME:-} ]]; then printf '%s' "${CONTAINER_NAME}" return 0 else @@ -169,8 +168,7 @@ function _repeat_in_container_until_success_or_timeout() { function _repeat_until_success_or_timeout() { local FATAL_FAILURE_TEST_COMMAND - if [[ "${1:-}" == "--fatal-test" ]] - then + if [[ "${1:-}" == "--fatal-test" ]]; then FATAL_FAILURE_TEST_COMMAND="${2:?Provided --fatal-test but no command}" shift 2 fi @@ -178,26 +176,22 @@ function _repeat_until_success_or_timeout() { local TIMEOUT=${1:?Timeout duration must be provided} shift 1 - if ! [[ "${TIMEOUT}" =~ ^[0-9]+$ ]] - then + if ! [[ "${TIMEOUT}" =~ ^[0-9]+$ ]]; then echo "First parameter for timeout must be an integer, received \"${TIMEOUT}\"" return 1 fi local STARTTIME=${SECONDS} - until "${@}" - do - if [[ -n ${FATAL_FAILURE_TEST_COMMAND} ]] && ! eval "${FATAL_FAILURE_TEST_COMMAND}" - then + until "${@}"; do + if [[ -n ${FATAL_FAILURE_TEST_COMMAND} ]] && ! eval "${FATAL_FAILURE_TEST_COMMAND}"; then echo "\`${FATAL_FAILURE_TEST_COMMAND}\` failed, early aborting repeat_until_success of \`${*}\`" >&2 return 1 fi sleep 1 - if [[ $(( SECONDS - STARTTIME )) -gt ${TIMEOUT} ]] - then + if [[ $(( SECONDS - STARTTIME )) -gt ${TIMEOUT} ]]; then echo "Timed out on command: ${*}" >&2 return 1 fi @@ -213,8 +207,7 @@ function _run_until_success_or_timeout() { local TIMEOUT=${1:?Timeout duration must be provided} shift 1 - if [[ ! ${TIMEOUT} =~ ^[0-9]+$ ]] - then + if [[ ! ${TIMEOUT} =~ ^[0-9]+$ ]]; then echo "First parameter for timeout must be an integer, received \"${TIMEOUT}\"" return 1 fi @@ -222,12 +215,10 @@ function _run_until_success_or_timeout() { local STARTTIME=${SECONDS} # shellcheck disable=SC2154 - until run "${@}" && [[ ${status} -eq 0 ]] - do + until run "${@}" && [[ ${status} -eq 0 ]]; do sleep 1 - if (( SECONDS - STARTTIME > TIMEOUT )) - then + if (( SECONDS - STARTTIME > TIMEOUT )); then echo "Timed out on command: ${*}" >&2 return 1 fi @@ -238,7 +229,6 @@ function _run_until_success_or_timeout() { # ! ------------------------------------------------------------------- # ? >> Functions to wait until a condition is met - # Wait until a port is ready. # # @param ${1} = port @@ -268,10 +258,8 @@ function _wait_for_smtp_port_in_container_to_respond() { local CONTAINER_NAME=$(__handle_container_name "${1:-}") local COUNT=0 - until [[ $(_exec_in_container timeout 10 /bin/bash -c 'echo QUIT | nc localhost 25') == *'221 2.0.0 Bye'* ]] - do - if [[ ${COUNT} -eq 20 ]] - then + until [[ $(_exec_in_container timeout 10 /bin/bash -c 'echo QUIT | nc localhost 25') == *'221 2.0.0 Bye'* ]]; do + if [[ ${COUNT} -eq 20 ]]; then echo "Unable to receive a valid response from 'nc localhost 25' within 20 seconds" return 1 fi @@ -281,6 +269,14 @@ function _wait_for_smtp_port_in_container_to_respond() { done } +# Wait for RSPAMD port (11332) to become ready. +# +# @param ${1} = name of the container [OPTIONAL] +function _wait_for_rspamd_port_in_container() { + local CONTAINER_NAME=$(__handle_container_name "${1:-}") + _wait_for_tcp_port_in_container 11332 +} + # Checks whether a service is running inside a container (${1}). # # @param ${1} = service name @@ -363,15 +359,6 @@ function _add_mail_account_then_wait_until_ready() { _wait_until_account_maildir_exists "${MAIL_ACCOUNT}" } -# Assert that the number of lines output by a previous command matches the given -# amount (${1}). `lines` is a special BATS variable updated via `run`. -# -# @param ${1} = number of lines that the output should have -function _should_output_number_of_lines() { - # shellcheck disable=SC2154 - assert_equal "${#lines[@]}" "${1:?Number of lines not provided}" -} - # Reloads the postfix service. # # @param ${1} = container name [OPTIONAL] @@ -384,13 +371,13 @@ function _reload_postfix() { _exec_in_container postfix reload } - # Get the IP of the container (${1}). +# This uses the "bridge" network IPAddress and doesn't consider other docker networks. # # @param ${1} = container name [OPTIONAL] function _get_container_ip() { local TARGET_CONTAINER_NAME=$(__handle_container_name "${1:-}") - docker inspect --format '{{ .NetworkSettings.IPAddress }}' "${TARGET_CONTAINER_NAME}" + docker inspect --format '{{ .NetworkSettings.Networks.bridge.IPAddress }}' "${TARGET_CONTAINER_NAME}" } # Check if a container is running. @@ -405,8 +392,7 @@ function _container_is_running() { # # @param ${1} = directory # @param ${2} = number of files that should be in ${1} -function _count_files_in_directory_in_container() -{ +function _count_files_in_directory_in_container() { local DIRECTORY=${1:?No directory provided} local NUMBER_OF_LINES=${2:?No line count provided} @@ -426,57 +412,34 @@ function _should_have_content_in_directory() { assert_success } -# Filters a service's logs (under `/var/log/supervisor/.log`) given -# a specific string. -# -# @param ${1} = service name -# @param ${2} = string to filter by -# @param ${3} = container name [OPTIONAL] +# A simple wrapper for netcat (`nc`). This is useful when sending +# "raw" e-mails or doing IMAP-related work. # -# ## Attention -# -# The string given to this function is interpreted by `grep -E`, i.e. -# as a regular expression. In case you use characters that are special -# in regular expressions, you need to escape them! -function _filter_service_log() { - local SERVICE=${1:?Service name must be provided} - local STRING=${2:?String to match must be provided} - local CONTAINER_NAME=$(__handle_container_name "${3:-}") +# @param ${1} = the file that is given to `nc` +# @param ${1} = custom parameters for `nc` [OPTIONAL] (default: 0.0.0.0 25) +function _nc_wrapper() { + local FILE=${1:?Must provide name of template file} + local NC_PARAMETERS=${2:-0.0.0.0 25} + + [[ -v CONTAINER_NAME ]] || return 1 - _run_in_container grep -E "${STRING}" "/var/log/supervisor/${SERVICE}.log" + _run_in_container_bash "nc ${NC_PARAMETERS} < /tmp/docker-mailserver-test/${FILE}" } -# Like `_filter_service_log` but asserts that the string was found. -# -# @param ${1} = service name -# @param ${2} = string to filter by -# @param ${3} = container name [OPTIONAL] +# A simple wrapper for a test that checks whether a file exists. # -# ## Attention -# -# The string given to this function is interpreted by `grep -E`, i.e. -# as a regular expression. In case you use characters that are special -# in regular expressions, you need to escape them! -function _service_log_should_contain_string() { - local SERVICE=${1:?Service name must be provided} - local STRING=${2:?String to match must be provided} - local CONTAINER_NAME=$(__handle_container_name "${3:-}") - - _filter_service_log "${SERVICE}" "${STRING}" +# @param ${1} = the path to the file inside the container +function _file_exists_in_container() { + _run_in_container_bash "[[ -f ${1} ]]" assert_success } -# Filters the mail log for lines that belong to a certain email identified -# by its ID. You can obtain the ID of an email you want to send by using -# `_send_email_and_get_id`. +# A simple wrapper for a test that checks whether a file does not exist. # -# @param ${1} = email ID -# @param ${2} = container name [OPTIONAL] -function _print_mail_log_for_id() { - local MAIL_ID=${1:?Mail ID must be provided} - local CONTAINER_NAME=$(__handle_container_name "${2:-}") - - _run_in_container grep -F "${MAIL_ID}" /var/log/mail.log +# @param ${1} = the path to the file (that should not exists) inside the container +function _file_does_not_exist_in_container() { + _run_in_container_bash "[[ -f ${1} ]]" + assert_failure } # ? << Miscellaneous helper functions diff --git a/test/helper/log_and_filtering.bash b/test/helper/log_and_filtering.bash new file mode 100644 index 00000000000..415a203b3a6 --- /dev/null +++ b/test/helper/log_and_filtering.bash @@ -0,0 +1,119 @@ +#!/bin/bash + +# ? ABOUT: Functions defined here aid in working with logs and filtering them. + +# ! ATTENTION: This file is loaded by `common.sh` - do not load it yourself! +# ! ATTENTION: This file requires helper functions from `common.sh`! + +# shellcheck disable=SC2034,SC2155 + +# Assert that the number of lines output by a previous command matches the given amount (${1}). +# `lines` is a special BATS variable updated via `run`. +# +# @param ${1} = number of lines that the output should have +function _should_output_number_of_lines() { + # shellcheck disable=SC2154 + assert_equal "${#lines[@]}" "${1:?Number of lines not provided}" +} + +# Filters a service's logs (under `/var/log/supervisor/.log`) given a specific string. +# +# @param ${1} = service name +# @param ${2} = string to filter by +# @param ... = options given to `grep` (which is used to filter logs) +function _filter_service_log() { + local SERVICE=${1:?Service name must be provided} + local STRING=${2:?String to match must be provided} + shift 2 + + local FILE="/var/log/supervisor/${SERVICE}.log" + # Alternative log location fallback: + [[ -f ${FILE} ]] || FILE="/var/log/mail/${SERVICE}.log" + _run_in_container grep "${@}" "${STRING}" "${FILE}" +} + +# Prints the entirety of the primary mail log. +# Avoid using this method when you could filter more specific log lines with: +# +# 1. _filter_service_log +# 2. _service_log_should[_not]_contain_string +function _show_complete_mail_log() { + _run_in_container cat /var/log/mail/mail.log +} + +# Like `_filter_service_log` but asserts that the string was found. +# +# @param ${1} = service name +# @param ${2} = string to filter by +function _service_log_should_contain_string() { + _filter_service_log "${1}" "${2}" --fixed-strings + assert_success +} + +# Like `_filter_service_log` but asserts that the string was _not_ found. +# +# @param ${1} = service name +# @param ${2} = string to filter by +function _service_log_should_not_contain_string() { + _filter_service_log "${1}" "${2}" --fixed-strings + assert_failure +} + +# Like `_filter_service_log` but asserts that the string was found. +# Uses regular expressions under the hood for pattern matching. +# +# @param ${1} = service name +# @param ${2} = regular expression to filter by +function _service_log_should_contain_string_regexp() { + _filter_service_log "${1}" "${2}" --extended-regexp + assert_success +} + +# Like `_filter_service_log` but asserts that the string was _not_ found. +# Uses regular expressions under the hood for pattern matching. +# +# @param ${1} = service name +# @param ${2} = regular expression to filter by +function _service_log_should_not_contain_string_regexp() { + _filter_service_log "${1}" "${2}" --extended-regexp + assert_failure +} + +# Filters the mail log by the given MSG_ID (Message-ID) parameter, +# printing log lines which include the associated Postfix Queue ID. +# +# @param ${1} = The local-part of a Message-ID header value (``) +function _print_mail_log_of_queue_id_from_msgid() { + # A unique ID Postfix generates for tracking queued mail as it's processed. + # The length can vary (as per the postfix docs). Hence, we use a range to safely capture it. + # https://github.com/docker-mailserver/docker-mailserver/pull/3747#discussion_r1446679671 + local QUEUE_ID_REGEX='[A-Z0-9]{9,12}' + + local MSG_ID=$(__construct_msgid "${1:?The local-part for MSG_ID was not provided}") + shift 1 + + _wait_for_empty_mail_queue_in_container + + QUEUE_ID=$(_exec_in_container tac /var/log/mail.log \ + | grep -E "postfix/cleanup.*: ${QUEUE_ID_REGEX}:.*message-id=${MSG_ID}" \ + | grep -E --only-matching --max-count 1 "${QUEUE_ID_REGEX}" || :) + + # We perform plausibility checks on the IDs. + assert_not_equal "${QUEUE_ID}" '' + run echo "${QUEUE_ID}" + assert_line --regexp "^${QUEUE_ID_REGEX}$" + + # Postfix specific logs: + _filter_service_log 'mail' "${QUEUE_ID}" +} + +# A convenience method that filters for Dovecot specific logs with a `msgid` field that matches the MSG_ID input. +# +# @param ${1} = The local-part of a Message-ID header value (``) +function _print_mail_log_for_msgid() { + local MSG_ID=$(__construct_msgid "${1:?The local-part for MSG_ID was not provided}") + shift 1 + + # Dovecot specific logs: + _filter_service_log 'mail' "msgid=${MSG_ID}" +} diff --git a/test/helper/sending.bash b/test/helper/sending.bash index 631617a172d..1c5d844a4ae 100644 --- a/test/helper/sending.bash +++ b/test/helper/sending.bash @@ -1,80 +1,136 @@ #!/bin/bash -# shellcheck disable=SC2034,SC2155 - # ? ABOUT: Functions defined here help with sending emails in tests. # ! ATTENTION: This file is loaded by `common.sh` - do not load it yourself! # ! ATTENTION: This file requires helper functions from `common.sh`! +# ! ATTENTION: Functions prefixed with `__` are intended for internal use within +# ! this file (or other helpers) only, not in tests. + +# shellcheck disable=SC2034,SC2155 -# Sends a mail from localhost (127.0.0.1) to a container. To send -# a custom email, create a file at `test/test-files/`, -# and provide `` as an argument to this function. +# Sends an e-mail from the container named by the environment variable `CONTAINER_NAME` +# to the same or another container. +# +# To send a custom email, you can +# +# 1. create a file at `test/files/` and provide `` via `--data` as an argument to this function; +# 2. use this function without the `--data` argument, in which case we provide a default; +# 3. provide data inline (`--data `). +# +# The very first parameter **may** be `--expect-rejection` - use it of you expect the mail transaction to not finish +# successfully. All other (following) parameters include all options that one can supply to `swaks` itself. +# As mentioned before, the `--data` parameter expects a value of either: # -# @param ${1} = template file (path) name -# @param ${2} = parameters for `nc` [OPTIONAL] (default: `0.0.0.0 25`) +# - A relative path from `test/files/emails/` +# - An "inline" data string (e.g., `Date: 1 Jan 2024\nSubject: This is a test`) +# +# ## Output +# +# This functions prints the output of the transaction that `swaks` prints. # # ## Attention # # This function assumes `CONTAINER_NAME` to be properly set (to the container # name the command should be executed in)! # -# This function will just send the email in an "asynchronous" fashion, i.e. it will -# send the email but it will not make sure the mail queue is empty after the mail -# has been sent. +# This function will send the email in an "asynchronous" fashion, +# it will return without waiting for the Postfix mail queue to be emptied. function _send_email() { - local TEMPLATE_FILE=${1:?Must provide name of template file} - local NC_PARAMETERS=${2:-0.0.0.0 25} + local RETURN_VALUE=0 + local COMMAND_STRING + + function __parse_arguments() { + [[ -v CONTAINER_NAME ]] || return 1 + + # Parameter defaults common to our testing needs: + local EHLO='mail.external.tld' + local FROM='user@external.tld' + local TO='user1@localhost.localdomain' + local SERVER='0.0.0.0' + local PORT=25 + # Extra options for `swaks` that aren't covered by the default options above: + local ADDITIONAL_SWAKS_OPTIONS=() + local DATA_WAS_SUPPLIED=0 + + while [[ ${#} -gt 0 ]]; do + case "${1}" in + ( '--ehlo' ) EHLO=${2:?--ehlo given but no argument} ; shift 2 ;; + ( '--from' ) FROM=${2:?--from given but no argument} ; shift 2 ;; + ( '--to' ) TO=${2:?--to given but no argument} ; shift 2 ;; + ( '--server' ) SERVER=${2:?--server given but no argument} ; shift 2 ;; + ( '--port' ) PORT=${2:?--port given but no argument} ; shift 2 ;; + ( '--data' ) + ADDITIONAL_SWAKS_OPTIONS+=('--data') + local FILE_PATH="/tmp/docker-mailserver-test/emails/${2:?--data given but no argument provided}" + if _exec_in_container_bash "[[ -e ${FILE_PATH} ]]"; then + ADDITIONAL_SWAKS_OPTIONS+=("@${FILE_PATH}") + else + ADDITIONAL_SWAKS_OPTIONS+=("'${2}'") + fi + shift 2 + DATA_WAS_SUPPLIED=1 + ;; + ( * ) ADDITIONAL_SWAKS_OPTIONS+=("'${1}'") ; shift 1 ;; + esac + done + + if [[ ${DATA_WAS_SUPPLIED} -eq 0 ]]; then + # Fallback template (without the implicit `Message-Id` + `X-Mailer` headers from swaks): + # NOTE: It is better to let Postfix generate and append the `Message-Id` header itself, + # as it will contain the Queue ID for tracking in logs (which is also returned in swaks output). + ADDITIONAL_SWAKS_OPTIONS+=('--data') + ADDITIONAL_SWAKS_OPTIONS+=("'Date: %DATE%\nTo: %TO_ADDRESS%\nFrom: %FROM_ADDRESS%\nSubject: test %DATE%\n%NEW_HEADERS%\n%BODY%\n'") + fi + + echo "swaks --server '${SERVER}' --port '${PORT}' --ehlo '${EHLO}' --from '${FROM}' --to '${TO}' ${ADDITIONAL_SWAKS_OPTIONS[*]}" + } - assert_not_equal "${NC_PARAMETERS}" '' - assert_not_equal "${CONTAINER_NAME:-}" '' + if [[ ${1:-} == --expect-rejection ]]; then + shift 1 + COMMAND_STRING=$(__parse_arguments "${@}") + _run_in_container_bash "${COMMAND_STRING}" + RETURN_VALUE=${?} + else + COMMAND_STRING=$(__parse_arguments "${@}") + _run_in_container_bash "${COMMAND_STRING}" + assert_success + fi - _run_in_container_bash "nc ${NC_PARAMETERS} < /tmp/docker-mailserver-test/${TEMPLATE_FILE}.txt" - assert_success + # shellcheck disable=SC2154 + echo "${output}" + return "${RETURN_VALUE}" } -# Like `_send_mail` with two major differences: +# Construct the value for a 'Message-ID' header. +# For tests we use only the local-part to identify mail activity in logs. The rest of the value is fixed. # -# 1. this function waits for the mail to be processed; there is no asynchronicity -# because filtering the logs in a synchronous way is easier and safer! -# 2. this function prints an ID one can later filter by to check logs +# A Message-ID header value should be in the form of: `` +# https://en.wikipedia.org/wiki/Message-ID +# https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.4 # -# No. 2 is especially useful in case you send more than one email in a single -# test file and need to assert certain log entries for each mail individually. -# -# @param ${1} = template file (path) name -# @param ${2} = parameters for `nc` [OPTIONAL] (default: `0.0.0.0 25`) -# -# ## Attention -# -# This function assumes `CONTAINER_NAME` to be properly set (to the container -# name the command should be executed in)! -# -# ## Safety -# -# This functions assumes **no concurrent sending of emails to the same container**! -# If two clients send simultaneously, there is no guarantee the correct ID is -# chosen. Sending more than one mail at any given point in time with this function -# is UNDEFINED BEHAVIOR! -function _send_email_and_get_id() { - local TEMPLATE_FILE=${1:?Must provide name of template file} - local NC_PARAMETERS=${2:-0.0.0.0 25} - local MAIL_ID - - assert_not_equal "${NC_PARAMETERS}" '' - assert_not_equal "${CONTAINER_NAME:-}" '' +# @param ${1} = The local-part of a Message-ID header value (``) +function __construct_msgid() { + local MSG_ID_LOCALPART=${1:?The local-part for MSG_ID was not provided} + echo "<${MSG_ID_LOCALPART}@dms-tests>" +} - _wait_for_empty_mail_queue_in_container - _send_email "${TEMPLATE_FILE}" - _wait_for_empty_mail_queue_in_container +# Like `_send_email` but adds a "Message-ID: ${1}@dms-tests>" header, +# which allows for filtering logs later. +# +# @param ${1} = The local-part of a Message-ID header value (``) +function _send_email_with_msgid() { + local MSG_ID=$(__construct_msgid "${1:?The local-part for MSG_ID was not provided}") + shift 1 - # The unique ID Postfix (and other services) use may be different in length - # on different systems (e.g. amd64 (11) vs aarch64 (10)). Hence, we use a - # range to safely capture it. - MAIL_ID=$(_exec_in_container tac /var/log/mail.log \ - | grep -E -m 1 'postfix/smtpd.*: [A-Z0-9]+: client=localhost' \ - | grep -E -o '[A-Z0-9]{9,12}' || true) + _send_email "${@}" --header "Message-ID: ${MSG_ID}" +} - assert_not_equal "${MAIL_ID}" '' - echo "${MAIL_ID}" +# Send a spam e-mail by utilizing GTUBE. +# +# Extra arguments given to this function will be supplied by `_send_email_with_msgid` directly. +function _send_spam() { + _send_email_with_msgid 'dms-test-email-spam' "${@}" \ + --from 'spam@external.tld' \ + --body 'XJS*C4JDBQADN1.NSBN3*2IDNEN*GTUBE-STANDARD-ANTI-UBE-TEST-EMAIL*C.34X' } diff --git a/test/helper/setup.bash b/test/helper/setup.bash index 8fac99038af..95b7df28ee1 100644 --- a/test/helper/setup.bash +++ b/test/helper/setup.bash @@ -17,8 +17,7 @@ # This function is internal and should not be used in tests. function __initialize_variables() { function __check_if_set() { - if [[ ${!1+set} != 'set' ]] - then + if [[ -z ${!1:-} ]]; then echo "ERROR: (helper/setup.sh) '${1:?No variable name given to __check_if_set}' is not set" >&2 exit 1 fi @@ -30,8 +29,7 @@ function __initialize_variables() { 'CONTAINER_NAME' ) - for VARIABLE in "${REQUIRED_VARIABLES_FOR_TESTS[@]}" - do + for VARIABLE in "${REQUIRED_VARIABLES_FOR_TESTS[@]}"; do __check_if_set "${VARIABLE}" done @@ -64,8 +62,7 @@ function _duplicate_config_for_container() { local OUTPUT_FOLDER OUTPUT_FOLDER=$(_print_private_config_path "${2}") - if [[ -z ${OUTPUT_FOLDER} ]] - then + if [[ -z ${OUTPUT_FOLDER} ]]; then echo "'OUTPUT_FOLDER' in '_duplicate_config_for_container' is empty" >&2 return 1 fi @@ -101,11 +98,10 @@ function _init_with_defaults() { # Common complimentary test files, read-only safe to share across containers: export TEST_FILES_CONTAINER_PATH='/tmp/docker-mailserver-test' - export TEST_FILES_VOLUME="${REPOSITORY_ROOT}/test/test-files:${TEST_FILES_CONTAINER_PATH}:ro" + export TEST_FILES_VOLUME="${REPOSITORY_ROOT}/test/files:${TEST_FILES_CONTAINER_PATH}:ro" # The config volume cannot be read-only as some data needs to be written at container startup # - # - two sed failures (unknown lines) # - dovecot-quotas.cf (setup-stack.sh:_setup_dovecot_quotas) # - postfix-aliases.cf (setup-stack.sh:_setup_postfix_aliases) # TODO: Check how many tests need write access. Consider using `docker create` + `docker cp` for easier cleanup. diff --git a/test/helper/tls.bash b/test/helper/tls.bash index 529c84c686f..74b8ebe603c 100644 --- a/test/helper/tls.bash +++ b/test/helper/tls.bash @@ -15,7 +15,7 @@ load "${REPOSITORY_ROOT}/test/helper/common" # For certs actually provisioned from LetsEncrypt the Root CA cert should not need to be provided, # as it would already be available by default in `/etc/ssl/certs`, requiring only the cert chain (fullchain.pem). -function _should_succesfully_negotiate_tls() { +function _should_successfully_negotiate_tls() { local FQDN=${1} # shellcheck disable=SC2031 local CA_CERT=${2:-${TEST_CA_CERT}} @@ -29,8 +29,7 @@ function _should_succesfully_negotiate_tls() { assert_success local PORTS=(25 587 465 143 993) - for PORT in "${PORTS[@]}" - do + for PORT in "${PORTS[@]}"; do _negotiate_tls "${FQDN}" "${PORT}" done } @@ -71,14 +70,11 @@ function _generate_openssl_cmd() { local CMD_OPENSSL="timeout 1 openssl s_client -connect ${HOST}:${PORT}" # STARTTLS ports need to add a hint: - if [[ ${PORT} =~ ^(25|587)$ ]] - then + if [[ ${PORT} =~ ^(25|587)$ ]]; then CMD_OPENSSL="${CMD_OPENSSL} -starttls smtp" - elif [[ ${PORT} == 143 ]] - then + elif [[ ${PORT} == 143 ]]; then CMD_OPENSSL="${CMD_OPENSSL} -starttls imap" - elif [[ ${PORT} == 110 ]] - then + elif [[ ${PORT} == 110 ]]; then CMD_OPENSSL="${CMD_OPENSSL} -starttls pop3" fi diff --git a/test/linting/.ecrc.json b/test/linting/.ecrc.json index d9abd2f6438..44d501ba13d 100644 --- a/test/linting/.ecrc.json +++ b/test/linting/.ecrc.json @@ -1,25 +1,9 @@ { - "Verbose": false, - "Debug": false, "IgnoreDefaults": false, - "SpacesAftertabs": true, - "NoColor": false, "Exclude": [ - "^test/", - "\\.git.*", - "\\.cf$", - "\\.conf$", - "\\.init$", - "\\.md$" - ], - "AllowedContentTypes": [], - "PassedFiles": [], - "Disable": { - "EndOfLine": false, - "Indentation": false, - "InsertFinalNewline": false, - "TrimTrailingWhitespace": false, - "IndentSize": false, - "MaxLineLength": false - } + "^test/bats/", + "^test/test_helper/bats-(assert|support)", + "\\.git/", + "CONTRIBUTORS\\.md" + ] } diff --git a/test/linting/.hadolint.yaml b/test/linting/.hadolint.yml similarity index 100% rename from test/linting/.hadolint.yaml rename to test/linting/.hadolint.yml diff --git a/test/linting/lint.sh b/test/linting/lint.sh index 39c86250109..30cab39fc23 100755 --- a/test/linting/lint.sh +++ b/test/linting/lint.sh @@ -9,20 +9,22 @@ shopt -s inherit_errexit REPOSITORY_ROOT=$(realpath "$(dirname "$(readlink -f "${0}")")"/../../) LOG_LEVEL=${LOG_LEVEL:-debug} -HADOLINT_VERSION='2.9.2' -ECLINT_VERSION='2.4.0' +HADOLINT_VERSION='2.12.0' +ECLINT_VERSION='2.7.2' SHELLCHECK_VERSION='0.9.0' # shellcheck source=./../../target/scripts/helpers/log.sh source "${REPOSITORY_ROOT}/target/scripts/helpers/log.sh" -function _eclint -{ +function _eclint() { + # `/check` is used instead of `/ci` as the mount path due to: + # https://github.com/editorconfig-checker/editorconfig-checker/issues/268#issuecomment-1826200253 + # `.ecrc.json` continues to explicitly ignores the `.git/` path to avoid any potential confusion if docker run --rm --tty \ - --volume "${REPOSITORY_ROOT}:/ci:ro" \ - --workdir "/ci" \ + --volume "${REPOSITORY_ROOT}:/check:ro" \ + --workdir "/check" \ --name dms-test_eclint \ - "mstruebing/editorconfig-checker:${ECLINT_VERSION}" ec -config "/ci/test/linting/.ecrc.json" + "mstruebing/editorconfig-checker:${ECLINT_VERSION}" ec -config "/check/test/linting/.ecrc.json" then _log 'info' 'ECLint succeeded' else @@ -31,13 +33,12 @@ function _eclint fi } -function _hadolint -{ +function _hadolint() { if docker run --rm --tty \ --volume "${REPOSITORY_ROOT}:/ci:ro" \ --workdir "/ci" \ --name dms-test_hadolint \ - "hadolint/hadolint:v${HADOLINT_VERSION}-alpine" hadolint --config "/ci/test/linting/.hadolint.yaml" Dockerfile + "hadolint/hadolint:v${HADOLINT_VERSION}-alpine" hadolint --config "/ci/test/linting/.hadolint.yml" Dockerfile then _log 'info' 'Hadolint succeeded' else @@ -46,22 +47,42 @@ function _hadolint fi } -function _shellcheck -{ - # File paths for shellcheck: - F_SH=$(find . -type f -iname '*.sh' \ +# Create three arrays (F_SH, F_BIN, F_BATS) containing our BASH scripts +function _getBashScripts() { + readarray -d '' F_SH < <(find . -type f -iname '*.sh' \ -not -path './test/bats/*' \ - -not -path './test/test_helper/*' + -not -path './test/test_helper/*' \ + -not -path './.git/*' \ + -print0 \ ) + # shellcheck disable=SC2248 - F_BIN=$(find 'target/bin' -type f -not -name '*.py') - F_BATS=$(find 'test' -maxdepth 1 -type f -iname '*.bats') + readarray -d '' F_BIN < <(find 'target/bin' -type f -not -name '*.py' -print0) + readarray -d '' F_BATS < <(find 'test/tests/' -type f -iname '*.bats' -print0) +} + +# Check BASH files for correct syntax +function _bashcheck() { + local ERROR=0 SCRIPT + # .bats files are excluded from the test below: Due to their custom syntax ( @test ), .bats files are not standard bash + for SCRIPT in "${F_SH[@]}" "${F_BIN[@]}"; do + bash -n "${SCRIPT}" || ERROR=1 + done + + if [[ ${ERROR} -eq 0 ]]; then + _log 'info' 'BASH syntax check succeeded' + else + _log 'error' 'BASH syntax check failed' + return 1 + fi +} +function _shellcheck() { # This command is a bit easier to grok as multi-line. # There is a `.shellcheckrc` file, but it's only supports half of the options below, thus kept as CLI: # `SCRIPTDIR` is a special value that represents the path of the script being linted, # all sourced scripts share the same SCRIPTDIR source-path of the original script being linted. - CMD_SHELLCHECK=(shellcheck + local CMD_SHELLCHECK=(shellcheck --external-sources --check-sourced --severity=style @@ -73,7 +94,13 @@ function _shellcheck --exclude=SC2311 --exclude=SC2312 --source-path=SCRIPTDIR - "${F_SH} ${F_BIN} ${F_BATS}" + ) + + local BATS_EXTRA_ARGS=( + --exclude=SC2030 + --exclude=SC2031 + --exclude=SC2034 + --exclude=SC2155 ) # The linter can reference additional source-path values declared in scripts, @@ -86,12 +113,22 @@ function _shellcheck # Otherwise it only applies to the line below it. You can declare multiple source-paths, they don't override the previous. # `source=relative/path/to/file.sh` will check the source value in each source-path as well. # shellcheck disable=SC2068 - if docker run --rm --tty \ + local ERROR=0 + + docker run --rm --tty \ --volume "${REPOSITORY_ROOT}:/ci:ro" \ --workdir "/ci" \ --name dms-test_shellcheck \ - "koalaman/shellcheck-alpine:v${SHELLCHECK_VERSION}" ${CMD_SHELLCHECK[@]} - then + "koalaman/shellcheck-alpine:v${SHELLCHECK_VERSION}" "${CMD_SHELLCHECK[@]}" "${F_SH[@]}" "${F_BIN[@]}" || ERROR=1 + + docker run --rm --tty \ + --volume "${REPOSITORY_ROOT}:/ci:ro" \ + --workdir "/ci" \ + --name dms-test_shellcheck \ + "koalaman/shellcheck-alpine:v${SHELLCHECK_VERSION}" "${CMD_SHELLCHECK[@]}" \ + "${BATS_EXTRA_ARGS[@]}" "${F_BATS[@]}" || ERROR=1 + + if [[ ${ERROR} -eq 0 ]]; then _log 'info' 'ShellCheck succeeded' else _log 'error' 'ShellCheck failed' @@ -99,12 +136,12 @@ function _shellcheck fi } -function _main -{ +function _main() { case "${1:-}" in - ( 'eclint' ) _eclint ;; - ( 'hadolint' ) _hadolint ;; - ( 'shellcheck' ) _shellcheck ;; + ( 'eclint' ) _eclint ;; + ( 'hadolint' ) _hadolint ;; + ( 'bashcheck' ) _getBashScripts; _bashcheck ;; + ( 'shellcheck' ) _getBashScripts; _shellcheck ;; ( * ) _log 'error' "'${1:-}' is not a command nor an option" return 3 diff --git a/test/test-files/auth/added-smtp-auth-login-wrong.txt b/test/test-files/auth/added-smtp-auth-login-wrong.txt deleted file mode 100644 index a75856f11e5..00000000000 --- a/test/test-files/auth/added-smtp-auth-login-wrong.txt +++ /dev/null @@ -1,4 +0,0 @@ -EHLO mail -AUTH LOGIN YWRkZWRAbG9jYWxob3N0LmxvY2FsZG9tYWlu -Bn3JKisq4HQ2RO== -QUIT diff --git a/test/test-files/auth/added-smtp-auth-login.txt b/test/test-files/auth/added-smtp-auth-login.txt deleted file mode 100644 index 5276b7f4181..00000000000 --- a/test/test-files/auth/added-smtp-auth-login.txt +++ /dev/null @@ -1,4 +0,0 @@ -EHLO mail -AUTH LOGIN YWRkZWRAbG9jYWxob3N0LmxvY2FsZG9tYWlu -bXlwYXNzd29yZA== -QUIT diff --git a/test/test-files/auth/added-smtp-auth-plain-wrong.txt b/test/test-files/auth/added-smtp-auth-plain-wrong.txt deleted file mode 100644 index 6ce5a3833ef..00000000000 --- a/test/test-files/auth/added-smtp-auth-plain-wrong.txt +++ /dev/null @@ -1,3 +0,0 @@ -EHLO mail -AUTH PLAIN YWRkZWRAbG9jYWxob3N0LmxvY2FsZG9tYWluAGFkZGVkQGxvY2FsaG9zdC5sb2NhbGRvbWFpbgBCQURQQVNTV09SRA== -QUIT diff --git a/test/test-files/auth/added-smtp-auth-plain.txt b/test/test-files/auth/added-smtp-auth-plain.txt deleted file mode 100644 index ed48d77df8a..00000000000 --- a/test/test-files/auth/added-smtp-auth-plain.txt +++ /dev/null @@ -1,3 +0,0 @@ -EHLO mail -AUTH PLAIN YWRkZWRAbG9jYWxob3N0LmxvY2FsZG9tYWluAGFkZGVkQGxvY2FsaG9zdC5sb2NhbGRvbWFpbgBteXBhc3N3b3Jk -QUIT diff --git a/test/test-files/auth/sasl-ldap-smtp-auth.txt b/test/test-files/auth/sasl-ldap-smtp-auth.txt deleted file mode 100644 index df4d7db4064..00000000000 --- a/test/test-files/auth/sasl-ldap-smtp-auth.txt +++ /dev/null @@ -1,5 +0,0 @@ -EHLO mail -AUTH LOGIN -c29tZS51c2VyQGxvY2FsaG9zdC5sb2NhbGRvbWFpbg== -c2VjcmV0 -QUIT diff --git a/test/test-files/auth/smtp-auth-login-wrong.txt b/test/test-files/auth/smtp-auth-login-wrong.txt deleted file mode 100644 index 39b4f01c619..00000000000 --- a/test/test-files/auth/smtp-auth-login-wrong.txt +++ /dev/null @@ -1,4 +0,0 @@ -EHLO mail -AUTH LOGIN dXNlcjFAbG9jYWxob3N0LmxvY2FsZG9tYWlu -Bn3JKisq4HQ2RO== -QUIT diff --git a/test/test-files/auth/smtp-auth-login.txt b/test/test-files/auth/smtp-auth-login.txt deleted file mode 100644 index 50ff99f3104..00000000000 --- a/test/test-files/auth/smtp-auth-login.txt +++ /dev/null @@ -1,4 +0,0 @@ -EHLO mail -AUTH LOGIN dXNlcjFAbG9jYWxob3N0LmxvY2FsZG9tYWlu -bXlwYXNzd29yZA== -QUIT diff --git a/test/test-files/auth/smtp-auth-plain-wrong.txt b/test/test-files/auth/smtp-auth-plain-wrong.txt deleted file mode 100644 index d8d8ad2a355..00000000000 --- a/test/test-files/auth/smtp-auth-plain-wrong.txt +++ /dev/null @@ -1,3 +0,0 @@ -EHLO mail -AUTH PLAIN WRONGPASSWORD -QUIT diff --git a/test/test-files/auth/smtp-auth-plain.txt b/test/test-files/auth/smtp-auth-plain.txt deleted file mode 100644 index 2e60fdc3e20..00000000000 --- a/test/test-files/auth/smtp-auth-plain.txt +++ /dev/null @@ -1,3 +0,0 @@ -EHLO mail -AUTH PLAIN dXNlcjFAbG9jYWxob3N0LmxvY2FsZG9tYWluAHVzZXIxQGxvY2FsaG9zdC5sb2NhbGRvbWFpbgBteXBhc3N3b3Jk -QUIT diff --git a/test/test-files/email-templates/amavis-spam.txt b/test/test-files/email-templates/amavis-spam.txt deleted file mode 100644 index 66be1df3d32..00000000000 --- a/test/test-files/email-templates/amavis-spam.txt +++ /dev/null @@ -1,13 +0,0 @@ -HELO mail.external.tld -MAIL FROM: spam@external.tld -RCPT TO: user1@localhost.localdomain -DATA -From: Docker Mail Server -To: Existing Local User -Date: Sat, 22 May 2010 07:43:25 -0400 -Subject: Test Message amavis-spam.txt -This is a test mail. -XJS*C4JDBQADN1.NSBN3*2IDNEN*GTUBE-STANDARD-ANTI-UBE-TEST-EMAIL*C.34X - -. -QUIT diff --git a/test/test-files/email-templates/existing-added.txt b/test/test-files/email-templates/existing-added.txt deleted file mode 100644 index 320fa4d2a10..00000000000 --- a/test/test-files/email-templates/existing-added.txt +++ /dev/null @@ -1,12 +0,0 @@ -HELO mail.external.tld -MAIL FROM: user@external.tld -RCPT TO: added@localhost.localdomain -DATA -From: Docker Mail Server -To: Existing Local User -Date: Sat, 22 May 2010 07:43:25 -0400 -Subject: Test Message existing-added.txt -This is a test mail. - -. -QUIT diff --git a/test/test-files/email-templates/existing-alias-external.txt b/test/test-files/email-templates/existing-alias-external.txt deleted file mode 100644 index 61b1df3c5c8..00000000000 --- a/test/test-files/email-templates/existing-alias-external.txt +++ /dev/null @@ -1,12 +0,0 @@ -HELO mail.external.tld -MAIL FROM: user@external.tld -RCPT TO: alias1@localhost.localdomain -DATA -From: Docker Mail Server -To: Existing Local User -Date: Sat, 22 May 2010 07:43:25 -0400 -Subject: Test Message existing-alias-external.txt -This is a test mail. - -. -QUIT diff --git a/test/test-files/email-templates/existing-alias-local.txt b/test/test-files/email-templates/existing-alias-local.txt deleted file mode 100644 index c1bbc890c53..00000000000 --- a/test/test-files/email-templates/existing-alias-local.txt +++ /dev/null @@ -1,12 +0,0 @@ -HELO mail.external.tld -MAIL FROM: user@external.tld -RCPT TO: alias2@localhost.localdomain -DATA -From: Docker Mail Server -To: Existing Local Alias -Date: Sat, 22 May 2010 07:43:25 -0400 -Subject: Test Message existing-alias-local.txt -This is a test mail. - -. -QUIT diff --git a/test/test-files/email-templates/existing-alias-recipient-delimiter.txt b/test/test-files/email-templates/existing-alias-recipient-delimiter.txt deleted file mode 100644 index 47b0139769f..00000000000 --- a/test/test-files/email-templates/existing-alias-recipient-delimiter.txt +++ /dev/null @@ -1,12 +0,0 @@ -HELO mail.external.tld -MAIL FROM: user@external.tld -RCPT TO: alias1~test@localhost.localdomain -DATA -From: Docker Mail Server -To: Existing Local Alias With Delimiter -Date: Sat, 22 May 2010 07:43:25 -0400 -Subject: Test Message existing-alias-recipient-delimiter.txt -This is a test mail. - -. -QUIT diff --git a/test/test-files/email-templates/existing-catchall-local.txt b/test/test-files/email-templates/existing-catchall-local.txt deleted file mode 100644 index c80db170988..00000000000 --- a/test/test-files/email-templates/existing-catchall-local.txt +++ /dev/null @@ -1,12 +0,0 @@ -HELO mail.external.tld -MAIL FROM: user@external.tld -RCPT TO: wildcard@localdomain2.com -DATA -From: Docker Mail Server -To: Existing Local User -Date: Sat, 22 May 2010 07:43:25 -0400 -Subject: Test Message existing-catchall-local.txt -This is a test mail. - -. -QUIT diff --git a/test/test-files/email-templates/existing-regexp-alias-external.txt b/test/test-files/email-templates/existing-regexp-alias-external.txt deleted file mode 100644 index 0e214db4a2b..00000000000 --- a/test/test-files/email-templates/existing-regexp-alias-external.txt +++ /dev/null @@ -1,12 +0,0 @@ -HELO mail.external.tld -MAIL FROM: user@external.tld -RCPT TO: bounce-always@localhost.localdomain -DATA -From: Docker Mail Server -To: Existing Local User -Date: Sat, 22 May 2010 07:43:25 -0400 -Subject: Test Message existing-regexp-alias-external.txt -This is a test mail. - -. -QUIT diff --git a/test/test-files/email-templates/existing-regexp-alias-local.txt b/test/test-files/email-templates/existing-regexp-alias-local.txt deleted file mode 100644 index 6af46e92e7a..00000000000 --- a/test/test-files/email-templates/existing-regexp-alias-local.txt +++ /dev/null @@ -1,12 +0,0 @@ -HELO mail.external.tld -MAIL FROM: user@external.tld -RCPT TO: test123@localhost.localdomain -DATA -From: Docker Mail Server -To: Existing Local User -Date: Sat, 22 May 2010 07:43:25 -0400 -Subject: Test Message existing-regexp-alias-local.txt -This is a test mail. - -. -QUIT diff --git a/test/test-files/email-templates/existing-user-and-cc-local-alias.txt b/test/test-files/email-templates/existing-user-and-cc-local-alias.txt deleted file mode 100644 index 5fcb333b45f..00000000000 --- a/test/test-files/email-templates/existing-user-and-cc-local-alias.txt +++ /dev/null @@ -1,13 +0,0 @@ -HELO mail.external.tld -MAIL FROM: user@external.tld -RCPT TO: user1@localhost.localdomain -DATA -From: Docker Mail Server -To: Existing Local User -Cc: Existing Local Alias -Date: Sat, 22 May 2010 07:43:25 -0400 -Subject: Test Message existing-user-and-cc-local-alias.txt -This is a test mail. - -. -QUIT diff --git a/test/test-files/email-templates/existing-user2.txt b/test/test-files/email-templates/existing-user2.txt deleted file mode 100644 index 63554f27ab3..00000000000 --- a/test/test-files/email-templates/existing-user2.txt +++ /dev/null @@ -1,12 +0,0 @@ -HELO mail.external.tld -MAIL FROM: user@external.tld -RCPT TO: user2@otherdomain.tld -DATA -From: Docker Mail Server -To: Existing Local User -Date: Sat, 22 May 2010 07:43:25 -0400 -Subject: Test Message existing-user2.txt -This is a test mail. - -. -QUIT diff --git a/test/test-files/email-templates/existing-user3.txt b/test/test-files/email-templates/existing-user3.txt deleted file mode 100644 index facd5328b66..00000000000 --- a/test/test-files/email-templates/existing-user3.txt +++ /dev/null @@ -1,12 +0,0 @@ -HELO mail.external.tld -MAIL FROM: user@external.tld -RCPT TO: user3@localhost.localdomain -DATA -From: Docker Mail Server -To: Existing Local User -Date: Sat, 22 May 2010 07:43:33 -0400 -Subject: Test Message existing-user1.txt -This is a test mail. - -. -QUIT diff --git a/test/test-files/email-templates/non-existing-user.txt b/test/test-files/email-templates/non-existing-user.txt deleted file mode 100644 index 406f67555fc..00000000000 --- a/test/test-files/email-templates/non-existing-user.txt +++ /dev/null @@ -1,12 +0,0 @@ -HELO mail.external.tld -MAIL FROM: user@external.tld -RCPT TO: nouser@localhost.localdomain -DATA -From: Docker Mail Server -To: Existing Local User -Date: Sat, 22 May 2010 07:43:25 -0400 -Subject: Test Message non-existing-user.txt -This is a test mail. - -. -QUIT diff --git a/test/test-files/email-templates/rspamd-spam.txt b/test/test-files/email-templates/rspamd-spam.txt deleted file mode 100644 index 2e6d566a181..00000000000 --- a/test/test-files/email-templates/rspamd-spam.txt +++ /dev/null @@ -1,13 +0,0 @@ -HELO mail.example.test -MAIL FROM: spam@example.test -RCPT TO: user1@localhost.localdomain -DATA -From: Docker Mail Server -To: Existing Local User -Date: Sat, 21 Jan 2023 11:11:11 +0000 -Subject: Test Message rspamd-spam.txt -This is a test mail. -XJS*C4JDBQADN1.NSBN3*2IDNEN*GTUBE-STANDARD-ANTI-UBE-TEST-EMAIL*C.34X - -. -QUIT diff --git a/test/test-files/email-templates/rspamd-virus.txt b/test/test-files/email-templates/rspamd-virus.txt deleted file mode 100644 index cdacf9af43f..00000000000 --- a/test/test-files/email-templates/rspamd-virus.txt +++ /dev/null @@ -1,12 +0,0 @@ -HELO mail.example.test -MAIL FROM: virus@example.test -RCPT TO: user1@localhost.localdomain -DATA -From: Docker Mail Server -To: Existing Local User -Date: Sat, 21 Jan 2023 11:11:11 +0000 -Subject: Test Message rspamd-virus.txt -X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H* - -. -QUIT diff --git a/test/test-files/email-templates/smtp-ehlo.txt b/test/test-files/email-templates/smtp-ehlo.txt deleted file mode 100644 index 05524efd1f6..00000000000 --- a/test/test-files/email-templates/smtp-ehlo.txt +++ /dev/null @@ -1,2 +0,0 @@ -EHLO mail.localhost -QUIT diff --git a/test/test_helper/common.bash b/test/test_helper/common.bash index e0a4facd539..979b3c08d52 100644 --- a/test/test_helper/common.bash +++ b/test/test_helper/common.bash @@ -37,8 +37,7 @@ function repeat_until_success_or_timeout { local STARTTIME=${SECONDS} shift 1 - until "${@}" - do + until "${@}"; do if [[ -n ${FATAL_FAILURE_TEST_COMMAND} ]] && ! eval "${FATAL_FAILURE_TEST_COMMAND}"; then echo "\`${FATAL_FAILURE_TEST_COMMAND}\` failed, early aborting repeat_until_success of \`${*}\`" >&2 return 1 @@ -66,8 +65,7 @@ function run_until_success_or_timeout { local STARTTIME=${SECONDS} shift 1 - until run "${@}" && [[ $status -eq 0 ]] - do + until run "${@}" && [[ $status -eq 0 ]]; do sleep 1 if (( SECONDS - STARTTIME > TIMEOUT )); then @@ -107,8 +105,7 @@ function wait_for_smtp_port_in_container() { function wait_for_smtp_port_in_container_to_respond() { local COUNT=0 until [[ $(docker exec "${1}" timeout 10 /bin/sh -c "echo QUIT | nc localhost 25") == *"221 2.0.0 Bye"* ]]; do - if [[ $COUNT -eq 20 ]] - then + if [[ $COUNT -eq 20 ]]; then echo "Unable to receive a valid response from 'nc localhost 25' within 20 seconds" return 1 fi diff --git a/test/tests/parallel/set1/config_overrides.bats b/test/tests/parallel/set1/config_overrides.bats index 0a7a5e7f4ad..0281d197722 100644 --- a/test/tests/parallel/set1/config_overrides.bats +++ b/test/tests/parallel/set1/config_overrides.bats @@ -15,6 +15,9 @@ function setup_file() { function teardown_file() { _default_teardown ; } +# The `postconf` command can query both `main.cf` and `master.cf` at `/etc/postfix/`. +# Reference: http://www.postfix.org/postconf.1.html + @test "Postfix - 'postfix-main.cf' overrides applied to '/etc/postfix/main.cf'" { _run_in_container grep -q 'max_idle = 600s' /tmp/docker-mailserver/postfix-main.cf assert_success @@ -37,6 +40,24 @@ function teardown_file() { _default_teardown ; } assert_output --partial '-o smtpd_sasl_security_options=noanonymous' } +# Custom parameter support works correctly: +# NOTE: This would only fail on a fresh container state, any restart would pass successfully: +# https://github.com/docker-mailserver/docker-mailserver/pull/3880 +@test "Postfix - 'postfix-master.cf' should apply before 'postfix-main.cf'" { + # Retrieve the value for this setting, `postfix-master.cf` should have the override set: + _run_in_container postconf -Ph 'submission/inet/smtpd_client_restrictions' + assert_success + refute_output --partial 'postconf: warning: /etc/postfix/master.cf: undefined parameter: custom_parameter' + #shellcheck disable=SC2016 + assert_output '$custom_parameter' + + # As it's a custom parameter (`$` prefix), ensure the parameter value expands correctly: + _run_in_container postconf -Phx 'submission/inet/smtpd_client_restrictions' + assert_success + refute_output --partial 'postconf: warning: /etc/postfix/master.cf: undefined parameter: custom_parameter' + assert_output 'cidr:{{!172.16.0.42 REJECT}}, permit_sasl_authenticated, reject' +} + @test "Dovecot - 'dovecot.cf' overrides applied to '/etc/dovecot/local.conf'" { _run_in_container grep -q 'mail_max_userip_connections = 69' /tmp/docker-mailserver/dovecot.cf assert_success diff --git a/test/tests/parallel/set1/dovecot/dovecot_quotas.bats b/test/tests/parallel/set1/dovecot/dovecot_quotas.bats new file mode 100644 index 00000000000..8e71bf7327b --- /dev/null +++ b/test/tests/parallel/set1/dovecot/dovecot_quotas.bats @@ -0,0 +1,246 @@ +load "${REPOSITORY_ROOT}/test/helper/common" +load "${REPOSITORY_ROOT}/test/helper/setup" + +# upstream default: 10 240 000 +# https://www.postfix.org/postconf.5.html#message_size_limit +# > The maximal size in bytes of a message, including envelope information. +# > The value cannot exceed LONG_MAX (typically, a 32-bit or 64-bit signed integer). +# > Note: Be careful when making changes. Excessively small values will result in the loss of non-delivery notifications, when a bounce message size exceeds the local or remote MTA's message size limit. + +# upstream default: 51 200 000 +# https://www.postfix.org/postconf.5.html#mailbox_size_limit +# > The maximal size of any local(8) individual mailbox or maildir file, or zero (no limit). +# > In fact, this limits the size of any file that is written to upon local delivery, including files written by external commands that are executed by the local(8) delivery agent. +# > The value cannot exceed LONG_MAX (typically, a 32-bit or 64-bit signed integer). +# > This limit must not be smaller than the message size limit. + +# upstream default: 51 200 000 +# https://www.postfix.org/postconf.5.html#virtual_mailbox_limit +# > The maximal size in bytes of an individual virtual(8) mailbox or maildir file, or zero (no limit). +# > This parameter is specific to the virtual(8) delivery agent. +# > It does not apply when mail is delivered with a different mail delivery program. + +BATS_TEST_NAME_PREFIX='[Dovecot Quotas] ' +CONTAINER_NAME='dms-test_dovecot-quotas' + +function setup_file() { + _init_with_defaults + + local CONTAINER_ARGS_ENV_CUSTOM=( + --env ENABLE_QUOTAS=1 + --env POSTFIX_MAILBOX_SIZE_LIMIT=4096000 + --env POSTFIX_MESSAGE_SIZE_LIMIT=2048000 + --env PERMIT_DOCKER=container + ) + _common_container_setup 'CONTAINER_ARGS_ENV_CUSTOM' +} + +function teardown_file() { _default_teardown ; } + +@test 'should only support setting quota for a valid account' { + # Prepare + _add_mail_account_then_wait_until_ready 'quota_user@domain.tld' + + # Actual tests + _run_in_container setup quota set quota_user 50M + assert_failure + + _run_in_container setup quota set username@fulldomain 50M + assert_failure + + _run_in_container setup quota set quota_user@domain.tld 50M + assert_success + + # Cleanup + _run_in_container setup email del -y quota_user@domain.tld + assert_success +} + +@test 'should only allow valid units as quota size' { + # Prepare + _add_mail_account_then_wait_until_ready 'quota_user@domain.tld' + + # Actual tests + _run_in_container setup quota set quota_user@domain.tld 26GIGOTS + assert_failure + _run_in_container setup quota set quota_user@domain.tld 123 + assert_failure + _run_in_container setup quota set quota_user@domain.tld M + assert_failure + _run_in_container setup quota set quota_user@domain.tld -60M + assert_failure + + + _run_in_container setup quota set quota_user@domain.tld 10B + assert_success + _run_in_container setup quota set quota_user@domain.tld 10k + assert_success + _run_in_container setup quota set quota_user@domain.tld 10M + assert_success + _run_in_container setup quota set quota_user@domain.tld 10G + assert_success + _run_in_container setup quota set quota_user@domain.tld 10T + assert_success + + # Cleanup + _run_in_container setup email del -y quota_user@domain.tld + assert_success +} + +@test 'should only support removing quota from a valid account' { + # Prepare + _add_mail_account_then_wait_until_ready 'quota_user@domain.tld' + + # Actual tests + _run_in_container setup quota del uota_user@domain.tld + assert_failure + _run_in_container setup quota del quota_user + assert_failure + _run_in_container setup quota del dontknowyou@domain.tld + assert_failure + + _run_in_container setup quota set quota_user@domain.tld 10T + assert_success + _run_in_container setup quota del quota_user@domain.tld + assert_success + _run_in_container grep -i 'quota_user@domain.tld' /tmp/docker-mailserver/dovecot-quotas.cf + assert_failure + + # Cleanup + _run_in_container setup email del -y quota_user@domain.tld + assert_success +} + +@test 'should not error when there is no quota to remove for an account' { + # Prepare + _add_mail_account_then_wait_until_ready 'quota_user@domain.tld' + + # Actual tests + _run_in_container grep -i 'quota_user@domain.tld' /tmp/docker-mailserver/dovecot-quotas.cf + assert_failure + + _run_in_container setup quota del quota_user@domain.tld + assert_success + _run_in_container setup quota del quota_user@domain.tld + assert_success + + # Cleanup + _run_in_container setup email del -y quota_user@domain.tld + assert_success +} + +@test 'should have configured Postfix to use the Dovecot quota-status service' { + _run_in_container postconf + assert_success + assert_output --partial 'check_policy_service inet:localhost:65265' +} + +@test '(ENV POSTFIX_MAILBOX_SIZE_LIMIT) should be configured for both Postfix and Dovecot' { + _run_in_container postconf -h mailbox_size_limit + assert_output 4096000 + + # Dovecot mailbox is sized by `virtual_mailbox_size` from Postfix: + _run_in_container postconf -h virtual_mailbox_limit + assert_output 4096000 + + # Quota support: + _run_in_container doveconf -h plugin/quota_rule + # Global default storage limit quota for each mailbox 4 MiB: + assert_output '*:storage=4M' + + # Sizes are equivalent - Bytes to MiB (rounded): + run numfmt --to=iec --format '%.0f' 4096000 + assert_output '4M' +} + +@test '(ENV POSTFIX_MESSAGE_SIZE_LIMIT) should be configured for both Postfix and Dovecot' { + _run_in_container postconf -h message_size_limit + assert_output 2048000 + + _run_in_container doveconf -h plugin/quota_max_mail_size + assert_output '2M' + + # Sizes are equivalent - Bytes to MiB (rounded): + run numfmt --to=iec --format '%.0f' 2048000 + assert_output '2M' +} + +@test 'Deleting an mailbox account should also remove that account from dovecot-quotas.cf' { + _add_mail_account_then_wait_until_ready 'quserremoved@domain.tld' + + _run_in_container setup quota set quserremoved@domain.tld 12M + assert_success + + _run_in_container cat '/tmp/docker-mailserver/dovecot-quotas.cf' + assert_success + assert_output 'quserremoved@domain.tld:12M' + + _run_in_container setup email del -y quserremoved@domain.tld + assert_success + + _run_in_container cat /tmp/docker-mailserver/dovecot-quotas.cf + assert_success + refute_output --partial 'quserremoved@domain.tld:12M' +} + +@test 'Dovecot should acknowledge quota configured for accounts' { + # sed -nE 's/.*STORAGE.*Limit=([0-9]+).*/\1/p' | numfmt --from-unit=1024 --to=iec --format '%.0f' + local CMD_GET_QUOTA="doveadm -f flow quota get -u 'user1@localhost.localdomain'" + + # 4M == 4096 kiB (numfmt --to-unit=1024 --from=iec 4M) + _run_in_container_bash "${CMD_GET_QUOTA}" + assert_line --partial 'Type=STORAGE Value=0 Limit=4096' + + # Setting a new limit for the user: + _run_in_container setup quota set 'user1@localhost.localdomain' 50M + assert_success + # 50M (50 * 1024^2) == 51200 kiB (numfmt --to-unit=1024 --from=iec 52428800) + run _repeat_until_success_or_timeout 20 _exec_in_container_bash "${CMD_GET_QUOTA} | grep -o 'Type=STORAGE Value=0 Limit=51200'" + assert_success + + # Deleting quota resets it to default global quota limit (`plugin/quota_rule`): + _run_in_container setup quota del 'user1@localhost.localdomain' + assert_success + run _repeat_until_success_or_timeout 20 _exec_in_container_bash "${CMD_GET_QUOTA} | grep -o 'Type=STORAGE Value=0 Limit=4096'" + assert_success +} + +@test 'should receive a warning mail from Dovecot when quota is exceeded' { + # skip 'disabled as it fails randomly: https://github.com/docker-mailserver/docker-mailserver/pull/2511' + + # Prepare + _add_mail_account_then_wait_until_ready 'quotauser@otherdomain.tld' + + # Actual tests + _run_in_container setup quota set quotauser@otherdomain.tld 10k + assert_success + + # wait until quota has been updated + run _repeat_until_success_or_timeout 20 _exec_in_container_bash "doveadm -f flow quota get -u 'quotauser@otherdomain.tld' | grep -o 'Type=STORAGE Value=0 Limit=10'" + assert_success + + # dovecot and postfix has been restarted + _wait_for_service postfix + _wait_for_service dovecot + sleep 10 + + # send some big emails + _send_email --to 'quotauser@otherdomain.tld' --data 'quota-exceeded.txt' + _send_email --to 'quotauser@otherdomain.tld' --data 'quota-exceeded.txt' + _send_email --to 'quotauser@otherdomain.tld' --data 'quota-exceeded.txt' + # check for quota warn message existence + run _repeat_until_success_or_timeout 20 _exec_in_container grep -R 'Subject: quota warning' /var/mail/otherdomain.tld/quotauser/new/ + assert_success + + run _repeat_until_success_or_timeout 20 sh -c "docker logs ${CONTAINER_NAME} | grep 'Quota exceeded (mailbox for user is full)'" + assert_success + + # ensure only the first big message and the warn message are present (other messages are rejected: mailbox is full) + _run_in_container sh -c 'ls /var/mail/otherdomain.tld/quotauser/new/ | wc -l' + assert_success + assert_output "2" + + # Cleanup + _run_in_container setup email del -y quotauser@otherdomain.tld + assert_success +} diff --git a/test/tests/parallel/set1/dovecot/dovecot_sieve.bats b/test/tests/parallel/set1/dovecot/dovecot_sieve.bats index 889890d243e..6acbc4a57b7 100644 --- a/test/tests/parallel/set1/dovecot/dovecot_sieve.bats +++ b/test/tests/parallel/set1/dovecot/dovecot_sieve.bats @@ -17,18 +17,18 @@ function setup_file() { --env ENABLE_MANAGESIEVE=1 # Required for mail delivery via nc: --env PERMIT_DOCKER=container - # Mount into mail dir for user1 to treat as a user-sieve: + # Mount into home dir for user1 to treat as a user-sieve: # NOTE: Cannot use ':ro', 'start-mailserver.sh' attempts to 'chown -R' /var/mail: - --volume "${TEST_TMP_CONFIG}/dovecot.sieve:/var/mail/localhost.localdomain/user1/.dovecot.sieve" + --volume "${TEST_TMP_CONFIG}/dovecot.sieve:/var/mail/localhost.localdomain/user1/home/.dovecot.sieve" ) _common_container_setup 'CONTAINER_ARGS_ENV_CUSTOM' _wait_for_smtp_port_in_container # Single mail sent from 'spam@spam.com' that is handled by User (relocate) and Global (copy) sieves for user1: - _send_email 'email-templates/sieve-spam-folder' + _send_email --data 'sieve/spam-folder.txt' # Mail for user2 triggers the sieve-pipe: - _send_email 'email-templates/sieve-pipe' + _send_email --to 'user2@otherdomain.tld' --data 'sieve/pipe.txt' _wait_for_empty_mail_queue_in_container } @@ -37,27 +37,24 @@ function teardown_file() { _default_teardown ; } # dovecot-sieve/dovecot.sieve @test "User Sieve - should store mail from 'spam@spam.com' into recipient (user1) mailbox 'INBOX.spam'" { - _run_in_container_bash 'ls -A /var/mail/localhost.localdomain/user1/.INBOX.spam/new' - assert_success - _should_output_number_of_lines 1 + _count_files_in_directory_in_container /var/mail/localhost.localdomain/user1/.INBOX.spam/new 1 } # dovecot-sieve/before.dovecot.sieve @test "Global Sieve - should have copied mail from 'spam@spam.com' to recipient (user1) inbox" { - _run_in_container grep 'Spambot ' -R /var/mail/localhost.localdomain/user1/new/ + _run_in_container grep -R 'Spambot ' /var/mail/localhost.localdomain/user1/new/ assert_success } # dovecot-sieve/sieve-pipe + dovecot-sieve/user2@otherdomain.tld.dovecot.sieve @test "Sieve Pipe - should pipe mail received for user2 into '/tmp/pipe-test.out'" { - _run_in_container_bash 'ls -A /tmp/pipe-test.out' + _run_in_container_bash '[[ -f /tmp/pipe-test.out ]]' assert_success - _should_output_number_of_lines 1 } # Only test coverage for feature is to check that the service is listening on the expected port: # https://doc.dovecot.org/admin_manual/pigeonhole_managesieve_server/ @test "ENV 'ENABLE_MANAGESIEVE' - should have enabled service on port 4190" { - _run_in_container_bash 'nc -z 0.0.0.0 4190' + _run_in_container nc -z 0.0.0.0 4190 assert_success } diff --git a/test/tests/parallel/set1/dovecot/mailbox_format_dbox.bats b/test/tests/parallel/set1/dovecot/mailbox_format_dbox.bats index 8ce03d9aee9..b9f6d8f6efc 100644 --- a/test/tests/parallel/set1/dovecot/mailbox_format_dbox.bats +++ b/test/tests/parallel/set1/dovecot/mailbox_format_dbox.bats @@ -26,7 +26,7 @@ function teardown() { _default_teardown ; } _common_container_setup 'CUSTOM_SETUP_ARGUMENTS' _wait_for_smtp_port_in_container - _send_email 'email-templates/existing-user1' + _send_email _wait_for_empty_mail_queue_in_container # Mail received should be stored as `u.1` (one file per message) @@ -47,7 +47,7 @@ function teardown() { _default_teardown ; } _common_container_setup 'CUSTOM_SETUP_ARGUMENTS' _wait_for_smtp_port_in_container - _send_email 'email-templates/existing-user1' + _send_email _wait_for_empty_mail_queue_in_container # Mail received should be stored in `m.1` (1 or more messages) diff --git a/test/tests/parallel/set1/dovecot/special_use_folders.bats b/test/tests/parallel/set1/dovecot/special_use_folders.bats index e70899a050c..4965b84452c 100644 --- a/test/tests/parallel/set1/dovecot/special_use_folders.bats +++ b/test/tests/parallel/set1/dovecot/special_use_folders.bats @@ -14,7 +14,7 @@ function setup_file() { function teardown_file() { _default_teardown ; } @test 'normal delivery works' { - _send_email 'email-templates/existing-user1' + _send_email _count_files_in_directory_in_container /var/mail/localhost.localdomain/user1/new 1 } @@ -26,7 +26,7 @@ function teardown_file() { _default_teardown ; } } @test "(IMAP) special-use folders should be created when necessary" { - _send_email 'nc_templates/imap_special_use_folders' '-w 8 0.0.0.0 143' + _nc_wrapper 'nc/imap_special_use_folders.txt' '-w 8 0.0.0.0 143' assert_output --partial 'Drafts' assert_output --partial 'Junk' assert_output --partial 'Trash' diff --git a/test/tests/parallel/set1/fetchmail.bats b/test/tests/parallel/set1/fetchmail.bats index 8d62731ad4c..fa497f6b9ef 100644 --- a/test/tests/parallel/set1/fetchmail.bats +++ b/test/tests/parallel/set1/fetchmail.bats @@ -60,6 +60,13 @@ function teardown_file() { _should_not_have_in_config 'pop3.third-party.test. ' /etc/fetchmailrc.d/fetchmail-2.rc } +@test "(ENV FETCHMAIL_PARALLEL=1) should not have logs emitted into fetchmail-x.rc" { + export CONTAINER_NAME=${CONTAINER2_NAME} + + _should_not_have_in_config 'INFO' /etc/fetchmailrc.d/fetchmail-1.rc + _should_not_have_in_config 'INFO' /etc/fetchmailrc.d/fetchmail-2.rc +} + function _should_have_in_config() { local MATCH_CONTENT=$1 local MATCH_IN_FILE=$2 diff --git a/test/tests/parallel/set1/getmail.bats b/test/tests/parallel/set1/getmail.bats new file mode 100644 index 00000000000..8d69ec3df72 --- /dev/null +++ b/test/tests/parallel/set1/getmail.bats @@ -0,0 +1,80 @@ +load "${REPOSITORY_ROOT}/test/helper/setup" +load "${REPOSITORY_ROOT}/test/helper/common" + +BATS_TEST_NAME_PREFIX='[Getmail] ' +CONTAINER_NAME='dms-test_getmail' + +function setup_file() { + + local CUSTOM_SETUP_ARGUMENTS=(--env 'ENABLE_GETMAIL=1') + _init_with_defaults + _common_container_setup 'CUSTOM_SETUP_ARGUMENTS' +} + +function teardown_file() { _default_teardown ; } + +#? The file used in the following tests is placed in test/config/getmail/user3.cf + +@test 'default configuration exists and is correct' { + _run_in_container cat /etc/getmailrc_general + assert_success + assert_line '[options]' + assert_line 'verbose = 0' + assert_line 'read_all = false' + assert_line 'delete = false' + assert_line 'max_messages_per_session = 500' + assert_line 'received = false' + assert_line 'delivered_to = false' + assert_line 'message_log_syslog = true' + + _run_in_container_bash '[[ -f /usr/local/bin/debug-getmail ]]' + assert_success + _run_in_container_bash '[[ -f /usr/local/bin/getmail-service.sh ]]' + assert_success +} + +@test 'debug-getmail works as expected' { + _run_in_container cat /etc/getmailrc.d/user3 + assert_success + assert_line '[options]' + assert_line 'verbose = 0' + assert_line 'read_all = false' + assert_line 'delete = false' + assert_line 'max_messages_per_session = 500' + assert_line 'received = false' + assert_line 'delivered_to = false' + assert_line 'message_log_syslog = true' + assert_line '[retriever]' + assert_line 'type = SimpleIMAPSSLRetriever' + assert_line 'server = imap.remote-service.test' + assert_line 'username = user3' + assert_line 'password=secret' + assert_line '[destination]' + assert_line 'type = MDA_external' + assert_line 'path = /usr/lib/dovecot/deliver' + assert_line 'allow_root_commands = true' + assert_line 'arguments =("-d","user3@example.test")' + + _run_in_container /usr/local/bin/debug-getmail + assert_success + assert_line --regexp 'retriever:.*SimpleIMAPSSLRetriever\(ca_certs="None", certfile="None", getmaildir="\/var\/lib\/getmail", imap_on_delete="None", imap_search="None", keyfile="None", mailboxes="\(.*INBOX.*\)", move_on_delete="None", password="\*", password_command="\(\)", port="993", record_mailbox="True", server="imap.remote-service.test", ssl_cert_hostname="None", ssl_ciphers="None", ssl_fingerprints="\(\)", ssl_version="None", timeout="180", use_cram_md5="False", use_kerberos="False", use_peek="True", use_xoauth2="False", username="user3"\)' + assert_line --regexp 'destination:.*MDA_external\(allow_root_commands="True", arguments="\(.*-d.*user3@example.test.*\)", command="deliver", group="None", ignore_stderr="False", path="\/usr\/lib\/dovecot\/deliver", pipe_stdout="True", unixfrom="False", user="None"\)' + assert_line ' delete : False' + assert_line ' delete_after : 0' + assert_line ' delete_bigger_than : 0' + assert_line ' delivered_to : False' + assert_line ' fingerprint : False' + assert_line ' max_bytes_per_session : 0' + assert_line ' max_message_size : 0' + assert_line ' max_messages_per_session : 500' + assert_line ' message_log : None' + assert_line ' message_log_syslog : True' + assert_line ' message_log_verbose : False' + assert_line ' netrc_file : None' + assert_line ' read_all : False' + assert_line ' received : False' + assert_line ' skip_imap_fetch_size : False' + assert_line ' to_oldmail_on_each_mail : False' + assert_line ' use_netrc : False' + assert_line ' verbose : 0' +} diff --git a/test/tests/parallel/set1/spam_virus/amavis.bats b/test/tests/parallel/set1/spam_virus/amavis.bats index 875127b1465..73c5b2ae1bb 100644 --- a/test/tests/parallel/set1/spam_virus/amavis.bats +++ b/test/tests/parallel/set1/spam_virus/amavis.bats @@ -2,8 +2,9 @@ load "${REPOSITORY_ROOT}/test/helper/common" load "${REPOSITORY_ROOT}/test/helper/setup" BATS_TEST_NAME_PREFIX='[Amavis + SA] ' -CONTAINER1_NAME='dms-test_amavis_enabled' -CONTAINER2_NAME='dms-test_amavis_disabled' +CONTAINER1_NAME='dms-test_amavis-enabled-default' +CONTAINER2_NAME='dms-test_amavis-enabled-custom' +CONTAINER3_NAME='dms-test_amavis-disabled' function setup_file() { export CONTAINER_NAME @@ -12,14 +13,26 @@ function setup_file() { _init_with_defaults local CUSTOM_SETUP_ARGUMENTS=( --env ENABLE_AMAVIS=1 - --env AMAVIS_LOGLEVEL=2 --env ENABLE_SPAMASSASSIN=1 ) _common_container_setup 'CUSTOM_SETUP_ARGUMENTS' CONTAINER_NAME=${CONTAINER2_NAME} _init_with_defaults - local CUSTOM_SETUP_ARGUMENTS=( + local CUSTOM_SETUP_ARGUMENTS=( + --env ENABLE_AMAVIS=1 + --env AMAVIS_LOGLEVEL=2 + --env ENABLE_SPAMASSASSIN=1 + --env SA_TAG=-5.0 + --env SA_TAG2=2.0 + --env SA_KILL=3.0 + --env SPAM_SUBJECT='***SPAM*** ' + ) + _common_container_setup 'CUSTOM_SETUP_ARGUMENTS' + + CONTAINER_NAME=${CONTAINER3_NAME} + _init_with_defaults + local CUSTOM_SETUP_ARGUMENTS=( --env ENABLE_AMAVIS=0 --env ENABLE_SPAMASSASSIN=0 ) @@ -27,11 +40,37 @@ function setup_file() { } function teardown_file() { - docker rm -f "${CONTAINER1_NAME}" "${CONTAINER2_NAME}" + docker rm -f "${CONTAINER1_NAME}" "${CONTAINER2_NAME}" "${CONTAINER3_NAME}" } -@test '(Amavis enabled) configuration should be correct' { +@test '(Amavis enabled - defaults) default Amavis config is correct' { export CONTAINER_NAME=${CONTAINER1_NAME} + local AMAVIS_DEFAULTS_FILE='/etc/amavis/conf.d/20-debian_defaults' + + _run_in_container grep 'sa_tag_level_deflt' "${AMAVIS_DEFAULTS_FILE}" + assert_success + assert_output --partial 'sa_tag_level_deflt = 2.0;' + + _run_in_container grep 'sa_tag2_level_deflt' "${AMAVIS_DEFAULTS_FILE}" + assert_success + # shellcheck disable=SC2016 + assert_output --partial '$sa_tag2_level_deflt = 6.31;' + + _run_in_container grep 'sa_kill_level_deflt' "${AMAVIS_DEFAULTS_FILE}" + assert_success + # shellcheck disable=SC2016 + assert_output --partial '$sa_kill_level_deflt = 10.0;' + + # This feature is handled by our SPAM_SUBJECT ENV through a sieve script instead. + # Thus the feature here should always be disabled via the 'undef' value. + _run_in_container grep 'sa_spam_subject_tag' "${AMAVIS_DEFAULTS_FILE}" + assert_success + # shellcheck disable=SC2016 + assert_output --partial '$sa_spam_subject_tag = undef;' +} + +@test '(Amavis enabled - custom) configuration should be correct' { + export CONTAINER_NAME=${CONTAINER2_NAME} _run_in_container postconf -h content_filter assert_success @@ -41,45 +80,47 @@ function teardown_file() { _run_in_container grep -F '127.0.0.1:10025' /etc/postfix/master.cf assert_success - _run_in_container_bash '[[ -f /etc/cron.d/amavisd-new.disabled ]]' - assert_failure - _run_in_container_bash '[[ -f /etc/cron.d/amavisd-new ]]' - assert_success + _file_does_not_exist_in_container /etc/cron.d/amavisd-new.disabled + _file_exists_in_container /etc/cron.d/amavisd-new } -@test '(Amavis enabled) SA integration should be active' { - export CONTAINER_NAME=${CONTAINER1_NAME} +@test '(Amavis enabled - custom) SA integration should be active' { + export CONTAINER_NAME=${CONTAINER2_NAME} # give Amavis just a bit of time to print out its full debug log - run _repeat_in_container_until_success_or_timeout 5 "${CONTAINER_NAME}" grep 'ANTI-SPAM-SA' /var/log/mail/mail.log + run _repeat_in_container_until_success_or_timeout 20 "${CONTAINER_NAME}" grep 'SpamControl: init_pre_fork on SpamAssassin done' /var/log/mail/mail.log assert_success - assert_output --partial 'loaded' - refute_output --partial 'NOT loaded' } -@test '(Amavis enabled) SA ENV should update Amavis config' { - export CONTAINER_NAME=${CONTAINER1_NAME} - +@test '(Amavis enabled - custom) ENV should update Amavis config' { + export CONTAINER_NAME=${CONTAINER2_NAME} local AMAVIS_DEFAULTS_FILE='/etc/amavis/conf.d/20-debian_defaults' - _run_in_container grep '\$sa_tag_level_deflt' "${AMAVIS_DEFAULTS_FILE}" + + _run_in_container grep 'sa_tag_level_deflt' "${AMAVIS_DEFAULTS_FILE}" assert_success - assert_output --partial '= 2.0' + # shellcheck disable=SC2016 + assert_output --partial '$sa_tag_level_deflt = -5.0;' - _run_in_container grep '\$sa_tag2_level_deflt' "${AMAVIS_DEFAULTS_FILE}" + _run_in_container grep 'sa_tag2_level_deflt' "${AMAVIS_DEFAULTS_FILE}" assert_success - assert_output --partial '= 6.31' + # shellcheck disable=SC2016 + assert_output --partial '$sa_tag2_level_deflt = 2.0;' - _run_in_container grep '\$sa_kill_level_deflt' "${AMAVIS_DEFAULTS_FILE}" + _run_in_container grep 'sa_kill_level_deflt' "${AMAVIS_DEFAULTS_FILE}" assert_success - assert_output --partial '= 6.31' + # shellcheck disable=SC2016 + assert_output --partial '$sa_kill_level_deflt = 3.0;' - _run_in_container grep '\$sa_spam_subject_tag' "${AMAVIS_DEFAULTS_FILE}" + # This feature is handled by our SPAM_SUBJECT ENV through a sieve script instead. + # Thus the feature here should always be disabled via the 'undef' value. + _run_in_container grep 'sa_spam_subject_tag' "${AMAVIS_DEFAULTS_FILE}" assert_success - assert_output --partial "= '***SPAM*** ';" + # shellcheck disable=SC2016 + assert_output --partial '$sa_spam_subject_tag = undef;' } @test '(Amavis disabled) configuration should be correct' { - export CONTAINER_NAME=${CONTAINER2_NAME} + export CONTAINER_NAME=${CONTAINER3_NAME} _run_in_container postconf -h content_filter assert_success @@ -89,8 +130,6 @@ function teardown_file() { _run_in_container grep -F '127.0.0.1:10025' /etc/postfix/master.cf assert_failure - _run_in_container_bash '[[ -f /etc/cron.d/amavisd-new.disabled ]]' - assert_success - _run_in_container_bash '[[ -f /etc/cron.d/amavisd-new ]]' - assert_failure + _file_exists_in_container /etc/cron.d/amavisd-new.disabled + _file_does_not_exist_in_container /etc/cron.d/amavisd-new } diff --git a/test/tests/parallel/set1/spam_virus/clamav.bats b/test/tests/parallel/set1/spam_virus/clamav.bats index 31608ef86aa..36149edc3b4 100644 --- a/test/tests/parallel/set1/spam_virus/clamav.bats +++ b/test/tests/parallel/set1/spam_virus/clamav.bats @@ -25,34 +25,33 @@ function setup_file() { _wait_for_service postfix _wait_for_smtp_port_in_container - _send_email 'email-templates/amavis-virus' + _send_email --from 'virus@external.tld' --data 'amavis/virus.txt' _wait_for_empty_mail_queue_in_container } function teardown_file() { _default_teardown ; } -@test "log files exist at /var/log/mail directory" { - _run_in_container_bash "ls -1 /var/log/mail/ | grep -E 'clamav|freshclam|mail.log' | wc -l" +@test 'log files exist at /var/log/mail directory' { + _run_in_container_bash "ls -1 /var/log/mail/ | grep -c -E 'clamav|freshclam|mail.log'" assert_success assert_output 3 } -@test "should be identified by Amavis" { - _run_in_container grep -i 'Found secondary av scanner ClamAV-clamscan' /var/log/mail/mail.log - assert_success +@test 'should be identified by Amavis' { + _service_log_should_contain_string 'mail' 'Found secondary av scanner ClamAV-clamscan' } -@test "freshclam cron is enabled" { +@test 'freshclam cron is enabled' { _run_in_container_bash "grep '/usr/bin/freshclam' -r /etc/cron.d" assert_success } -@test "env CLAMAV_MESSAGE_SIZE_LIMIT is set correctly" { +@test 'env CLAMAV_MESSAGE_SIZE_LIMIT is set correctly' { _run_in_container grep -q '^MaxFileSize 30M$' /etc/clamav/clamd.conf assert_success } -@test "rejects virus" { - _run_in_container_bash "grep 'Blocked INFECTED' /var/log/mail/mail.log | grep ' -> '" - assert_success +@test 'rejects virus' { + _service_log_should_contain_string 'mail' 'Blocked INFECTED' + assert_output --partial ' -> ' } diff --git a/test/tests/parallel/set1/spam_virus/disabled_clamav_spamassassin.bats b/test/tests/parallel/set1/spam_virus/disabled_clamav_spamassassin.bats index 8402422c308..16e4f5266bb 100644 --- a/test/tests/parallel/set1/spam_virus/disabled_clamav_spamassassin.bats +++ b/test/tests/parallel/set1/spam_virus/disabled_clamav_spamassassin.bats @@ -2,7 +2,7 @@ load "${REPOSITORY_ROOT}/test/helper/setup" load "${REPOSITORY_ROOT}/test/helper/common" BATS_TEST_NAME_PREFIX='[ClamAV + SA] (disabled) ' -CONTAINER_NAME='dms-test_clamav-spamassasin_disabled' +CONTAINER_NAME='dms-test_clamav-spamassassin_disabled' function setup_file() { _init_with_defaults @@ -12,28 +12,31 @@ function setup_file() { --env ENABLE_CLAMAV=0 --env ENABLE_SPAMASSASSIN=0 --env AMAVIS_LOGLEVEL=2 + --env PERMIT_DOCKER=container ) _common_container_setup 'CUSTOM_SETUP_ARGUMENTS' _wait_for_smtp_port_in_container - _send_email 'email-templates/existing-user1' + _send_email _wait_for_empty_mail_queue_in_container } function teardown_file() { _default_teardown ; } @test "ClamAV - Amavis integration should not be active" { - _run_in_container grep -i 'Found secondary av scanner ClamAV-clamscan' /var/log/mail/mail.log - assert_failure + _service_log_should_not_contain_string 'mail' 'Found secondary av scanner ClamAV-clamscan' } @test "SA - Amavis integration should not be active" { - _run_in_container_bash "grep -i 'ANTI-SPAM-SA code' /var/log/mail/mail.log | grep 'NOT loaded'" + # Wait until Amavis has finished initializing: + run _repeat_in_container_until_success_or_timeout 20 "${CONTAINER_NAME}" grep 'Deleting db files in /var/lib/amavis/db' /var/log/mail/mail.log assert_success + + # Amavis module for SA should not be loaded (`SpamControl: scanner SpamAssassin, module Amavis::SpamControl::SpamAssassin`): + _service_log_should_not_contain_string 'mail' 'scanner SpamAssassin' } @test "SA - should not have been called" { - _run_in_container grep -i 'connect to /var/run/clamav/clamd.ctl failed' /var/log/mail/mail.log - assert_failure + _service_log_should_not_contain_string 'mail' 'connect to /var/run/clamav/clamd.ctl failed' } diff --git a/test/tests/parallel/set1/spam_virus/fail2ban.bats b/test/tests/parallel/set1/spam_virus/fail2ban.bats index 8ce843fc953..5088fa1cab5 100644 --- a/test/tests/parallel/set1/spam_virus/fail2ban.bats +++ b/test/tests/parallel/set1/spam_virus/fail2ban.bats @@ -34,7 +34,7 @@ function teardown_file() { } @test "localhost is not banned because ignored" { - _run_in_container fail2ban-client status postfix-sasl + _run_in_container fail2ban-client status postfix assert_success refute_output --regexp '.*IP list:.*127\.0\.0\.1.*' @@ -49,8 +49,7 @@ function teardown_file() { } @test "fail2ban-jail.cf overrides" { - for FILTER in 'dovecot' 'postfix' 'postfix-sasl' - do + for FILTER in 'dovecot' 'postfix'; do _run_in_container fail2ban-client get "${FILTER}" bantime assert_output 1234 @@ -63,7 +62,6 @@ function teardown_file() { _run_in_container fail2ban-client -d assert_output --partial "['set', 'dovecot', 'addaction', 'nftables-multiport']" assert_output --partial "['set', 'postfix', 'addaction', 'nftables-multiport']" - assert_output --partial "['set', 'postfix-sasl', 'addaction', 'nftables-multiport']" done } @@ -74,17 +72,26 @@ function teardown_file() { @test "ban ip on multiple failed login" { CONTAINER1_IP=$(_get_container_ip "${CONTAINER1_NAME}") # Trigger a ban by failing to login twice: - CONTAINER_NAME=${CONTAINER2_NAME} _send_email 'auth/smtp-auth-login-wrong' "${CONTAINER1_IP} 465" - CONTAINER_NAME=${CONTAINER2_NAME} _send_email 'auth/smtp-auth-login-wrong' "${CONTAINER1_IP} 465" + for _ in {1..2}; do + CONTAINER_NAME=${CONTAINER2_NAME} _send_email --expect-rejection \ + --server "${CONTAINER1_IP}" \ + --port 465 \ + --auth PLAIN \ + --auth-user user1@localhost.localdomain \ + --auth-password wrongpassword + assert_failure + assert_output --partial 'authentication failed' + assert_output --partial 'No authentication type succeeded' + done # Checking that CONTAINER2_IP is banned in "${CONTAINER1_NAME}" CONTAINER2_IP=$(_get_container_ip "${CONTAINER2_NAME}") - run _repeat_in_container_until_success_or_timeout 10 "${CONTAINER_NAME}" /bin/bash -c "fail2ban-client status postfix-sasl | grep -F '${CONTAINER2_IP}'" + run _repeat_in_container_until_success_or_timeout 10 "${CONTAINER_NAME}" /bin/bash -c "fail2ban-client status postfix | grep -F '${CONTAINER2_IP}'" assert_success assert_output --partial 'Banned IP list:' # Checking that CONTAINER2_IP is banned by nftables - _run_in_container_bash 'nft list set inet f2b-table addr-set-postfix-sasl' + _run_in_container_bash 'nft list set inet f2b-table addr-set-postfix' assert_success assert_output --partial "elements = { ${CONTAINER2_IP} }" } @@ -92,17 +99,13 @@ function teardown_file() { # NOTE: Depends on previous test case, if no IP was banned at this point, it passes regardless.. @test "unban ip works" { CONTAINER2_IP=$(_get_container_ip "${CONTAINER2_NAME}") - _run_in_container fail2ban-client set postfix-sasl unbanip "${CONTAINER2_IP}" + _run_in_container fail2ban-client set postfix unbanip "${CONTAINER2_IP}" assert_success # Checking that CONTAINER2_IP is unbanned in "${CONTAINER1_NAME}" - _run_in_container fail2ban-client status postfix-sasl + _run_in_container fail2ban-client status postfix assert_success refute_output --partial "${CONTAINER2_IP}" - - # Checking that CONTAINER2_IP is unbanned by nftables - _run_in_container_bash 'nft list set inet f2b-table addr-set-postfix-sasl' - refute_output --partial "${CONTAINER2_IP}" } @test "bans work properly (single IP)" { @@ -149,7 +152,7 @@ function teardown_file() { @test "FAIL2BAN_BLOCKTYPE is really set to drop" { # ban IPs here manually so we can be sure something is inside the jails - for JAIL in dovecot postfix-sasl custom; do + for JAIL in dovecot custom; do _run_in_container fail2ban-client set "${JAIL}" banip 192.33.44.55 assert_success done @@ -157,11 +160,10 @@ function teardown_file() { _run_in_container nft list table inet f2b-table assert_success assert_output --partial 'tcp dport { 110, 143, 465, 587, 993, 995, 4190 } ip saddr @addr-set-dovecot drop' - assert_output --partial 'tcp dport { 25, 110, 143, 465, 587, 993, 995 } ip saddr @addr-set-postfix-sasl drop' assert_output --partial 'tcp dport { 25, 110, 143, 465, 587, 993, 995, 4190 } ip saddr @addr-set-custom drop' # unban the IPs previously banned to get a clean state again - for JAIL in dovecot postfix-sasl custom; do + for JAIL in dovecot custom; do _run_in_container fail2ban-client set "${JAIL}" unbanip 192.33.44.55 assert_success done diff --git a/test/tests/parallel/set1/spam_virus/postgrey_enabled.bats b/test/tests/parallel/set1/spam_virus/postgrey_enabled.bats index 3eaaca9de19..2609812e90b 100644 --- a/test/tests/parallel/set1/spam_virus/postgrey_enabled.bats +++ b/test/tests/parallel/set1/spam_virus/postgrey_enabled.bats @@ -44,24 +44,22 @@ function teardown_file() { _default_teardown ; } # The other spam checks in `main.cf:smtpd_recipient_restrictions` would interfere with testing postgrey. _run_in_container sed -i \ -e 's/permit_sasl_authenticated.*policyd-spf,$//g' \ - -e 's/reject_unauth_pipelining.*reject_unknown_recipient_domain,$//g' \ + -e 's/reject_invalid_helo_hostname.*reject_unknown_recipient_domain,$//g' \ -e 's/reject_rbl_client.*inet:127\.0\.0\.1:10023$//g' \ -e 's/smtpd_recipient_restrictions =/smtpd_recipient_restrictions = check_policy_service inet:127.0.0.1:10023/g' \ /etc/postfix/main.cf _reload_postfix # Send test mail (it should fail to deliver): - _send_test_mail '/tmp/docker-mailserver-test/email-templates/postgrey.txt' '25' + _send_email --expect-rejection --from 'user@external.tld' --port 25 --data 'postgrey.txt' + assert_failure + assert_output --partial 'Recipient address rejected: Delayed by Postgrey' # Confirm mail was greylisted: _should_have_log_entry \ 'action=greylist' \ 'reason=new' \ - 'client_address=127.0.0.1/32, sender=user@external.tld, recipient=user1@localhost.localdomain' - - _repeat_until_success_or_timeout 10 _run_in_container grep \ - 'Recipient address rejected: Delayed by Postgrey' \ - /var/log/mail/mail.log + 'client_address=127.0.0.1, sender=user@external.tld, recipient=user1@localhost.localdomain' } # NOTE: This test case depends on the previous one @@ -69,17 +67,18 @@ function teardown_file() { _default_teardown ; } # Wait until `$POSTGREY_DELAY` seconds pass before trying again: sleep 3 # Retry delivering test mail (it should be trusted this time): - _send_test_mail '/tmp/docker-mailserver-test/email-templates/postgrey.txt' '25' + _send_email --from 'user@external.tld' --port 25 --data 'postgrey.txt' # Confirm postgrey permitted delivery (triplet is now trusted): _should_have_log_entry \ 'action=pass' \ 'reason=triplet found' \ - 'client_address=127.0.0.1/32, sender=user@external.tld, recipient=user1@localhost.localdomain' + 'client_address=127.0.0.1, sender=user@external.tld, recipient=user1@localhost.localdomain' } - -# NOTE: These two whitelist tests use `test-files/nc_templates/` instead of `test-files/email-templates`. +# NOTE: These two whitelist tests use `files/nc/` instead of `files/emails`. +# `nc` option `-w 0` terminates the connection after sending the template, it does not wait for a response. +# This is required for port 10023, otherwise the connection never drops. # - This allows to bypass the SMTP protocol on port 25, and send data directly to Postgrey instead. # - Appears to be a workaround due to `client_name=localhost` when sent from Postfix. # - Could send over port 25 if whitelisting `localhost`, @@ -87,43 +86,32 @@ function teardown_file() { _default_teardown ; } # - It'd also cause the earlier greylist test to fail. # - TODO: Actually confirm whitelist feature works correctly as these test cases are using a workaround: @test "should whitelist sender 'user@whitelist.tld'" { - _send_test_mail '/tmp/docker-mailserver-test/nc_templates/postgrey_whitelist.txt' '10023' + _nc_wrapper 'nc/postgrey_whitelist.txt' '-w 0 0.0.0.0 10023' _should_have_log_entry \ 'action=pass' \ 'reason=client whitelist' \ - 'client_address=127.0.0.1/32, sender=test@whitelist.tld, recipient=user1@localhost.localdomain' + 'client_address=127.0.0.1, sender=test@whitelist.tld, recipient=user1@localhost.localdomain' } @test "should whitelist recipient 'user2@otherdomain.tld'" { - _send_test_mail '/tmp/docker-mailserver-test/nc_templates/postgrey_whitelist_recipients.txt' '10023' + _nc_wrapper 'nc/postgrey_whitelist_recipients.txt' '-w 0 0.0.0.0 10023' _should_have_log_entry \ 'action=pass' \ 'reason=recipient whitelist' \ - 'client_address=127.0.0.1/32, sender=test@nonwhitelist.tld, recipient=user2@otherdomain.tld' -} - -function _send_test_mail() { - local MAIL_TEMPLATE=$1 - local PORT=${2:-25} - - # `-w 0` terminates the connection after sending the template, it does not wait for a response. - # This is required for port 10023, otherwise the connection never drops. - # It could increase the number of seconds to wait for port 25 to allow for asserting a response, - # but that would enforce the delay in tests for port 10023. - _run_in_container_bash "nc -w 0 0.0.0.0 ${PORT} < ${MAIL_TEMPLATE}" + 'client_address=127.0.0.1, sender=test@nonwhitelist.tld, recipient=user2@otherdomain.tld' } function _should_have_log_entry() { - local ACTION=$1 - local REASON=$2 - local TRIPLET=$3 + local ACTION=${1} + local REASON=${2} + local TRIPLET=${3} # Allow some extra time for logs to update to avoids a false-positive failure: _run_until_success_or_timeout 10 _exec_in_container grep \ "${ACTION}, ${REASON}," \ - /var/log/mail/mail.log + /var/log/supervisor/postgrey.log # Log entry matched should be for the expected triplet: assert_output --partial "${TRIPLET}" @@ -132,5 +120,5 @@ function _should_have_log_entry() { # `lines` is a special BATS variable updated via `run`: function _should_output_number_of_lines() { - assert_equal "${#lines[@]}" $1 + assert_equal "${#lines[@]}" "${1}" } diff --git a/test/tests/parallel/set1/spam_virus/postscreen.bats b/test/tests/parallel/set1/spam_virus/postscreen.bats index 240ba58a606..3f93fe9d566 100644 --- a/test/tests/parallel/set1/spam_virus/postscreen.bats +++ b/test/tests/parallel/set1/spam_virus/postscreen.bats @@ -6,7 +6,7 @@ CONTAINER1_NAME='dms-test_postscreen_enforce' CONTAINER2_NAME='dms-test_postscreen_sender' function setup() { - CONTAINER1_IP=$(_get_container_ip ${CONTAINER1_NAME}) + CONTAINER1_IP=$(_get_container_ip "${CONTAINER1_NAME}") } function setup_file() { @@ -37,45 +37,30 @@ function teardown_file() { docker rm -f "${CONTAINER1_NAME}" "${CONTAINER2_NAME}" } +# `POSTSCREEN_ACTION=enforce` (DMS default) should reject delivery with a 550 SMTP reply +# A legitimate mail client should speak SMTP by waiting it's turn, which postscreen defaults enforce (only on port 25) +# https://www.postfix.org/postconf.5.html#postscreen_greet_wait +# +# Use `nc` to send all SMTP commands at once instead (emulate a misbehaving client that should be rejected) +# NOTE: Postscreen only runs on port 25, avoid implicit ports in test methods @test 'should fail send when talking out of turn' { - CONTAINER_NAME=${CONTAINER2_NAME} _send_email 'email-templates/postscreen' "${CONTAINER1_IP} 25" + CONTAINER_NAME=${CONTAINER2_NAME} _nc_wrapper 'emails/nc_raw/postscreen.txt' "${CONTAINER1_IP} 25" + # Expected postscreen log entry: assert_output --partial 'Protocol error' - # Expected postscreen log entry: - _run_in_container cat /var/log/mail/mail.log + _run_in_container cat /var/log/mail.log assert_output --partial 'COMMAND PIPELINING' + assert_output --partial 'DATA without valid RCPT' } @test "should successfully pass postscreen and get postfix greeting message (respecting postscreen_greet_wait time)" { - # NOTE: Sometimes fails on first attempt (trying too soon?), - # Instead of a `run` + asserting partial, Using repeat + internal grep match: - _repeat_until_success_or_timeout 10 _should_wait_turn_speaking_smtp \ - "${CONTAINER2_NAME}" \ - "${CONTAINER1_IP}" \ - '/tmp/docker-mailserver-test/email-templates/postscreen.txt' \ - '220 mail.example.test ESMTP' - - # Expected postscreen log entry: - _run_in_container cat /var/log/mail/mail.log - assert_output --partial 'PASS NEW' -} - -# When postscreen is active, it prevents the usual method of piping a file through nc: -# (Won't work: CONTAINER_NAME=${CLIENT_CONTAINER_NAME} _send_email "${SMTP_TEMPLATE}" "${TARGET_CONTAINER_IP} 25") -# The below workaround respects `postscreen_greet_wait` time (default 6 sec), talking to the mail-server in turn: -# https://www.postfix.org/postconf.5.html#postscreen_greet_wait -function _should_wait_turn_speaking_smtp() { - local CLIENT_CONTAINER_NAME=$1 - local TARGET_CONTAINER_IP=$2 - local SMTP_TEMPLATE=$3 - local EXPECTED=$4 - - local UGLY_WORKAROUND='exec 3<>/dev/tcp/'"${TARGET_CONTAINER_IP}"'/25 && \ - while IFS= read -r cmd; do \ - head -1 <&3; \ - [[ ${cmd} == "EHLO"* ]] && sleep 6; \ - echo ${cmd} >&3; \ - done < '"${SMTP_TEMPLATE}" + # Configure `send_email()` to send from the mail client container (CONTAINER2_NAME) via ENV override, + # mail is sent to the DMS server container (CONTAINER1_NAME) via `--server` parameter: + CONTAINER_NAME=${CONTAINER2_NAME} _send_email --expect-rejection --server "${CONTAINER1_IP}" --port 25 --data 'postscreen.txt' + # TODO: Use _send_email_with_msgid when proper resolution of domain names is possible: + # CONTAINER_NAME=${CONTAINER2_NAME} _send_email_with_msgid 'msgid-postscreen' --server "${CONTAINER1_IP}" --data 'postscreen.txt' + # _print_mail_log_for_msgid 'msgid-postscreen' + # assert_output --partial "stored mail into mailbox 'INBOX'" - docker exec "${CLIENT_CONTAINER_NAME}" bash -c "${UGLY_WORKAROUND}" | grep "${EXPECTED}" + _service_log_should_contain_string 'mail' 'PASS NEW' } diff --git a/test/tests/parallel/set1/spam_virus/rspamd.bats b/test/tests/parallel/set1/spam_virus/rspamd.bats deleted file mode 100644 index 6c3855531ce..00000000000 --- a/test/tests/parallel/set1/spam_virus/rspamd.bats +++ /dev/null @@ -1,155 +0,0 @@ -load "${REPOSITORY_ROOT}/test/helper/setup" -load "${REPOSITORY_ROOT}/test/helper/common" - -BATS_TEST_NAME_PREFIX='[Rspamd] ' -CONTAINER_NAME='dms-test_rspamd' - -function setup_file() { - _init_with_defaults - - # Comment for maintainers about `PERMIT_DOCKER=host`: - # https://github.com/docker-mailserver/docker-mailserver/pull/2815/files#r991087509 - local CUSTOM_SETUP_ARGUMENTS=( - --env ENABLE_CLAMAV=1 - --env ENABLE_RSPAMD=1 - --env ENABLE_OPENDKIM=0 - --env ENABLE_OPENDMARC=0 - --env PERMIT_DOCKER=host - --env LOG_LEVEL=trace - ) - - _common_container_setup 'CUSTOM_SETUP_ARGUMENTS' - - # wait for ClamAV to be fully setup or we will get errors on the log - _repeat_in_container_until_success_or_timeout 60 "${CONTAINER_NAME}" test -e /var/run/clamav/clamd.ctl - - _wait_for_service rspamd-redis - _wait_for_service rspamd - _wait_for_service clamav - _wait_for_service postfix - _wait_for_smtp_port_in_container - - # We will send 3 emails: the first one should pass just fine; the second one should - # be rejected due to spam; the third one should be rejected due to a virus. - export MAIL_ID1=$(_send_email_and_get_id 'email-templates/existing-user1') - export MAIL_ID2=$(_send_email_and_get_id 'email-templates/rspamd-spam') - export MAIL_ID3=$(_send_email_and_get_id 'email-templates/rspamd-virus') - - # add a nested option to a module - _exec_in_container_bash "echo -e 'complicated {\n anOption = someValue;\n}' >/etc/rspamd/override.d/testmodule_complicated.conf" -} - -function teardown_file() { _default_teardown ; } - -@test "Postfix's main.cf was adjusted" { - _run_in_container grep -F 'smtpd_milters = $rspamd_milter' /etc/postfix/main.cf - assert_success -} - -@test 'logs exist and contains proper content' { - _service_log_should_contain_string 'rspamd' 'rspamd .* is loading configuration' - _service_log_should_contain_string 'rspamd' 'lua module clickhouse is disabled in the configuration' - _service_log_should_contain_string 'rspamd' 'lua module elastic is disabled in the configuration' - _service_log_should_contain_string 'rspamd' 'lua module neural is disabled in the configuration' - _service_log_should_contain_string 'rspamd' 'lua module reputation is disabled in the configuration' - _service_log_should_contain_string 'rspamd' 'lua module spamassassin is disabled in the configuration' - _service_log_should_contain_string 'rspamd' 'lua module url_redirector is disabled in the configuration' - _service_log_should_contain_string 'rspamd' 'lua module metric_exporter is disabled in the configuration' -} - -@test 'normal mail passes fine' { - _service_log_should_contain_string 'rspamd' 'F \(no action\)' - - _print_mail_log_for_id "${MAIL_ID1}" - assert_output --partial "stored mail into mailbox 'INBOX'" -} - -@test 'detects and rejects spam' { - _service_log_should_contain_string 'rspamd' 'S \(reject\)' - _service_log_should_contain_string 'rspamd' 'reject "Gtube pattern"' - - _print_mail_log_for_id "${MAIL_ID2}" - assert_output --partial 'milter-reject' - assert_output --partial '5.7.1 Gtube pattern' -} - -@test 'detects and rejects virus' { - _service_log_should_contain_string 'rspamd' 'T \(reject\)' - _service_log_should_contain_string 'rspamd' 'reject "ClamAV FOUND VIRUS "Eicar-Signature"' - - _print_mail_log_for_id "${MAIL_ID3}" - assert_output --partial 'milter-reject' - assert_output --partial '5.7.1 ClamAV FOUND VIRUS "Eicar-Signature"' - refute_output --partial "stored mail into mailbox 'INBOX'" -} - -@test 'custom commands work correctly' { - # check `testmodule1` which should be disabled - local MODULE_PATH='/etc/rspamd/override.d/testmodule1.conf' - _run_in_container_bash "[[ -f ${MODULE_PATH} ]]" - assert_success - _run_in_container grep -F '# documentation: https://rspamd.com/doc/modules/testmodule1.html' "${MODULE_PATH}" - assert_success - _run_in_container grep -F 'enabled = false;' "${MODULE_PATH}" - assert_success - _run_in_container grep -F 'someoption = somevalue;' "${MODULE_PATH}" - assert_failure - - # check `testmodule2` which should be enabled and it should have extra options set - MODULE_PATH='/etc/rspamd/override.d/testmodule2.conf' - _run_in_container_bash "[[ -f ${MODULE_PATH} ]]" - assert_success - _run_in_container grep -F '# documentation: https://rspamd.com/doc/modules/testmodule2.html' "${MODULE_PATH}" - assert_success - _run_in_container grep -F 'enabled = true;' "${MODULE_PATH}" - assert_success - _run_in_container grep -F 'someoption = somevalue;' "${MODULE_PATH}" - assert_success - _run_in_container grep -F 'anotheroption = whatAvaLue;' "${MODULE_PATH}" - assert_success - - # check whether writing the same option twice overwrites the first value in `testmodule3` - MODULE_PATH='/etc/rspamd/override.d/testmodule3.conf' - _run_in_container grep -F 'someoption = somevalue;' "${MODULE_PATH}" - assert_failure - _run_in_container grep -F 'someoption = somevalue2;' "${MODULE_PATH}" - assert_success - - # check whether adding a single line writes the line properly in `testmodule4.something` - MODULE_PATH='/etc/rspamd/override.d/testmodule4.something' - _run_in_container_bash "[[ -f ${MODULE_PATH} ]]" - assert_success - _run_in_container grep -F 'some very long line with "weird $charact"ers' "${MODULE_PATH}" - assert_success - _run_in_container grep -F 'and! ano. ther &line' "${MODULE_PATH}" - assert_success - _run_in_container grep -F '# some comment' "${MODULE_PATH}" - assert_success - - # check whether spaces in front of options are handles properly in `testmodule_complicated` - MODULE_PATH='/etc/rspamd/override.d/testmodule_complicated.conf' - _run_in_container_bash "[[ -f ${MODULE_PATH} ]]" - assert_success - _run_in_container grep -F ' anOption = anotherValue;' "${MODULE_PATH}" - - # check whether controller option was written properly - MODULE_PATH='/etc/rspamd/override.d/worker-controller.inc' - _run_in_container_bash "[[ -f ${MODULE_PATH} ]]" - assert_success - _run_in_container grep -F 'someOption = someValue42;' "${MODULE_PATH}" - assert_success - - # check whether controller option was written properly - MODULE_PATH='/etc/rspamd/override.d/worker-proxy.inc' - _run_in_container_bash "[[ -f ${MODULE_PATH} ]]" - assert_success - _run_in_container grep -F 'abcdefg71 = RAAAANdooM;' "${MODULE_PATH}" - assert_success - - # check whether basic options are written properly - MODULE_PATH='/etc/rspamd/override.d/options.inc' - _run_in_container_bash "[[ -f ${MODULE_PATH} ]]" - assert_success - _run_in_container grep -F 'OhMy = "PraiseBeLinters !";' "${MODULE_PATH}" - assert_success -} diff --git a/test/tests/parallel/set1/spam_virus/rspamd_dkim.bats b/test/tests/parallel/set1/spam_virus/rspamd_dkim.bats new file mode 100644 index 00000000000..f5e030b5d35 --- /dev/null +++ b/test/tests/parallel/set1/spam_virus/rspamd_dkim.bats @@ -0,0 +1,275 @@ +load "${REPOSITORY_ROOT}/test/helper/common" +load "${REPOSITORY_ROOT}/test/helper/setup" + +BATS_TEST_NAME_PREFIX='[Rspamd] (DKIM) ' +CONTAINER_NAME='dms-test_rspamd-dkim' + +DOMAIN_NAME='example.test' +SIGNING_CONF_FILE='/tmp/docker-mailserver/rspamd/override.d/dkim_signing.conf' + +function setup_file() { + _init_with_defaults + + # Comment for maintainers about `PERMIT_DOCKER=host`: + # https://github.com/docker-mailserver/docker-mailserver/pull/2815/files#r991087509 + local CUSTOM_SETUP_ARGUMENTS=( + --env ENABLE_RSPAMD=1 + --env ENABLE_OPENDKIM=0 + --env ENABLE_OPENDMARC=0 + --env ENABLE_POLICYD_SPF=0 + --env LOG_LEVEL=trace + --env OVERRIDE_HOSTNAME="mail.${DOMAIN_NAME}" + ) + + _common_container_setup 'CUSTOM_SETUP_ARGUMENTS' + _wait_for_service rspamd-redis + _wait_for_service rspamd + _wait_for_rspamd_port_in_container +} + +# We want each test to start with a clean state. +function teardown() { + __remove_signing_config_file + _run_in_container rm -rf /tmp/docker-mailserver/rspamd/dkim + assert_success +} + +function teardown_file() { _default_teardown ; } + +@test 'log level is applied correctly' { + _run_in_container setup config dkim -vv help + __log_is_free_of_warnings_and_errors + assert_output --partial 'Enabled trace-logging' + + _run_in_container setup config dkim -v help + __log_is_free_of_warnings_and_errors + assert_output --partial 'Enabled debug-logging' +} + +@test 'help message is properly shown' { + _run_in_container setup config dkim help + __log_is_free_of_warnings_and_errors + assert_output --partial 'Showing usage message now' + assert_output --partial 'rspamd-dkim - Configure DKIM (DomainKeys Identified Mail)' +} + +@test 'default signing config is created if it does not exist and not overwritten' { + # Required pre-condition: no default configuration is present + __remove_signing_config_file + + __create_key + assert_success + __log_is_free_of_warnings_and_errors + assert_output --partial "Supplying a default configuration (to '${SIGNING_CONF_FILE}')" + refute_output --partial "'${SIGNING_CONF_FILE}' exists, not supplying a default" + assert_output --partial "Finished DKIM key creation" + _file_exists_in_container "${SIGNING_CONF_FILE}" + _exec_in_container_bash "echo 'blabla' >${SIGNING_CONF_FILE}" + local INITIAL_SHA512_SUM=$(_exec_in_container sha512sum "${SIGNING_CONF_FILE}") + + __create_key + assert_failure + assert_output --partial "Not overwriting existing files (use '--force' to overwrite existing files)" + + # the same as before, but with the '--force' option + __create_key 'rsa' 'mail' "${DOMAIN_NAME}" '2048' '--force' + __log_is_free_of_warnings_and_errors + refute_output --partial "Supplying a default configuration ('${SIGNING_CONF_FILE}')" + assert_output --partial "Overwriting existing files as the '--force' option was supplied" + assert_output --partial "'${SIGNING_CONF_FILE}' exists, not supplying a default" + assert_output --partial "Finished DKIM key creation" + local SECOND_SHA512_SUM=$(_exec_in_container sha512sum "${SIGNING_CONF_FILE}") + assert_equal "${INITIAL_SHA512_SUM}" "${SECOND_SHA512_SUM}" +} + +@test 'default directories and files are created' { + __create_key + assert_success + + _count_files_in_directory_in_container /tmp/docker-mailserver/rspamd/dkim/ 3 + _file_exists_in_container "${SIGNING_CONF_FILE}" + + __check_path_in_signing_config "/tmp/docker-mailserver/rspamd/dkim/rsa-2048-mail-${DOMAIN_NAME}.private.txt" + __check_selector_in_signing_config 'mail' +} + +@test "argument 'domain' is applied correctly" { + for DOMAIN in 'blabla.org' 'someother.com' 'random.de'; do + _run_in_container setup config dkim domain "${DOMAIN}" + assert_success + assert_line --partial "Domain set to '${DOMAIN}'" + + local BASE_FILE_NAME="/tmp/docker-mailserver/rspamd/dkim/rsa-2048-mail-${DOMAIN}" + __check_key_files_are_present "${BASE_FILE_NAME}" + __check_path_in_signing_config "${BASE_FILE_NAME}.private.txt" + __remove_signing_config_file + done +} + +@test "argument 'keytype' is applied correctly" { + _run_in_container setup config dkim keytype foobar + assert_failure + assert_line --partial "Unknown keytype 'foobar'" + + for KEYTYPE in 'rsa' 'ed25519'; do + _run_in_container setup config dkim keytype "${KEYTYPE}" + assert_success + assert_line --partial "Keytype set to '${KEYTYPE}'" + + local BASE_FILE_NAME="/tmp/docker-mailserver/rspamd/dkim/ed25519-mail-${DOMAIN_NAME}" + [[ ${KEYTYPE} == 'rsa' ]] && BASE_FILE_NAME="/tmp/docker-mailserver/rspamd/dkim/rsa-2048-mail-${DOMAIN_NAME}" + __check_key_files_are_present "${BASE_FILE_NAME}" + + _run_in_container grep ".*k=${KEYTYPE};.*" "${BASE_FILE_NAME}.public.txt" + assert_success + _run_in_container grep ".*k=${KEYTYPE};.*" "${BASE_FILE_NAME}.public.dns.txt" + assert_success + __check_path_in_signing_config "${BASE_FILE_NAME}.private.txt" + __remove_signing_config_file + done +} + +@test "argument 'selector' is applied correctly" { + for SELECTOR in 'foo' 'bar' 'baz'; do + __create_key 'rsa' "${SELECTOR}" + assert_success + assert_line --partial "Selector set to '${SELECTOR}'" + + local BASE_FILE_NAME="/tmp/docker-mailserver/rspamd/dkim/rsa-2048-${SELECTOR}-${DOMAIN_NAME}" + __check_key_files_are_present "${BASE_FILE_NAME}" + _run_in_container grep "^${SELECTOR}\._domainkey.*" "${BASE_FILE_NAME}.public.txt" + assert_success + + __check_rsa_keys 2048 "${SELECTOR}-${DOMAIN_NAME}" + __check_path_in_signing_config "${BASE_FILE_NAME}.private.txt" + __check_selector_in_signing_config "${SELECTOR}" + __remove_signing_config_file + done +} + +@test "argument 'keysize' is applied correctly for RSA keys" { + for KEYSIZE in 1024 2048 4096; do + __create_key 'rsa' 'mail' "${DOMAIN_NAME}" "${KEYSIZE}" + assert_success + __log_is_free_of_warnings_and_errors + assert_line --partial "Keysize set to '${KEYSIZE}'" + __check_rsa_keys "${KEYSIZE}" "mail-${DOMAIN_NAME}" + __remove_signing_config_file + done +} + +@test "when 'keytype=ed25519' is set, setting custom 'keysize' is rejected" { + __create_key 'ed25519' 'mail' "${DOMAIN_NAME}" 4096 + assert_failure + assert_line --partial "Chosen keytype does not accept the 'keysize' argument" +} + +@test "setting all arguments to a custom value works" { + local KEYTYPE='ed25519' + local SELECTOR='someselector' + local DOMAIN='dms.org' + + __create_key "${KEYTYPE}" "${SELECTOR}" "${DOMAIN}" + assert_success + __log_is_free_of_warnings_and_errors + + assert_line --partial "Keytype set to '${KEYTYPE}'" + assert_line --partial "Selector set to '${SELECTOR}'" + assert_line --partial "Domain set to '${DOMAIN}'" + + local BASE_FILE_NAME="/tmp/docker-mailserver/rspamd/dkim/${KEYTYPE}-${SELECTOR}-${DOMAIN}" + __check_path_in_signing_config "${BASE_FILE_NAME}.private.txt" + __check_selector_in_signing_config 'someselector' +} + +# Create DKIM keys. +# +# @param ${1} = keytype (default: rsa) +# @param ${2} = selector (default: mail) +# @param ${3} = domain (default: ${DOMAIN}) +# @param ${4} = keysize (default: 2048) +function __create_key() { + local KEYTYPE=${1:-rsa} + local SELECTOR=${2:-mail} + local DOMAIN=${3:-${DOMAIN_NAME}} + local KEYSIZE=${4:-2048} + local FORCE=${5:-} + + # Not quoting is intended here as we would otherwise provide + # the argument "''" (empty string), which would cause errors + # shellcheck disable=SC2086 + _run_in_container setup config dkim ${FORCE} \ + keytype "${KEYTYPE}" \ + keysize "${KEYSIZE}" \ + selector "${SELECTOR}" \ + domain "${DOMAIN}" +} + +# Check whether an RSA key is created successfully and correctly +# for a specific key size. +# +# @param ${1} = key size +# @param ${2} = name of the selector and domain name (as one string) +function __check_rsa_keys() { + local KEYSIZE=${1:?Keysize must be supplied to __check_rsa_keys} + local SELECTOR_AND_DOMAIN=${2:?Selector and domain name must be supplied to __check_rsa_keys} + local BASE_FILE_NAME="/tmp/docker-mailserver/rspamd/dkim/rsa-${KEYSIZE}-${SELECTOR_AND_DOMAIN}" + + __check_key_files_are_present "${BASE_FILE_NAME}" + __check_path_in_signing_config "${BASE_FILE_NAME}.private.txt" + + # Check the private key matches the specification + _run_in_container_bash "openssl rsa -in '${BASE_FILE_NAME}.private.txt' -noout -text" + assert_success + assert_line --index 0 "Private-Key: (${KEYSIZE} bit, 2 primes)" + + # Check the public key matches the specification + # + # We utilize the file for the DNS record contents which is already created + # by the Rspamd DKIM helper script. This makes parsing easier here. + local PUBKEY PUBKEY_INFO + PUBKEY=$(_exec_in_container_bash "grep -o 'p=.*' ${BASE_FILE_NAME}.public.dns.txt") + _run_in_container_bash "openssl enc -base64 -d <<< ${PUBKEY#p=} | openssl pkey -inform DER -pubin -noout -text" + assert_success + assert_line --index 0 "Public-Key: (${KEYSIZE} bit)" +} + +# Verify that all DKIM key files are present. +# +# @param ${1} = base file name that all DKIM key files have +function __check_key_files_are_present() { + local BASE_FILE_NAME="${1:?Base file name must be supplied to __check_key_files_are_present}" + for FILE in ${BASE_FILE_NAME}.{public.txt,public.dns.txt,private.txt}; do + _file_exists_in_container "${FILE}" + done +} + +# Check whether `path = .*` is set correctly in the signing configuration file. +# +# @param ${1} = file name that `path` should be set to +function __check_path_in_signing_config() { + local BASE_FILE_NAME=${1:?Base file name must be supplied to __check_path_in_signing_config} + _run_in_container grep "[[:space:]]*path = \"${BASE_FILE_NAME}\";" "${SIGNING_CONF_FILE}" + assert_success +} + +# Check whether `selector = .*` is set correctly in the signing configuration file. +# +# @param ${1} = name that `selector` should be set to +function __check_selector_in_signing_config() { + local SELECTOR=${1:?Selector name must be supplied to __check_selector_in_signing_config} + _run_in_container grep "[[:space:]]*selector = \"${SELECTOR}\";" "${SIGNING_CONF_FILE}" + assert_success +} + +# Check whether the script output is free of warnings and errors. +function __log_is_free_of_warnings_and_errors() { + assert_success + refute_output --partial '[ WARN ]' + refute_output --partial '[ ERROR ]' +} + +# Remove the signing configuration file inside the container. +function __remove_signing_config_file() { + _exec_in_container rm -f "${SIGNING_CONF_FILE}" +} diff --git a/test/tests/parallel/set1/spam_virus/rspamd_full.bats b/test/tests/parallel/set1/spam_virus/rspamd_full.bats new file mode 100644 index 00000000000..276b4c7a44b --- /dev/null +++ b/test/tests/parallel/set1/spam_virus/rspamd_full.bats @@ -0,0 +1,365 @@ +load "${REPOSITORY_ROOT}/test/helper/setup" +load "${REPOSITORY_ROOT}/test/helper/common" + +# This file tests Rspamd when all of its features are enabled, and +# all other interfering features are disabled. +BATS_TEST_NAME_PREFIX='[Rspamd] (full) ' +CONTAINER_NAME='dms-test_rspamd-full' + +function setup_file() { + _init_with_defaults + + # Comment for maintainers about `PERMIT_DOCKER=host`: + # https://github.com/docker-mailserver/docker-mailserver/pull/2815/files#r991087509 + local CUSTOM_SETUP_ARGUMENTS=( + --env ENABLE_AMAVIS=0 + --env ENABLE_SPAMASSASSIN=0 + --env ENABLE_CLAMAV=1 + --env ENABLE_RSPAMD=1 + --env ENABLE_OPENDKIM=0 + --env ENABLE_OPENDMARC=0 + --env ENABLE_POLICYD_SPF=0 + --env ENABLE_POSTGREY=0 + --env CLAMAV_MESSAGE_SIZE_LIMIT=42M + --env PERMIT_DOCKER=host + --env LOG_LEVEL=trace + --env MOVE_SPAM_TO_JUNK=1 + --env RSPAMD_LEARN=1 + --env RSPAMD_CHECK_AUTHENTICATED=0 + --env RSPAMD_GREYLISTING=1 + --env RSPAMD_HFILTER=1 + --env RSPAMD_HFILTER_HOSTNAME_UNKNOWN_SCORE=7 + --env SPAM_SUBJECT='[POTENTIAL SPAM] ' + ) + + cp -r "${TEST_TMP_CONFIG}"/rspamd_full/* "${TEST_TMP_CONFIG}/" + _common_container_setup 'CUSTOM_SETUP_ARGUMENTS' + + # wait for ClamAV to be fully setup or we will get errors on the log + _repeat_in_container_until_success_or_timeout 60 "${CONTAINER_NAME}" test -e /var/run/clamav/clamd.ctl + + _wait_for_service rspamd-redis + _wait_for_service rspamd + _wait_for_rspamd_port_in_container + _wait_for_service clamav + _wait_for_service postfix + _wait_for_smtp_port_in_container + + # We will send 5 emails: + # 1. The first ones should pass just fine + _send_email_with_msgid 'rspamd-test-email-pass' + _send_email_with_msgid 'rspamd-test-email-pass-gtube' \ + --body 'AJS*C4JDBQADN1.NSBN3*2IDNEN*GTUBE-STANDARD-ANTI-UBE-TEST-EMAIL*C.34X' + # 2. The second one should be rejected (Rspamd-specific GTUBE pattern for rejection) + _send_spam --expect-rejection + # 3. The third one should be rejected due to a virus (ClamAV EICAR pattern) + # shellcheck disable=SC2016 + _send_email_with_msgid 'rspamd-test-email-virus' --expect-rejection \ + --body 'X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*' + # 4. The fourth one will receive an added header (Rspamd-specific GTUBE pattern for adding a spam header) + # ref: https://rspamd.com/doc/other/gtube_patterns.html + _send_email_with_msgid 'rspamd-test-email-header' \ + --body "YJS*C4JDBQADN1.NSBN3*2IDNEN*GTUBE-STANDARD-ANTI-UBE-TEST-EMAIL*C.34X" + # 5. The fifth one will have its subject rewritten, but now spam header is applied. + _send_email_with_msgid 'rspamd-test-email-rewrite_subject' \ + --body "ZJS*C4JDBQADN1.NSBN3*2IDNEN*GTUBE-STANDARD-ANTI-UBE-TEST-EMAIL*C.34X" + + _run_in_container cat /var/log/mail.log + assert_success + refute_output --partial 'inet:localhost:11332: Connection refused' +} + +function teardown_file() { _default_teardown ; } + +@test "Postfix's main.cf was adjusted" { + # shellcheck disable=SC2016 + _run_in_container grep -F 'smtpd_milters = $rspamd_milter' /etc/postfix/main.cf + assert_success + _run_in_container postconf rspamd_milter + assert_success + assert_output 'rspamd_milter = inet:localhost:11332' +} + +@test 'Rspamd base configuration is correct' { + _run_in_container rspamadm configdump actions + assert_success + assert_line 'greylist = 4;' + assert_line 'reject = 11;' + assert_line 'add_header = 6;' + refute_line --regexp 'rewrite_subject = [0-9]+;' +} + +@test 'Rspamd Redis configuration is correct' { + _run_in_container rspamadm configdump redis + assert_success + assert_line 'servers = "127.0.0.1:6379";' + + _run_in_container rspamadm configdump history_redis + assert_success + assert_line 'compress = true;' + assert_line 'key_prefix = "rs_history{{COMPRESS}}";' +} + +@test "contents of '/etc/rspamd/override.d/' are copied" { + local OVERRIDE_D='/etc/rspamd/override.d' + _file_exists_in_container "${OVERRIDE_D}/testmodule_complicated.conf" +} + +@test 'startup log shows all features as properly enabled' { + run docker logs "${CONTAINER_NAME}" + assert_success + assert_line --partial 'Enabling ClamAV integration' + assert_line --partial 'Adjusting maximum size for ClamAV to 42000000 bytes (42M)' + assert_line --partial 'Setting up intelligent learning of spam and ham' + assert_line --partial 'Enabling greylisting' + assert_line --partial 'Hfilter (group) module is enabled' + assert_line --partial "Adjusting score for 'HFILTER_HOSTNAME_UNKNOWN' in Hfilter group module to" + assert_line --partial "Spam subject is set - the prefix '[POTENTIAL SPAM] ' will be added to spam e-mails" + assert_line --partial "Found file '/tmp/docker-mailserver/rspamd/custom-commands.conf' - parsing and applying it" +} + +@test 'service log exist and contains proper content' { + _service_log_should_contain_string_regexp 'rspamd' 'rspamd .* is loading configuration' + _service_log_should_contain_string 'rspamd' 'lua module clickhouse is disabled in the configuration' + _service_log_should_contain_string 'rspamd' 'lua module elastic is disabled in the configuration' + _service_log_should_contain_string 'rspamd' 'lua module neural is disabled in the configuration' + _service_log_should_contain_string 'rspamd' 'lua module reputation is disabled in the configuration' + _service_log_should_contain_string 'rspamd' 'lua module spamassassin is disabled in the configuration' + _service_log_should_contain_string 'rspamd' 'lua module url_redirector is disabled in the configuration' + _service_log_should_contain_string 'rspamd' 'lua module metric_exporter is disabled in the configuration' +} + +@test 'antivirus maximum size was adjusted' { + _run_in_container grep 'max_size = 42000000' /etc/rspamd/local.d/antivirus.conf + assert_success +} + +@test 'normal mail passes fine' { + _service_log_should_contain_string 'rspamd' 'F (no action)' + _service_log_should_contain_string 'rspamd' 'S (no action)' + + _print_mail_log_for_msgid 'rspamd-test-email-pass' + assert_output --partial "stored mail into mailbox 'INBOX'" + + _count_files_in_directory_in_container /var/mail/localhost.localdomain/user1/new/ 3 +} + +@test 'detects and rejects spam' { + _service_log_should_contain_string 'rspamd' 'S (reject)' + _service_log_should_contain_string 'rspamd' 'reject "Gtube pattern"' + + _print_mail_log_of_queue_id_from_msgid 'dms-test-email-spam' + assert_output --partial 'milter-reject' + assert_output --partial '5.7.1 Gtube pattern' + + _print_mail_log_for_msgid 'dms-test-email-spam' + refute_output --partial "stored mail into mailbox 'INBOX'" + assert_failure + + _count_files_in_directory_in_container /var/mail/localhost.localdomain/user1/new/ 3 +} + +@test 'detects and rejects virus' { + _service_log_should_contain_string 'rspamd' 'T (reject)' + _service_log_should_contain_string 'rspamd' 'reject "ClamAV FOUND VIRUS "Eicar-Signature"' + + _print_mail_log_of_queue_id_from_msgid 'rspamd-test-email-virus' + assert_output --partial 'milter-reject' + assert_output --partial '5.7.1 ClamAV FOUND VIRUS "Eicar-Signature"' + + _print_mail_log_for_msgid 'dms-test-email-spam' + refute_output --partial "stored mail into mailbox 'INBOX'" + assert_failure + + _count_files_in_directory_in_container /var/mail/localhost.localdomain/user1/new/ 3 +} + +@test 'custom commands work correctly' { + # check `testmodule1` which should be disabled + local MODULE_PATH='/etc/rspamd/override.d/testmodule1.conf' + _file_exists_in_container "${MODULE_PATH}" + _run_in_container grep -F '# documentation: https://rspamd.com/doc/modules/testmodule1.html' "${MODULE_PATH}" + assert_success + _run_in_container grep -F 'enabled = false;' "${MODULE_PATH}" + assert_success + _run_in_container grep -F 'someoption = somevalue;' "${MODULE_PATH}" + assert_failure + + # check `testmodule2` which should be enabled and it should have extra options set + MODULE_PATH='/etc/rspamd/override.d/testmodule2.conf' + _file_exists_in_container "${MODULE_PATH}" + _run_in_container grep -F '# documentation: https://rspamd.com/doc/modules/testmodule2.html' "${MODULE_PATH}" + assert_success + _run_in_container grep -F 'enabled = true;' "${MODULE_PATH}" + assert_success + _run_in_container grep -F 'someoption = somevalue;' "${MODULE_PATH}" + assert_success + _run_in_container grep -F 'anotheroption = whatAvaLue;' "${MODULE_PATH}" + assert_success + + # check whether writing the same option twice overwrites the first value in `testmodule3` + MODULE_PATH='/etc/rspamd/override.d/testmodule3.conf' + _run_in_container grep -F 'someoption = somevalue;' "${MODULE_PATH}" + assert_failure + _run_in_container grep -F 'someoption = somevalue2;' "${MODULE_PATH}" + assert_success + + # check whether adding a single line writes the line properly in `testmodule4.something` + MODULE_PATH='/etc/rspamd/override.d/testmodule4.something' + _file_exists_in_container "${MODULE_PATH}" + # shellcheck disable=SC2016 + _run_in_container grep -F 'some very long line with "weird $charact"ers' "${MODULE_PATH}" + assert_success + _run_in_container grep -F 'and! ano. ther &line' "${MODULE_PATH}" + assert_success + _run_in_container grep -F '# some comment' "${MODULE_PATH}" + assert_success + + # check whether spaces in front of options are handles properly in `testmodule_complicated` + MODULE_PATH='/etc/rspamd/override.d/testmodule_complicated.conf' + _file_exists_in_container "${MODULE_PATH}" + _run_in_container grep -F ' anOption = anotherValue;' "${MODULE_PATH}" + + # check whether controller option was written properly + MODULE_PATH='/etc/rspamd/override.d/worker-controller.inc' + _file_exists_in_container "${MODULE_PATH}" + _run_in_container grep -F 'someOption = someValue42;' "${MODULE_PATH}" + assert_success + + # check whether controller option was written properly + MODULE_PATH='/etc/rspamd/override.d/worker-proxy.inc' + _file_exists_in_container "${MODULE_PATH}" + _run_in_container grep -F 'abcdefg71 = RAAAANdooM;' "${MODULE_PATH}" + assert_success + + # check whether basic options are written properly + MODULE_PATH='/etc/rspamd/override.d/options.inc' + _file_exists_in_container "${MODULE_PATH}" + _run_in_container grep -F 'OhMy = "PraiseBeLinters !";' "${MODULE_PATH}" + assert_success +} + +@test 'MOVE_SPAM_TO_JUNK works for Rspamd' { + _file_exists_in_container /usr/lib/dovecot/sieve-global/after/spam_to_junk.sieve + _file_exists_in_container /usr/lib/dovecot/sieve-global/after/spam_to_junk.svbin + + _service_log_should_contain_string 'rspamd' 'S (add header)' + _service_log_should_contain_string 'rspamd' 'add header "Gtube pattern"' + + _print_mail_log_for_msgid 'rspamd-test-email-header' + assert_output --partial "fileinto action: stored mail into mailbox [SPECIAL-USE \\Junk]" + + _count_files_in_directory_in_container /var/mail/localhost.localdomain/user1/new/ 3 + _count_files_in_directory_in_container /var/mail/localhost.localdomain/user1/.Junk/new/ 1 +} + +@test 'Rewriting subject works when enforcing it via GTUBE' { + _service_log_should_contain_string 'rspamd' 'S (rewrite subject)' + _service_log_should_contain_string 'rspamd' 'rewrite subject "Gtube pattern"' + + _print_mail_log_for_msgid 'rspamd-test-email-rewrite_subject' + assert_output --partial "stored mail into mailbox 'INBOX'" + + # check that the inbox contains the subject-rewritten e-mail + _run_in_container_bash "grep --fixed-strings 'Subject: *** SPAM ***' /var/mail/localhost.localdomain/user1/new/*" + assert_success + + # check that the inbox contains the normal e-mail (that passes just fine) + _run_in_container_bash "grep --fixed-strings 'Subject: test' /var/mail/localhost.localdomain/user1/new/*" + assert_success +} + +@test 'SPAM_SUBJECT works' { + _file_exists_in_container /usr/lib/dovecot/sieve-global/before/spam_subject.sieve + _file_exists_in_container /usr/lib/dovecot/sieve-global/before/spam_subject.svbin + + # we only have one e-mail in the junk folder, hence using '*' is fine + _run_in_container_bash "grep --fixed-strings 'Subject: [POTENTIAL SPAM]' /var/mail/localhost.localdomain/user1/.Junk/new/*" + assert_success +} + +@test 'RSPAMD_LEARN works' { + for FILE in learn-{ham,spam}.{sieve,svbin}; do + _file_exists_in_container "/usr/lib/dovecot/sieve-pipe/${FILE}" + done + + _run_in_container grep 'mail_plugins.*imap_sieve' /etc/dovecot/conf.d/20-imap.conf + assert_success + local SIEVE_CONFIG_FILE='/etc/dovecot/conf.d/90-sieve.conf' + _run_in_container grep 'sieve_plugins.*sieve_imapsieve' "${SIEVE_CONFIG_FILE}" + assert_success + _run_in_container grep -F 'sieve_pipe_bin_dir = /usr/lib/dovecot/sieve-pipe' "${SIEVE_CONFIG_FILE}" + assert_success + + local LEARN_SPAM_LINES=( + 'imapsieve: mailbox Junk: MOVE event' + "sieve: file storage: script: Opened script \`learn-spam'" + 'sieve: file storage: Using Sieve script path: /usr/lib/dovecot/sieve-pipe/learn-spam.sieve' + "sieve: Executing script from \`/usr/lib/dovecot/sieve-pipe/learn-spam.svbin'" + "Finished running script \`/usr/lib/dovecot/sieve-pipe/learn-spam.svbin'" + 'sieve: action pipe: running program: rspamc' + "pipe action: piped message to program \`rspamc'" + "left message in mailbox 'Junk'" + ) + + local LEARN_HAM_LINES=( + "sieve: file storage: script: Opened script \`learn-ham'" + 'sieve: file storage: Using Sieve script path: /usr/lib/dovecot/sieve-pipe/learn-ham.sieve' + "sieve: Executing script from \`/usr/lib/dovecot/sieve-pipe/learn-ham.svbin'" + "Finished running script \`/usr/lib/dovecot/sieve-pipe/learn-ham.svbin'" + "left message in mailbox 'INBOX'" + ) + + # Move an email to the "Junk" folder from "INBOX"; the first email we + # sent should pass fine, hence we can now move it. + _nc_wrapper 'nc/rspamd_imap_move_to_junk.txt' '0.0.0.0 143' + sleep 1 # wait for the transaction to finish + + _service_log_should_contain_string 'mail' 'imapsieve: Matched static mailbox rule [1]' + _service_log_should_not_contain_string 'mail' 'imapsieve: Matched static mailbox rule [2]' + + _show_complete_mail_log + for LINE in "${LEARN_SPAM_LINES[@]}"; do + assert_output --partial "${LINE}" + done + + # Move an email to the "INBOX" folder from "Junk"; there should be two mails + # in the "Junk" folder, since the second email we sent during setup should + # have landed in the Junk folder already. + _nc_wrapper 'nc/rspamd_imap_move_to_inbox.txt' '0.0.0.0 143' + sleep 1 # wait for the transaction to finish + + _service_log_should_contain_string 'mail' 'imapsieve: Matched static mailbox rule [2]' + + _show_complete_mail_log + for LINE in "${LEARN_HAM_LINES[@]}"; do + assert_output --partial "${LINE}" + done +} + +@test 'greylisting is enabled' { + _run_in_container grep 'enabled = true;' /etc/rspamd/local.d/greylist.conf + assert_success + _run_in_container rspamadm configdump greylist + assert_success + assert_output --partial 'enabled = true;' +} + +@test 'hfilter group module is configured correctly' { + local MODULE_FILE='/etc/rspamd/local.d/hfilter_group.conf' + _file_exists_in_container "${MODULE_FILE}" + + _run_in_container grep '__TAG__HFILTER_HOSTNAME_UNKNOWN' "${MODULE_FILE}" + assert_success + assert_output --partial 'score = 7;' +} + +@test 'checks on authenticated users are disabled' { + local MODULE_FILE='/etc/rspamd/local.d/settings.conf' + _file_exists_in_container "${MODULE_FILE}" + + _run_in_container grep -E -A 6 'authenticated \{' "${MODULE_FILE}" + assert_success + assert_output --partial 'authenticated = yes;' + assert_output --partial 'groups_enabled = [dkim];' +} diff --git a/test/tests/parallel/set1/spam_virus/rspamd_partly.bats b/test/tests/parallel/set1/spam_virus/rspamd_partly.bats new file mode 100644 index 00000000000..e3dd64e58f4 --- /dev/null +++ b/test/tests/parallel/set1/spam_virus/rspamd_partly.bats @@ -0,0 +1,97 @@ +load "${REPOSITORY_ROOT}/test/helper/setup" +load "${REPOSITORY_ROOT}/test/helper/common" + +# This file tests Rspamd when some of its features are enabled, and +# some other interfering features are enabled. +BATS_TEST_NAME_PREFIX='[Rspamd] (partly) ' +CONTAINER_NAME='dms-test_rspamd-partly' + +function setup_file() { + _init_with_defaults + + # Comment for maintainers about `PERMIT_DOCKER=host`: + # https://github.com/docker-mailserver/docker-mailserver/pull/2815/files#r991087509 + local CUSTOM_SETUP_ARGUMENTS=( + --env ENABLE_AMAVIS=1 + --env ENABLE_SPAMASSASSIN=1 + --env ENABLE_CLAMAV=0 + --env ENABLE_RSPAMD=1 + --env ENABLE_OPENDKIM=1 + --env ENABLE_OPENDMARC=1 + --env ENABLE_POLICYD_SPF=1 + --env ENABLE_POSTGREY=0 + --env PERMIT_DOCKER=host + --env LOG_LEVEL=trace + --env MOVE_SPAM_TO_JUNK=0 + --env RSPAMD_LEARN=0 + --env RSPAMD_CHECK_AUTHENTICATED=1 + --env RSPAMD_GREYLISTING=0 + --env RSPAMD_HFILTER=0 + ) + + _common_container_setup 'CUSTOM_SETUP_ARGUMENTS' + + _wait_for_service rspamd-redis + _wait_for_service rspamd + _wait_for_rspamd_port_in_container + _wait_for_service amavis + _wait_for_service postfix + _wait_for_smtp_port_in_container +} + +function teardown_file() { _default_teardown ; } + +@test "log warns about interfering features" { + run docker logs "${CONTAINER_NAME}" + assert_success + for SERVICE in 'Amavis/SA' 'OpenDKIM' 'OpenDMARC' 'policyd-spf'; do + assert_output --regexp ".*WARN.*Running ${SERVICE} & Rspamd at the same time is discouraged" + done +} + +@test 'log shows all features as properly disabled' { + run docker logs "${CONTAINER_NAME}" + assert_success + assert_line --partial 'Rspamd will not use ClamAV (which has not been enabled)' + assert_line --partial 'Intelligent learning of spam and ham is disabled' + assert_line --partial 'Greylisting is disabled' + assert_line --partial 'Disabling Hfilter (group) module' + assert_line --partial 'Spam subject is not set' +} + +@test 'antivirus maximum size was not adjusted unnecessarily' { + _run_in_container grep 'max_size = 25000000' /etc/rspamd/local.d/antivirus.conf + assert_success +} + +@test 'learning is properly disabled' { + for FILE in learn-{ham,spam}.{sieve,svbin}; do + _file_does_not_exist_in_container "/usr/lib/dovecot/sieve-pipe/${FILE}" + done + + _run_in_container grep 'mail_plugins.*imap_sieve' /etc/dovecot/conf.d/20-imap.conf + assert_failure + local SIEVE_CONFIG_FILE='/etc/dovecot/conf.d/90-sieve.conf' + _run_in_container grep -F 'imapsieve_mailbox1_name = Junk' "${SIEVE_CONFIG_FILE}" + assert_failure + _run_in_container grep -F 'imapsieve_mailbox1_causes = COPY' "${SIEVE_CONFIG_FILE}" + assert_failure +} + +@test 'greylisting is properly disabled' { + _run_in_container grep -F 'enabled = false;' '/etc/rspamd/local.d/greylist.conf' + assert_success +} + +@test 'hfilter group module configuration is deleted' { + _file_does_not_exist_in_container /etc/rspamd/local.d/hfilter_group.conf + assert_failure +} + +@test 'checks on authenticated users are enabled' { + local MODULE_FILE='/etc/rspamd/local.d/settings.conf' + _file_exists_in_container "${MODULE_FILE}" + + _run_in_container grep -E 'authenticated \{' "${MODULE_FILE}" + assert_failure +} diff --git a/test/tests/parallel/set1/spam_virus/spam_junk_folder.bats b/test/tests/parallel/set1/spam_virus/spam_junk_folder.bats index 237218e8b05..5590d6f7deb 100644 --- a/test/tests/parallel/set1/spam_virus/spam_junk_folder.bats +++ b/test/tests/parallel/set1/spam_virus/spam_junk_folder.bats @@ -8,6 +8,7 @@ BATS_TEST_NAME_PREFIX='[Spam - Amavis] ENV SPAMASSASSIN_SPAM_TO_INBOX ' CONTAINER1_NAME='dms-test_spam-amavis_bounced' CONTAINER2_NAME='dms-test_spam-amavis_env-move-spam-to-junk-0' CONTAINER3_NAME='dms-test_spam-amavis_env-move-spam-to-junk-1' +CONTAINER4_NAME='dms-test_spam-amavis_env-mark-spam-as-read-1' function teardown() { _default_teardown ; } @@ -33,7 +34,7 @@ function teardown() { _default_teardown ; } local CUSTOM_SETUP_ARGUMENTS=( --env ENABLE_AMAVIS=1 --env ENABLE_SPAMASSASSIN=1 - --env SA_SPAM_SUBJECT="SPAM: " + --env SPAM_SUBJECT="SPAM: " --env SPAMASSASSIN_SPAM_TO_INBOX=1 --env MOVE_SPAM_TO_JUNK=0 --env PERMIT_DOCKER=container @@ -48,31 +49,58 @@ function teardown() { _default_teardown ; } _should_receive_spam_at '/var/mail/localhost.localdomain/user1/new/' } -@test "(enabled + MOVE_SPAM_TO_JUNK=1) should deliver spam message into Junk folder" { +@test "(enabled + MOVE_SPAM_TO_JUNK=1) should deliver spam message into Junk mailbox" { export CONTAINER_NAME=${CONTAINER3_NAME} + _init_with_defaults + local CUSTOM_SETUP_ARGUMENTS=( --env ENABLE_AMAVIS=1 --env ENABLE_SPAMASSASSIN=1 - --env SA_SPAM_SUBJECT="SPAM: " + --env SPAM_SUBJECT="SPAM: " --env SPAMASSASSIN_SPAM_TO_INBOX=1 --env MOVE_SPAM_TO_JUNK=1 --env PERMIT_DOCKER=container ) + + # Adjust 'Junk' mailbox name to verify delivery to Junk mailbox based on special-use flag instead of mailbox's name + mv "${TEST_TMP_CONFIG}/junk-mailbox/user-patches.sh" "${TEST_TMP_CONFIG}/" + + _common_container_setup 'CUSTOM_SETUP_ARGUMENTS' + + _should_send_spam_message + _should_be_received_by_amavis 'Passed SPAM {RelayedTaggedInbound,Quarantined}' + + # Should move delivered spam to the Junk mailbox (adjusted to be located at '.Spam/') + _should_receive_spam_at '/var/mail/localhost.localdomain/user1/.Spam/new/' +} + +# NOTE: Same as test for `CONTAINER3_NAME`, only differing by ENV `MARK_SPAM_AS_READ=1` + `_should_receive_spam_at` location +@test "(enabled + MARK_SPAM_AS_READ=1) should mark spam message as read" { + export CONTAINER_NAME=${CONTAINER4_NAME} + + local CUSTOM_SETUP_ARGUMENTS=( + --env ENABLE_AMAVIS=1 + --env ENABLE_SPAMASSASSIN=1 + --env SPAM_SUBJECT="SPAM: " + --env SPAMASSASSIN_SPAM_TO_INBOX=1 + --env MARK_SPAM_AS_READ=1 + --env PERMIT_DOCKER=container + ) _init_with_defaults _common_container_setup 'CUSTOM_SETUP_ARGUMENTS' _should_send_spam_message _should_be_received_by_amavis 'Passed SPAM {RelayedTaggedInbound,Quarantined}' - # Should move delivered spam to Junk folder - _should_receive_spam_at '/var/mail/localhost.localdomain/user1/.Junk/new/' + # Should move delivered spam to Junk folder as read (`cur` instead of `new`) + _should_receive_spam_at '/var/mail/localhost.localdomain/user1/.Junk/cur/' } function _should_send_spam_message() { _wait_for_smtp_port_in_container _wait_for_tcp_port_in_container 10024 # port 10024 is for Amavis - _send_email 'email-templates/amavis-spam' + _send_spam } function _should_be_received_by_amavis() { diff --git a/test/tests/parallel/set1/spam_virus/undef_spam_subject.bats b/test/tests/parallel/set1/spam_virus/undef_spam_subject.bats deleted file mode 100644 index 35260d51e84..00000000000 --- a/test/tests/parallel/set1/spam_virus/undef_spam_subject.bats +++ /dev/null @@ -1,61 +0,0 @@ -load "${REPOSITORY_ROOT}/test/helper/setup" -load "${REPOSITORY_ROOT}/test/helper/common" - -BATS_TEST_NAME_PREFIX='[Spam] (undefined subject) ' - -CONTAINER1_NAME='dms-test_spam-undef-subject_1' -CONTAINER2_NAME='dms-test_spam-undef-subject_2' -CONTAINER_NAME=${CONTAINER2_NAME} - -function teardown() { _default_teardown ; } - -@test "'SA_SPAM_SUBJECT=undef' should update Amavis config" { - export CONTAINER_NAME=${CONTAINER1_NAME} - local CUSTOM_SETUP_ARGUMENTS=( - --env ENABLE_AMAVIS=1 - --env ENABLE_SPAMASSASSIN=1 - --env SA_SPAM_SUBJECT='undef' - ) - _init_with_defaults - _common_container_setup 'CUSTOM_SETUP_ARGUMENTS' - - _run_in_container_bash "grep '\$sa_spam_subject_tag' /etc/amavis/conf.d/20-debian_defaults | grep '= undef'" - assert_success -} - -# TODO: Unclear why some of these ENV are relevant for the test? -@test "Docker env variables are set correctly (custom)" { - export CONTAINER_NAME=${CONTAINER2_NAME} - - local CUSTOM_SETUP_ARGUMENTS=( - --env ENABLE_CLAMAV=1 - --env SPOOF_PROTECTION=1 - --env ENABLE_SPAMASSASSIN=1 - --env REPORT_RECIPIENT=user1@localhost.localdomain - --env REPORT_SENDER=report1@mail.my-domain.com - --env SA_TAG=-5.0 - --env SA_TAG2=2.0 - --env SA_KILL=3.0 - --env SA_SPAM_SUBJECT="SPAM: " - --env VIRUSMAILS_DELETE_DELAY=7 - --env ENABLE_SRS=1 - --env ENABLE_MANAGESIEVE=1 - --env PERMIT_DOCKER=host - # NOTE: ulimit required for `ENABLE_SRS=1` until running a newer `postsrsd` - --ulimit "nofile=$(ulimit -Sn):$(ulimit -Hn)" - ) - _init_with_defaults - _common_container_setup 'CUSTOM_SETUP_ARGUMENTS' - - _run_in_container_bash "grep '\$sa_tag_level_deflt' /etc/amavis/conf.d/20-debian_defaults | grep '= -5.0'" - assert_success - - _run_in_container_bash "grep '\$sa_tag2_level_deflt' /etc/amavis/conf.d/20-debian_defaults | grep '= 2.0'" - assert_success - - _run_in_container_bash "grep '\$sa_kill_level_deflt' /etc/amavis/conf.d/20-debian_defaults | grep '= 3.0'" - assert_success - - _run_in_container_bash "grep '\$sa_spam_subject_tag' /etc/amavis/conf.d/20-debian_defaults | grep '= .SPAM: .'" - assert_success -} diff --git a/test/tests/parallel/set1/tls/dhparams.bats b/test/tests/parallel/set1/tls/dhparams.bats index 3157034c254..86586f28c49 100644 --- a/test/tests/parallel/set1/tls/dhparams.bats +++ b/test/tests/parallel/set1/tls/dhparams.bats @@ -38,7 +38,7 @@ function teardown() { _default_teardown ; } # - A warning is raised about usage of potentially insecure parameters. @test "Custom" { export CONTAINER_NAME=${CONTAINER2_NAME} - local DH_PARAMS_CUSTOM='test/test-files/ssl/custom-dhe-params.pem' + local DH_PARAMS_CUSTOM='test/files/ssl/custom-dhe-params.pem' local DH_CHECKSUM_CUSTOM=$(sha512sum "${DH_PARAMS_CUSTOM}" | awk '{print $1}') _init_with_defaults @@ -50,7 +50,7 @@ function teardown() { _default_teardown ; } # Should emit a warning: run docker logs "${CONTAINER_NAME}" assert_success - assert_output --partial '[ WARNING ] Using self-generated dhparams is considered insecure - unless you know what you are doing, please remove' + assert_output --partial 'Using self-generated dhparams is considered insecure - unless you know what you are doing, please remove' } # Ensures the docker image services (Postfix and Dovecot) have the expected DH files: diff --git a/test/tests/parallel/set1/tls/letsencrypt.bats b/test/tests/parallel/set1/tls/letsencrypt.bats index 22fc70895f8..63bad8a6bee 100644 --- a/test/tests/parallel/set1/tls/letsencrypt.bats +++ b/test/tests/parallel/set1/tls/letsencrypt.bats @@ -45,7 +45,7 @@ function _initial_setup() { # Test that certificate files exist for the configured `hostname`: _should_have_valid_config "${TARGET_DOMAIN}" 'privkey.pem' 'fullchain.pem' - _should_succesfully_negotiate_tls "${TARGET_DOMAIN}" + _should_successfully_negotiate_tls "${TARGET_DOMAIN}" _should_not_support_fqdn_in_cert 'example.test' } @@ -65,7 +65,7 @@ function _initial_setup() { #test domain has certificate files _should_have_valid_config "${TARGET_DOMAIN}" 'privkey.pem' 'fullchain.pem' - _should_succesfully_negotiate_tls "${TARGET_DOMAIN}" + _should_successfully_negotiate_tls "${TARGET_DOMAIN}" _should_not_support_fqdn_in_cert 'mail.example.test' } @@ -88,7 +88,7 @@ function _initial_setup() { # All of these certs support both FQDNs (`mail.example.test` and `example.test`), # Except for the wildcard cert (`*.example.test`), that was created with `example.test` intentionally excluded from SAN. # We want to maintain the same FQDN (`mail.example.test`) between the _acme_ecdsa and _acme_rsa tests. - local LOCAL_BASE_PATH="${PWD}/test/test-files/ssl/example.test/with_ca/rsa" + local LOCAL_BASE_PATH="${PWD}/test/files/ssl/example.test/with_ca/rsa" function _prepare() { # Default `acme.json` for _acme_ecdsa test: @@ -148,7 +148,7 @@ function _initial_setup() { # The difference in support is: # - `example.test` should no longer be valid. # - `mail.example.test` should remain valid, but also allow any other subdomain/hostname. - _should_succesfully_negotiate_tls 'mail.example.test' + _should_successfully_negotiate_tls 'mail.example.test' _should_support_fqdn_in_cert 'fake.example.test' _should_not_support_fqdn_in_cert 'example.test' } @@ -240,8 +240,7 @@ function _copy_to_letsencrypt_storage() { FQDN_DIR=$(echo "${DEST}" | cut -d '/' -f1) mkdir -p "${TEST_TMP_CONFIG}/letsencrypt/${FQDN_DIR}" - if ! cp "${PWD}/test/test-files/ssl/${SRC}" "${TEST_TMP_CONFIG}/letsencrypt/${DEST}" - then + if ! cp "${PWD}/test/files/ssl/${SRC}" "${TEST_TMP_CONFIG}/letsencrypt/${DEST}"; then echo "Could not copy cert file '${SRC}'' to '${DEST}'" >&2 exit 1 fi diff --git a/test/tests/parallel/set1/tls/manual.bats b/test/tests/parallel/set1/tls/manual.bats index 2a55f14f593..c082d6ed728 100644 --- a/test/tests/parallel/set1/tls/manual.bats +++ b/test/tests/parallel/set1/tls/manual.bats @@ -20,7 +20,7 @@ function setup_file() { export TEST_DOMAIN='example.test' local CUSTOM_SETUP_ARGUMENTS=( - --volume "${PWD}/test/test-files/ssl/${TEST_DOMAIN}/with_ca/ecdsa/:/config/ssl/:ro" + --volume "${PWD}/test/files/ssl/${TEST_DOMAIN}/with_ca/ecdsa/:/config/ssl/:ro" --env LOG_LEVEL='trace' --env SSL_TYPE='manual' --env TLS_LEVEL='modern' @@ -108,10 +108,10 @@ function teardown_file() { _default_teardown ; } @test "manual cert changes are picked up by check-for-changes" { printf '%s' 'someThingsChangedHere' \ - >>"$(pwd)/test/test-files/ssl/${TEST_DOMAIN}/with_ca/ecdsa/key.ecdsa.pem" + >>"$(pwd)/test/files/ssl/${TEST_DOMAIN}/with_ca/ecdsa/key.ecdsa.pem" run timeout 15 docker exec "${CONTAINER_NAME}" bash -c "tail -F /var/log/supervisor/changedetector.log | sed '/Manual certificates have changed/ q'" assert_success - sed -i '/someThingsChangedHere/d' "$(pwd)/test/test-files/ssl/${TEST_DOMAIN}/with_ca/ecdsa/key.ecdsa.pem" + sed -i '/someThingsChangedHere/d' "$(pwd)/test/files/ssl/${TEST_DOMAIN}/with_ca/ecdsa/key.ecdsa.pem" } diff --git a/test/tests/parallel/set2/tls_cipherlists.bats b/test/tests/parallel/set2/tls_cipherlists.bats index 854a65146c8..a347b90cdee 100644 --- a/test/tests/parallel/set2/tls_cipherlists.bats +++ b/test/tests/parallel/set2/tls_cipherlists.bats @@ -17,7 +17,7 @@ function setup_file() { # Contains various certs for testing TLS support (read-only): export TLS_CONFIG_VOLUME - TLS_CONFIG_VOLUME="${PWD}/test/test-files/ssl/${TEST_DOMAIN}/:/config/ssl/:ro" + TLS_CONFIG_VOLUME="${PWD}/test/files/ssl/${TEST_DOMAIN}/:/config/ssl/:ro" # Used for connecting testssl and DMS containers via network name `TEST_DOMAIN`: # NOTE: If the network already exists, the test will fail to start @@ -25,7 +25,7 @@ function setup_file() { # Pull `testssl.sh` image in advance to avoid it interfering with the `run` captured output. # Only interferes (potential test failure) with `assert_output` not `assert_success`? - docker pull drwetter/testssl.sh:3.1dev + docker pull ghcr.io/testssl/testssl.sh:3.2 # Only used in `_should_support_expected_cipherlists()` to set a storage location for `testssl.sh` JSON output: # `${BATS_TMPDIR}` maps to `/tmp`: https://bats-core.readthedocs.io/en/v1.8.2/writing-tests.html#special-variables @@ -76,8 +76,7 @@ function _configure_and_run_dms_container() { local ALT_KEY_TYPE=$3 # Optional parameter export TEST_VARIANT="${TLS_LEVEL}-${KEY_TYPE}" - if [[ -n ${ALT_KEY_TYPE} ]] - then + if [[ -n ${ALT_KEY_TYPE} ]]; then TEST_VARIANT+="-${ALT_KEY_TYPE}" fi @@ -98,8 +97,7 @@ function _configure_and_run_dms_container() { --env SSL_KEY_PATH="/config/ssl/with_ca/ecdsa/key.${KEY_TYPE}.pem" ) - if [[ -n ${ALT_KEY_TYPE} ]] - then + if [[ -n ${ALT_KEY_TYPE} ]]; then CUSTOM_SETUP_ARGUMENTS+=( --env SSL_ALT_CERT_PATH="/config/ssl/with_ca/ecdsa/cert.${ALT_KEY_TYPE}.pem" --env SSL_ALT_KEY_PATH="/config/ssl/with_ca/ecdsa/key.${ALT_KEY_TYPE}.pem" @@ -113,7 +111,7 @@ function _configure_and_run_dms_container() { function _should_support_expected_cipherlists() { # Make a directory with test user ownership. Avoids Docker creating this with root ownership. - # TODO: Can switch to filename prefix for JSON output when this is resolved: https://github.com/drwetter/testssl.sh/issues/1845 + # TODO: Can switch to filename prefix for JSON output when this is resolved: https://github.com/testssl/testssl.sh/issues/1845 local RESULTS_PATH="${TLS_RESULTS_DIR}/${TEST_VARIANT}" mkdir -p "${RESULTS_PATH}" @@ -158,7 +156,7 @@ function _collect_cipherlists() { # NOTE: Batch testing ports via `--file` doesn't properly bubble up failure. # If the failure for a test is misleading consider testing a single port with: # local TESTSSL_CMD=(--quiet --jsonfile-pretty "/output/port_${PORT}.json" --starttls smtp "${TEST_DOMAIN}:${PORT}") - # TODO: Can use `jq` to check for failure when this is resolved: https://github.com/drwetter/testssl.sh/issues/1844 + # TODO: Can use `jq` to check for failure when this is resolved: https://github.com/testssl/testssl.sh/issues/1844 # `--user ":"` is a workaround: Avoids `permission denied` write errors for json output, uses `id` to match user uid & gid. run docker run --rm \ @@ -168,7 +166,7 @@ function _collect_cipherlists() { --volume "${TLS_CONFIG_VOLUME}" \ --volume "${RESULTS_PATH}:/output" \ --workdir "/output" \ - drwetter/testssl.sh:3.1dev "${TESTSSL_CMD[@]}" + ghcr.io/testssl/testssl.sh:3.2 "${TESTSSL_CMD[@]}" assert_success } @@ -199,9 +197,8 @@ function compare_cipherlist() { function get_cipherlist() { local TLS_VERSION=$1 - if [[ ${TLS_VERSION} == "TLSv1_3" ]] - then - # TLS v1.3 cipher suites are not user defineable and not unique to the available certificate(s). + if [[ ${TLS_VERSION} == "TLSv1_3" ]]; then + # TLS v1.3 cipher suites are not user definable and not unique to the available certificate(s). # They do not support server enforced order either. echo '"TLS_AES_256_GCM_SHA384 TLS_CHACHA20_POLY1305_SHA256 TLS_AES_128_GCM_SHA256"' else diff --git a/test/tests/parallel/set3/container_configuration/env_vars_from_files.bats b/test/tests/parallel/set3/container_configuration/env_vars_from_files.bats new file mode 100644 index 00000000000..340450e3db2 --- /dev/null +++ b/test/tests/parallel/set3/container_configuration/env_vars_from_files.bats @@ -0,0 +1,89 @@ +load "${REPOSITORY_ROOT}/test/helper/setup" +load "${REPOSITORY_ROOT}/test/helper/common" + +# Feature (ENV value sourced from file): +# - An ENV with a `__FILE` suffix will read a value from a referenced file path to set the actual ENV (assuming it is empty) +# - Feature implemented at: `variables-stack.sh:__environment_variables_from_files()` +# - Feature PR: https://github.com/docker-mailserver/docker-mailserver/pull/4359 + +BATS_TEST_NAME_PREFIX='[Configuration] (ENV __FILE support) ' +CONTAINER1_NAME='dms-test_env-files_success' +CONTAINER2_NAME='dms-test_env-files_warning' +CONTAINER3_NAME='dms-test_env-files_error' + +function setup_file() { + export CONTAINER_NAME + export FILEPATH_VALID='/tmp/file-with-value' + export FILEPATH_INVALID='/path/to/non-existent-file' + # Each `_init_with_defaults` call updates the `TEST_TMP_CONFIG` location to create a container specific file: + local FILE_WITH_VALUE + + # ENV is set via file content (valid file path): + CONTAINER_NAME=${CONTAINER1_NAME} + _init_with_defaults + FILE_WITH_VALUE=${TEST_TMP_CONFIG}/test_secret + echo 1 > "${FILE_WITH_VALUE}" + local CUSTOM_SETUP_ARGUMENTS=( + --env ENABLE_POP3__FILE="${FILEPATH_VALID}" + -v "${FILE_WITH_VALUE}:${FILEPATH_VALID}" + ) + _common_container_setup 'CUSTOM_SETUP_ARGUMENTS' + + # ENV is already set explicitly, a warning should be logged: + CONTAINER_NAME=${CONTAINER2_NAME} + _init_with_defaults + FILE_WITH_VALUE=${TEST_TMP_CONFIG}/test_secret + echo 1 > "${FILE_WITH_VALUE}" + local CUSTOM_SETUP_ARGUMENTS=( + --env ENABLE_POP3="0" + --env ENABLE_POP3__FILE="${FILEPATH_VALID}" + -v "${FILE_WITH_VALUE}:${FILEPATH_VALID}" + ) + _common_container_setup 'CUSTOM_SETUP_ARGUMENTS' + + # ENV is not set by file content (invalid file path): + CONTAINER_NAME=${CONTAINER3_NAME} + _init_with_defaults + local CUSTOM_SETUP_ARGUMENTS=( + --env ENABLE_POP3__FILE="${FILEPATH_INVALID}" + ) + _common_container_setup 'CUSTOM_SETUP_ARGUMENTS' +} + +function teardown_file() { + docker rm -f "${CONTAINER1_NAME}" "${CONTAINER2_NAME}" "${CONTAINER3_NAME}" +} + +@test "ENV can be set from a file" { + export CONTAINER_NAME=${CONTAINER1_NAME} + + # /var/log/mail/mail.log is not equivalent to stdout content, + # Relevant log content only available via docker logs: + run docker logs "${CONTAINER_NAME}" + assert_success + assert_line --partial "Getting secret 'ENABLE_POP3' from '${FILEPATH_VALID}'" + + # Verify ENABLE_POP3 was enabled (disabled by default), by checking this file path is valid: + _run_in_container [ -f /etc/dovecot/protocols.d/pop3d.protocol ] + assert_success +} + +@test "Non-empty ENV have precedence over their __FILE variant" { + export CONTAINER_NAME=${CONTAINER2_NAME} + + # /var/log/mail/mail.log is not equivalent to stdout content, + # Relevant log content only available via docker logs: + run docker logs "${CONTAINER_NAME}" + assert_success + assert_line --partial "ENV value will not be sourced from 'ENABLE_POP3__FILE' since 'ENABLE_POP3' is already set" +} + +@test "Referencing a non-existent file logs an error" { + export CONTAINER_NAME=${CONTAINER3_NAME} + + # /var/log/mail/mail.log is not equivalent to stdout content, + # Relevant log content only available via docker logs: + run docker logs "${CONTAINER_NAME}" + assert_success + assert_line --partial "File defined for secret 'ENABLE_POP3' with path '${FILEPATH_INVALID}' does not exist" +} diff --git a/test/tests/parallel/set3/container_configuration/hostname.bats b/test/tests/parallel/set3/container_configuration/hostname.bats index 98a988513f6..65d7ce136af 100644 --- a/test/tests/parallel/set3/container_configuration/hostname.bats +++ b/test/tests/parallel/set3/container_configuration/hostname.bats @@ -193,6 +193,7 @@ function _should_be_configured_to_fqdn() { assert_success # Amavis + # shellcheck disable=SC2016 _run_in_container grep '^\$myhostname' /etc/amavis/conf.d/05-node_id assert_output "\$myhostname = \"${EXPECTED_FQDN}\";" assert_success @@ -206,7 +207,7 @@ function _should_have_correct_mail_headers() { # (eg: OVERRIDE_HOSTNAME or `--hostname mail --domainname example.test`) local EXPECTED_HOSTNAME=${3:-${EXPECTED_FQDN}} - _send_email 'email-templates/existing-user1' + _send_email --from 'user@external.tld' _wait_for_empty_mail_queue_in_container _count_files_in_directory_in_container '/var/mail/localhost.localdomain/user1/new/' '1' @@ -233,8 +234,9 @@ function _should_have_correct_mail_headers() { # but Amavis is changing that. It also changes protocol from SMTP to ESMTP. assert_line --index 7 --partial 'Received: from localhost (localhost [127.0.0.1])' assert_line --index 8 --partial "by ${EXPECTED_FQDN} (Postfix) with ESMTP id" - assert_line --index 14 --partial 'Message-Id:' - assert_line --index 14 --partial "@${EXPECTED_FQDN}>" + assert_line --index 14 'X-MS-Reactions: disallow' + assert_line --index 15 --partial 'Message-Id:' + assert_line --index 15 --partial "@${EXPECTED_FQDN}>" # Mail contents example: # diff --git a/test/tests/parallel/set3/container_configuration/process_check_restart.bats b/test/tests/parallel/set3/container_configuration/process_check_restart.bats index 6a743bf87b4..c55f59fcec9 100644 --- a/test/tests/parallel/set3/container_configuration/process_check_restart.bats +++ b/test/tests/parallel/set3/container_configuration/process_check_restart.bats @@ -11,18 +11,17 @@ function teardown() { _default_teardown ; } # Process matching notes: # opendkim (/usr/sbin/opendkim) - x2 of the same process are found running (1 is the parent) # opendmarc (/usr/sbin/opendmarc) -# master (/usr/lib/postfix/sbin/master) - Postfix main process (Can take a few seconds running to be ready) -# NOTE: pgrep or pkill used with `--full` would also match `/usr/sbin/amavisd-new (master)` -# -# amavi (/usr/sbin/amavi) - Matches three processes, the main process is `/usr/sbin/amavisd-new (master)` -# NOTE: `amavisd-new` can only be matched with `--full`, regardless pkill would return `/usr/sbin/amavi` +# postfix (/usr/lib/postfix/sbin/master) - Postfix main process (two ancestors, launched via pidproxy python3 script) # +# amavisd (usr/sbin/amavisd) # clamd (/usr/sbin/clamd) # dovecot (/usr/sbin/dovecot) # fetchmail (/usr/bin/fetchmail) -# fail2ban-server (/usr/bin/python3 /usr/bin/fail2ban-server) - Started by fail2ban-wrapper.sh -# postgrey (postgrey) - NOTE: This process lacks path information to match with `--full` in pgrep / pkill -# postsrsd (/usr/sbin/postsrsd) - NOTE: Also matches the wrapper: `/bin/bash /usr/local/bin/postsrsd-wrapper.sh` +# fail2ban-server (/usr/bin/python3 /usr/bin/fail2ban-server) - NOTE: python3 is due to the shebang +# getmail (/bin/bash /usr/local/bin/getmail-service.sh) +# mta-sts-daemon (/usr/bin/bin/python3 /usr/bin/mta-sts-daemon) +# postgrey (postgrey) - NOTE: This process command uses perl via shebang, but unlike python3 the context is missing +# postsrsd (/usr/sbin/postsrsd) # saslauthd (/usr/sbin/saslauthd) - x5 of the same process are found running (1 is a parent of 4) # Delays: @@ -30,20 +29,21 @@ function teardown() { _default_teardown ; } # dovecot + fail2ban, take approx 1 sec to kill properly # opendkim + opendmarc can take up to 6 sec to kill properly # clamd + postsrsd sometimes take 1-3 sec to restart after old process is killed. -# postfix + fail2ban (due to Wrapper scripts) can delay a restart by up to 5 seconds from usage of sleep. # These processes should always be running: CORE_PROCESS_LIST=( - master + postfix ) # These processes can be toggled via ENV: # NOTE: clamd handled in separate test case ENV_PROCESS_LIST=( - amavi + amavisd dovecot fail2ban-server fetchmail + getmail + mta-sts-daemon opendkim opendmarc postgrey @@ -58,6 +58,8 @@ ENV_PROCESS_LIST=( --env ENABLE_CLAMAV=0 --env ENABLE_FAIL2BAN=0 --env ENABLE_FETCHMAIL=0 + --env ENABLE_GETMAIL=0 + --env ENABLE_MTA_STS=0 --env ENABLE_OPENDKIM=0 --env ENABLE_OPENDMARC=0 --env ENABLE_POSTGREY=0 @@ -72,29 +74,29 @@ ENV_PROCESS_LIST=( # Required for Postfix (when launched by wrapper script which is slow to start) _wait_for_smtp_port_in_container - for PROCESS in "${CORE_PROCESS_LIST[@]}" - do + for PROCESS in "${CORE_PROCESS_LIST[@]}"; do run _check_if_process_is_running "${PROCESS}" assert_success assert_output --partial "${PROCESS}" refute_output --partial "is not running" done - for PROCESS in "${ENV_PROCESS_LIST[@]}" clamd - do + for PROCESS in "${ENV_PROCESS_LIST[@]}" clamd; do run _check_if_process_is_running "${PROCESS}" assert_failure assert_output --partial "'${PROCESS}' is not running" done } -# Average time: 23 seconds (29 with wrapper scripts) +# Average time: 23 seconds @test "(enabled ENV) should restart processes when killed" { export CONTAINER_NAME=${CONTAINER2_NAME} local CONTAINER_ARGS_ENV_CUSTOM=( --env ENABLE_AMAVIS=1 --env ENABLE_FAIL2BAN=1 --env ENABLE_FETCHMAIL=1 + --env ENABLE_GETMAIL=1 + --env ENABLE_MTA_STS=1 --env ENABLE_OPENDKIM=1 --env ENABLE_OPENDMARC=1 --env FETCHMAIL_PARALLEL=1 @@ -116,16 +118,15 @@ ENV_PROCESS_LIST=( "${ENV_PROCESS_LIST[@]}" ) - for PROCESS in "${ENABLED_PROCESS_LIST[@]}" - do + for PROCESS in "${ENABLED_PROCESS_LIST[@]}"; do _should_restart_when_killed "${PROCESS}" done # By this point the fetchmail processes have been verified to exist and restart, # For FETCHMAIL_PARALLEL=1 coverage, match full commandline for COUNTER values: - pgrep --full 'fetchmail-1.rc' + _run_in_container pgrep --full 'fetchmail-1.rc' assert_success - pgrep --full 'fetchmail-2.rc' + _run_in_container pgrep --full 'fetchmail-2.rc' assert_success _should_stop_cleanly @@ -152,14 +153,18 @@ function _should_restart_when_killed() { # Wait until process has been running for at least MIN_PROCESS_AGE: # (this allows us to more confidently check the process was restarted) _run_until_success_or_timeout 30 _check_if_process_is_running "${PROCESS}" "${MIN_PROCESS_AGE}" - # NOTE: refute_output doesn't have output to compare to when a run failure is due to a timeout + # NOTE: refute_output will not have any output to compare against if a `run` failure is caused by a timeout assert_success assert_output --partial "${PROCESS}" # Should kill the process successfully: # (which should then get restarted by supervisord) - _run_in_container pkill --echo "${PROCESS}" - assert_output --partial "${PROCESS}" + # NOTE: The process name from `pkill --echo` does not always match the equivalent process name from `pgrep --list-full`. + # The oldest process returned (if multiple) should be the top-level process launched by supervisord, + # the PID will verify the target process was killed correctly: + local PID=$(_exec_in_container pgrep --full --oldest "${PROCESS}") + _run_in_container pkill --echo --full "${PROCESS}" + assert_output --partial "killed (pid ${PID})" assert_success # Wait until original process is not running: @@ -175,17 +180,19 @@ function _should_restart_when_killed() { assert_output --partial "${PROCESS}" } -# NOTE: CONTAINER_NAME is implicit; it should have be set prior to calling. +# NOTE: CONTAINER_NAME is implicit; it should have been set prior to calling. function _check_if_process_is_running() { local PROCESS=${1} local MIN_SECS_RUNNING - [[ -n ${2} ]] && MIN_SECS_RUNNING="--older ${2}" + [[ -n ${2:-} ]] && MIN_SECS_RUNNING=('--older' "${2}") - local IS_RUNNING=$(docker exec "${CONTAINER_NAME}" pgrep --list-full ${MIN_SECS_RUNNING} "${PROCESS}") + # `--list-full` provides information for matching against (full process path) + # `--full` allows matching the process against the full path (required if a process is not the exec command, such as started by python3 command without a shebang) + # `--oldest` should select the parent process when there are multiple results, typically the command defined in `dms-services.conf` + local IS_RUNNING=$(_exec_in_container pgrep --full --list-full "${MIN_SECS_RUNNING[@]}" --oldest "${PROCESS}") # When no matches are found, nothing is returned. Provide something we can assert on (helpful for debugging): - if [[ ! ${IS_RUNNING} =~ "${PROCESS}" ]] - then + if [[ ! ${IS_RUNNING} =~ ${PROCESS} ]]; then echo "'${PROCESS}' is not running" return 1 fi @@ -196,8 +203,8 @@ function _check_if_process_is_running() { # The process manager (supervisord) should perform a graceful shutdown: # NOTE: Time limit should never be below these configured values: -# - supervisor-app.conf:stopwaitsecs -# - docker-compose.yml:stop_grace_period +# - dms-services.conf:stopwaitsecs +# - compose.yaml:stop_grace_period function _should_stop_cleanly() { run docker stop -t 60 "${CONTAINER_NAME}" assert_success diff --git a/test/tests/parallel/set3/mta/account_management.bats b/test/tests/parallel/set3/mta/account_management.bats index bc97d710544..ae1ea9df37d 100644 --- a/test/tests/parallel/set3/mta/account_management.bats +++ b/test/tests/parallel/set3/mta/account_management.bats @@ -27,14 +27,31 @@ function teardown_file() { _default_teardown ; } assert_line --index 3 'added@localhost.localdomain' assert_line --index 4 'pass@localhost.localdomain' assert_line --index 5 'alias1@localhost.localdomain' - # TODO: Probably not intentional?: - assert_line --index 6 '@localdomain2.com' - _should_output_number_of_lines 7 + # Dovecot "dummy accounts" for quota support, see `test/config/postfix-virtual.cf` for more context + assert_line --index 6 'prefixtest@localhost.localdomain' + assert_line --index 7 'test@localhost.localdomain' + assert_line --index 8 'first-name@localhost.localdomain' + assert_line --index 9 'first.name@localhost.localdomain' + _should_output_number_of_lines 10 + + refute_line --partial '@localdomain2.com' # Relevant log output from scripts/helpers/accounts.sh:_create_dovecot_alias_dummy_accounts(): # [ DEBUG ] Adding alias 'alias1@localhost.localdomain' for user 'user1@localhost.localdomain' to Dovecot's userdb # [ DEBUG ] Alias 'alias2@localhost.localdomain' is non-local (or mapped to a non-existing account) and will not be added to Dovecot's userdb - # [ DEBUG ] Adding alias '@localdomain2.com' for user 'user1@localhost.localdomain' to Dovecot's userdb +} + +# Dovecot "dummy accounts" for quota support, see `test/config/postfix-virtual.cf` for more context +@test "should create all dovecot dummy accounts" { + run docker logs "${CONTAINER_NAME}" + assert_success + assert_line --partial "Adding alias 'prefixtest@localhost.localdomain' for user 'user2@otherdomain.tld' to Dovecot's userdb" + assert_line --partial "Adding alias 'test@localhost.localdomain' for user 'user2@otherdomain.tld' to Dovecot's userdb" + refute_line --partial "Alias 'test@localhost.localdomain' will not be added to '/etc/dovecot/userdb' twice" + + assert_line --partial "Adding alias 'first-name@localhost.localdomain' for user 'user2@otherdomain.tld' to Dovecot's userdb" + assert_line --partial "Adding alias 'first.name@localhost.localdomain' for user 'user2@otherdomain.tld' to Dovecot's userdb" + refute_line --partial "Alias 'first.name@localhost.localdomain' will not be added to '/etc/dovecot/userdb' twice" } @test "should have created maildir for 'user1@localhost.localdomain'" { @@ -74,6 +91,19 @@ function teardown_file() { _default_teardown ; } __should_add_new_user 'user3@domain.tld' } +@test "should add new user 'USeRx@domain.tld' as 'userx@domain.tld' into 'postfix-accounts.cf' and log a warning" { + local MAIL_ACCOUNT='USeRx@domain.tld' + local NORMALIZED_MAIL_ACCOUNT='userx@domain.tld' + + _run_in_container setup email add "${MAIL_ACCOUNT}" mypassword + assert_success + assert_output --partial "'USeRx@domain.tld' has uppercase letters and will be normalized to 'userx@domain.tld'" + + __check_mail_account_exists "${NORMALIZED_MAIL_ACCOUNT}" + assert_success + assert_output "${NORMALIZED_MAIL_ACCOUNT}" +} + # To catch mistakes from substring matching: @test "should add new user 'auser3@domain.tld' into 'postfix-accounts.cf'" { __should_add_new_user 'auser3@domain.tld' diff --git a/test/tests/parallel/set3/mta/dsn.bats b/test/tests/parallel/set3/mta/dsn.bats new file mode 100644 index 00000000000..7a6af76c1c9 --- /dev/null +++ b/test/tests/parallel/set3/mta/dsn.bats @@ -0,0 +1,95 @@ +load "${REPOSITORY_ROOT}/test/helper/setup" +load "${REPOSITORY_ROOT}/test/helper/common" + +BATS_TEST_NAME_PREFIX='[DSN] ' +CONTAINER1_NAME='dms-test_dsn_send_always' +CONTAINER2_NAME='dms-test_dsn_send_auth' +CONTAINER3_NAME='dms-test_dsn_send_none' +# A similar line is added to the log when a DSN (Delivery Status Notification) is sent: +# +# postfix/bounce[1023]: C943BA6B46: sender delivery status notification: DBF86A6B4CO +# +LOG_DSN='delivery status notification' + +function setup_file() { + local CUSTOM_SETUP_ARGUMENTS=( + # Required only for delivery via nc (_send_email) + --env PERMIT_DOCKER=container + ) + + export CONTAINER_NAME=${CONTAINER1_NAME} + _init_with_defaults + # Unset `smtpd_discard_ehlo_keywords` to allow DSNs by default on any `smtpd` service: + cp "${TEST_TMP_CONFIG}/dsn/postfix-main.cf" "${TEST_TMP_CONFIG}/postfix-main.cf" + _common_container_setup 'CUSTOM_SETUP_ARGUMENTS' + _wait_for_service postfix + _wait_for_smtp_port_in_container + + export CONTAINER_NAME=${CONTAINER2_NAME} + _init_with_defaults + _common_container_setup 'CUSTOM_SETUP_ARGUMENTS' + _wait_for_service postfix + _wait_for_smtp_port_in_container + + export CONTAINER_NAME=${CONTAINER3_NAME} + _init_with_defaults + # Mirror default main.cf (disable DSN on ports 465 + 587 too): + cp "${TEST_TMP_CONFIG}/dsn/postfix-master.cf" "${TEST_TMP_CONFIG}/postfix-master.cf" + _common_container_setup 'CUSTOM_SETUP_ARGUMENTS' + _wait_for_service postfix + _wait_for_smtp_port_in_container +} + +function teardown_file() { + docker rm -f "${CONTAINER1_NAME}" "${CONTAINER2_NAME}" "${CONTAINER3_NAME}" +} + +@test "should always send a DSN when requested" { + export CONTAINER_NAME=${CONTAINER1_NAME} + + # TODO replace with _send_email as soon as it supports DSN + # TODO ref: https://github.com/jetmore/swaks/issues/41 + _nc_wrapper 'emails/nc_raw/dsn/unauthenticated.txt' + _nc_wrapper 'emails/nc_raw/dsn/authenticated.txt' '0.0.0.0 465' + _nc_wrapper 'emails/nc_raw/dsn/authenticated.txt' '0.0.0.0 587' + _wait_for_empty_mail_queue_in_container + + _filter_service_log 'mail' "${LOG_DSN}" + _should_output_number_of_lines 3 +} + +# Defaults test case +@test "should only send a DSN when requested from ports 465/587" { + export CONTAINER_NAME=${CONTAINER2_NAME} + + _nc_wrapper 'emails/nc_raw/dsn/unauthenticated.txt' + _wait_for_empty_mail_queue_in_container + + # DSN requests can now only be made on ports 465 and 587, + # so grep should not find anything. + # + # Although external requests are discarded, anyone who has requested a DSN + # will still receive it, but it will come from the sending mail server, not this one. + _service_log_should_not_contain_string 'mail' "${LOG_DSN}" + + # These ports are excluded via master.cf. + _nc_wrapper 'emails/nc_raw/dsn/authenticated.txt' '0.0.0.0 465' + _nc_wrapper 'emails/nc_raw/dsn/authenticated.txt' '0.0.0.0 587' + _wait_for_empty_mail_queue_in_container + + _service_log_should_contain_string 'mail' "${LOG_DSN}" + _should_output_number_of_lines 2 +} + +@test "should never send a DSN" { + export CONTAINER_NAME=${CONTAINER3_NAME} + + _nc_wrapper 'emails/nc_raw/dsn/unauthenticated.txt' + _nc_wrapper 'emails/nc_raw/dsn/authenticated.txt' '0.0.0.0 465' + _nc_wrapper 'emails/nc_raw/dsn/authenticated.txt' '0.0.0.0 587' + _wait_for_empty_mail_queue_in_container + + # DSN requests are rejected regardless of origin. + # This is usually a bad idea, as you won't get them either. + _service_log_should_not_contain_string 'mail' "${LOG_DSN}" +} diff --git a/test/tests/parallel/set3/mta/lmtp_ip.bats b/test/tests/parallel/set3/mta/lmtp_ip.bats index 861f7f60969..201cb2379d5 100644 --- a/test/tests/parallel/set3/mta/lmtp_ip.bats +++ b/test/tests/parallel/set3/mta/lmtp_ip.bats @@ -38,11 +38,13 @@ function teardown_file() { _default_teardown ; } @test "delivers mail to existing account" { _wait_for_smtp_port_in_container - _send_email 'email-templates/existing-user1' # send a test email + _send_email # Verify delivery was successful, log line should look similar to: # postfix/lmtp[1274]: 0EA424ABE7D9: to=, relay=127.0.0.1[127.0.0.1]:24, delay=0.13, delays=0.07/0.01/0.01/0.05, dsn=2.0.0, status=sent (250 2.0.0 ixPpB+Zvv2P7BAAAUi6ngw Saved) - local MATCH_LOG_LINE='postfix/lmtp.* status=sent .* Saved)' - run timeout 60 docker exec "${CONTAINER_NAME}" bash -c "tail -F /var/log/mail/mail.log | grep --max-count 1 '${MATCH_LOG_LINE}'" + local MATCH_LOG_LINE='postfix/lmtp.* status' + _run_in_container_bash "timeout 60 tail -F /var/log/mail/mail.log | grep --max-count 1 '${MATCH_LOG_LINE}'" assert_success + # Assertion of full pattern here (instead of via grep) is a bit more helpful for debugging partial failures: + assert_output --regexp "${MATCH_LOG_LINE}=sent .* Saved)" } diff --git a/test/tests/parallel/set3/mta/privacy.bats b/test/tests/parallel/set3/mta/privacy.bats index f8160827327..614e2e87db0 100644 --- a/test/tests/parallel/set3/mta/privacy.bats +++ b/test/tests/parallel/set3/mta/privacy.bats @@ -25,8 +25,11 @@ function teardown_file() { _default_teardown ; } # this test covers https://github.com/docker-mailserver/docker-mailserver/issues/681 @test "(Postfix) remove privacy details of the sender" { - _run_in_container_bash "openssl s_client -quiet -starttls smtp -connect 0.0.0.0:587 < /tmp/docker-mailserver-test/email-templates/send-privacy-email.txt" - assert_success + _send_email \ + --port 587 -tls --auth PLAIN \ + --auth-user user1@localhost.localdomain \ + --auth-password mypassword \ + --data 'privacy.txt' _run_until_success_or_timeout 120 _exec_in_container_bash '[[ -d /var/mail/localhost.localdomain/user1/new ]]' assert_success diff --git a/test/tests/parallel/set3/mta/smtp_delivery.bats b/test/tests/parallel/set3/mta/smtp_delivery.bats index af98b2f4f1b..d183694b06e 100644 --- a/test/tests/parallel/set3/mta/smtp_delivery.bats +++ b/test/tests/parallel/set3/mta/smtp_delivery.bats @@ -63,34 +63,51 @@ function setup_file() { # TODO: Move to clamav tests (For use when ClamAV is enabled): # _repeat_in_container_until_success_or_timeout 60 "${CONTAINER_NAME}" test -e /var/run/clamav/clamd.ctl - # _send_email 'email-templates/amavis-virus' + # _send_email --from 'virus@external.tld' --data 'amavis/virus.txt' # Required for 'delivers mail to existing alias': - _send_email 'email-templates/existing-alias-external' + _send_email --to alias1@localhost.localdomain --header "Subject: Test Message existing-alias-external" # Required for 'delivers mail to existing alias with recipient delimiter': - _send_email 'email-templates/existing-alias-recipient-delimiter' + _send_email --to alias1~test@localhost.localdomain --header 'Subject: Test Message existing-alias-recipient-delimiter' # Required for 'delivers mail to existing catchall': - _send_email 'email-templates/existing-catchall-local' + _send_email --to wildcard@localdomain2.com --header 'Subject: Test Message existing-catchall-local' # Required for 'delivers mail to regexp alias': - _send_email 'email-templates/existing-regexp-alias-local' + _send_email --to test123@localhost.localdomain --header 'Subject: Test Message existing-regexp-alias-local' # Required for 'rejects mail to unknown user': - _send_email 'email-templates/non-existing-user' + _send_email --expect-rejection --to nouser@localhost.localdomain + assert_failure # Required for 'redirects mail to external aliases': - _send_email 'email-templates/existing-regexp-alias-external' - _send_email 'email-templates/existing-alias-local' + _send_email --to bounce-always@localhost.localdomain + _send_email --to alias2@localhost.localdomain # Required for 'rejects spam': - _send_email 'email-templates/amavis-spam' + _send_spam # Required for 'delivers mail to existing account': - _send_email 'email-templates/existing-user1' - _send_email 'email-templates/existing-user2' - _send_email 'email-templates/existing-user3' - _send_email 'email-templates/existing-added' - _send_email 'email-templates/existing-user-and-cc-local-alias' - _send_email 'email-templates/sieve-spam-folder' - _send_email 'email-templates/sieve-pipe' - _run_in_container_bash 'sendmail root < /tmp/docker-mailserver-test/email-templates/root-email.txt' + _send_email --header 'Subject: Test Message existing-user1' + _send_email --to user2@otherdomain.tld + _send_email --to user3@localhost.localdomain + _send_email --to added@localhost.localdomain --header 'Subject: Test Message existing-added' + _send_email \ + --to user1@localhost.localdomain \ + --header 'Subject: Test Message existing-user-and-cc-local-alias' \ + --cc 'alias2@localhost.localdomain' + _send_email --data 'sieve/spam-folder.txt' + _send_email --to user2@otherdomain.tld --data 'sieve/pipe.txt' + _run_in_container_bash 'sendmail root < /tmp/docker-mailserver-test/emails/sendmail/root-email.txt' + assert_success +} + +function _unsuccessful() { + _send_email --expect-rejection --port 465 --auth "${1}" --auth-user "${2}" --auth-password wrongpassword --quit-after AUTH + assert_failure + assert_output --partial 'authentication failed' + assert_output --partial 'No authentication type succeeded' +} + +function _successful() { + _send_email --port 465 --auth "${1}" --auth-user "${2}" --auth-password mypassword --quit-after AUTH + assert_output --partial 'Authentication successful' } @test "should succeed at emptying mail queue" { @@ -103,44 +120,35 @@ function setup_file() { } @test "should successfully authenticate with good password (plain)" { - _send_email 'auth/smtp-auth-plain' '-w 5 0.0.0.0 465' - assert_output --partial 'Authentication successful' + _successful PLAIN user1@localhost.localdomain } @test "should fail to authenticate with wrong password (plain)" { - _send_email 'auth/smtp-auth-plain-wrong' '-w 20 0.0.0.0 465' - assert_output --partial 'authentication failed' + _unsuccessful PLAIN user1@localhost.localdomain } @test "should successfully authenticate with good password (login)" { - _send_email 'auth/smtp-auth-login' '-w 5 0.0.0.0 465' - assert_output --partial 'Authentication successful' + _successful LOGIN user1@localhost.localdomain } @test "should fail to authenticate with wrong password (login)" { - _send_email 'auth/smtp-auth-login-wrong' '-w 20 0.0.0.0 465' - assert_output --partial 'authentication failed' + _unsuccessful LOGIN user1@localhost.localdomain } @test "[user: 'added'] should successfully authenticate with good password (plain)" { - _send_email 'auth/added-smtp-auth-plain' '-w 5 0.0.0.0 465' - assert_output --partial 'Authentication successful' + _successful PLAIN added@localhost.localdomain } @test "[user: 'added'] should fail to authenticate with wrong password (plain)" { - _send_email 'auth/added-smtp-auth-plain-wrong' '-w 20 0.0.0.0 465' - assert_output --partial 'authentication failed' + _unsuccessful PLAIN added@localhost.localdomain } @test "[user: 'added'] should successfully authenticate with good password (login)" { - _send_email 'auth/added-smtp-auth-login' '-w 5 0.0.0.0 465' - assert_success - assert_output --partial 'Authentication successful' + _successful LOGIN added@localhost.localdomain } @test "[user: 'added'] should fail to authenticate with wrong password (login)" { - _send_email 'auth/added-smtp-auth-login-wrong' '-w 20 0.0.0.0 465' - assert_output --partial 'authentication failed' + _unsuccessful LOGIN added@localhost.localdomain } # TODO: Add a test covering case SPAMASSASSIN_SPAM_TO_INBOX=1 (default) @@ -166,33 +174,28 @@ function setup_file() { } @test "delivers mail to existing alias" { - _run_in_container grep 'to=, orig_to=' /var/log/mail/mail.log - assert_success + _service_log_should_contain_string 'mail' 'to=, orig_to=' assert_output --partial 'status=sent' _should_output_number_of_lines 1 } @test "delivers mail to existing alias with recipient delimiter" { - _run_in_container grep 'to=, orig_to=' /var/log/mail/mail.log - assert_success + _service_log_should_contain_string 'mail' 'to=, orig_to=' assert_output --partial 'status=sent' _should_output_number_of_lines 1 - _run_in_container grep 'to=' /var/log/mail/mail.log - assert_success + _service_log_should_contain_string 'mail' 'to=' refute_output --partial 'status=bounced' } @test "delivers mail to existing catchall" { - _run_in_container grep 'to=, orig_to=' /var/log/mail/mail.log - assert_success + _service_log_should_contain_string 'mail' 'to=, orig_to=' assert_output --partial 'status=sent' _should_output_number_of_lines 1 } @test "delivers mail to regexp alias" { - _run_in_container grep 'to=, orig_to=' /var/log/mail/mail.log - assert_success + _service_log_should_contain_string 'mail' 'to=, orig_to=' assert_output --partial 'status=sent' _should_output_number_of_lines 1 } @@ -219,23 +222,20 @@ function setup_file() { } @test "rejects mail to unknown user" { - _run_in_container grep ': Recipient address rejected: User unknown in virtual mailbox table' /var/log/mail/mail.log - assert_success + _service_log_should_contain_string 'mail' ': Recipient address rejected: User unknown in virtual mailbox table' _should_output_number_of_lines 1 } @test "redirects mail to external aliases" { - _run_in_container_bash "grep 'Passed CLEAN {RelayedInbound}' /var/log/mail/mail.log | grep -- '-> '" - assert_success - assert_output --partial ' -> ' + _service_log_should_contain_string 'mail' 'Passed CLEAN {RelayedInbound}' + run bash -c "grep ' -> ' <<< '${output}'" _should_output_number_of_lines 2 # assert_output --partial 'external.tld=user@example.test> -> ' } # TODO: Add a test covering case SPAMASSASSIN_SPAM_TO_INBOX=1 (default) @test "rejects spam" { - _run_in_container grep 'Blocked SPAM {NoBounceInbound,Quarantined}' /var/log/mail/mail.log - assert_success + _service_log_should_contain_string 'mail' 'Blocked SPAM {NoBounceInbound,Quarantined}' assert_output --partial ' -> ' _should_output_number_of_lines 1 @@ -258,7 +258,17 @@ function setup_file() { # Dovecot does not support SMTPUTF8, so while we can send we cannot receive # Better disable SMTPUTF8 support entirely if we can't handle it correctly @test "not advertising smtputf8" { - _send_email 'email-templates/smtp-ehlo' + # Query supported extensions; SMTPUTF8 should not be available. + # - This query requires a EHLO greeting to the destination server. + _send_email \ + --ehlo mail.external.tld \ + --protocol ESMTP \ + --server mail.example.test \ + --quit-after FIRST-EHLO + + # Ensure the output is actually related to what we want to refute against: + assert_output --partial 'EHLO mail.external.tld' + assert_output --partial '221 2.0.0 Bye' refute_output --partial 'SMTPUTF8' } diff --git a/test/tests/parallel/set3/mta/smtponly.bats b/test/tests/parallel/set3/mta/smtponly.bats index 66123de6914..7b1f8699edc 100644 --- a/test/tests/parallel/set3/mta/smtponly.bats +++ b/test/tests/parallel/set3/mta/smtponly.bats @@ -32,7 +32,16 @@ function teardown_file() { _default_teardown ; } assert_success # it looks as if someone tries to send mail to another domain outside of DMS - _send_email 'email-templates/smtp-only' + _send_email \ + --ehlo mail.origin.test \ + --protocol SSMTPA \ + --server mail.origin.test \ + --from user@origin.test \ + --to user@destination.test \ + --auth PLAIN \ + --auth-user user@origin.test \ + --auth-password secret + assert_success _wait_for_empty_mail_queue_in_container # this seemingly succeeds, but looking at the logs, it doesn't diff --git a/test/tests/parallel/set3/scripts/helper_functions.bats b/test/tests/parallel/set3/scripts/helper_functions.bats index 1a62414aaf8..518f87170f1 100644 --- a/test/tests/parallel/set3/scripts/helper_functions.bats +++ b/test/tests/parallel/set3/scripts/helper_functions.bats @@ -1,23 +1,114 @@ -load "${REPOSITORY_ROOT}/test/helper/setup" load "${REPOSITORY_ROOT}/test/helper/common" -BATS_TEST_NAME_PREFIX='[Scripts] (helper functions inside container) ' -CONTAINER_NAME='dms-test_helper_functions' +BATS_TEST_NAME_PREFIX='[Scripts] (helper functions) ' +SOURCE_BASE_PATH="${REPOSITORY_ROOT:?Expected REPOSITORY_ROOT to be set}/target/scripts/helpers" -function setup_file() { - _init_with_defaults - _common_container_setup +@test '(network.sh) _sanitize_ipv4_to_subnet_cidr' { + # shellcheck source=../../../../../target/scripts/helpers/network.sh + source "${SOURCE_BASE_PATH}/network.sh" + + run _sanitize_ipv4_to_subnet_cidr '255.255.255.255/0' + assert_output '0.0.0.0/0' + + run _sanitize_ipv4_to_subnet_cidr '192.168.255.14/20' + assert_output '192.168.240.0/20' + + run _sanitize_ipv4_to_subnet_cidr '192.168.255.14/32' + assert_output '192.168.255.14/32' +} + +@test '(utils.sh) _env_var_expect_zero_or_one' { + # shellcheck source=../../../../../target/scripts/helpers/log.sh + source "${SOURCE_BASE_PATH}/log.sh" + # shellcheck source=../../../../../target/scripts/helpers/utils.sh + source "${SOURCE_BASE_PATH}/utils.sh" + + ZERO=0 + ONE=1 + TWO=2 + + run _env_var_expect_zero_or_one ZERO + assert_success + + run _env_var_expect_zero_or_one ONE + assert_success + + run _env_var_expect_zero_or_one TWO + assert_failure + assert_output --partial "The value of 'TWO' (= '2') is not 0 or 1, but was expected to be" + + run _env_var_expect_zero_or_one UNSET + assert_failure + assert_output --partial "'UNSET' is not set, but was expected to be" + + run _env_var_expect_zero_or_one + assert_failure + assert_output --partial "ENV var name must be provided to _env_var_expect_zero_or_one" +} + +@test '(utils.sh) _env_var_expect_integer' { + # shellcheck source=../../../../../target/scripts/helpers/log.sh + source "${SOURCE_BASE_PATH}/log.sh" + # shellcheck source=../../../../../target/scripts/helpers/utils.sh + source "${SOURCE_BASE_PATH}/utils.sh" + + INTEGER=1234 + NEGATIVE=-${INTEGER} + NaN=not_an_integer + + run _env_var_expect_integer INTEGER + assert_success + + run _env_var_expect_integer NEGATIVE + assert_success + + run _env_var_expect_integer NaN + assert_failure + assert_output --partial "The value of 'NaN' is not an integer ('not_an_integer'), but was expected to be" + + run _env_var_expect_integer + assert_failure + assert_output --partial "ENV var name must be provided to _env_var_expect_integer" +} + +@test '(utils.sh) _convert_crlf_to_lf_if_necessary' { + # shellcheck source=../../../../../target/scripts/helpers/log.sh + source "${SOURCE_BASE_PATH}/log.sh" + # shellcheck source=../../../../../target/scripts/helpers/utils.sh + source "${SOURCE_BASE_PATH}/utils.sh" + + # Create a temporary file in the BATS test-case folder: + local TMP_DMS_CONFIG=$(mktemp -p "${BATS_TEST_TMPDIR}" -t 'dms_XXX.cf') + # A file with mixed line-endings including CRLF: + echo -en 'line one\nline two\r\n' > "${TMP_DMS_CONFIG}" + + # Confirm CRLF detected: + run file "${TMP_DMS_CONFIG}" + assert_output --partial 'CRLF' + + # Helper method detects and fixes: + _convert_crlf_to_lf_if_necessary "${TMP_DMS_CONFIG}" + run file "${TMP_DMS_CONFIG}" + refute_output --partial 'CRLF' } -function teardown_file() { _default_teardown ; } +@test '(utils.sh) _append_final_newline_if_missing' { + # shellcheck source=../../../../../target/scripts/helpers/log.sh + source "${SOURCE_BASE_PATH}/log.sh" + # shellcheck source=../../../../../target/scripts/helpers/utils.sh + source "${SOURCE_BASE_PATH}/utils.sh" -@test "_sanitize_ipv4_to_subnet_cidr" { - _run_in_container_bash "source /usr/local/bin/helpers/index.sh; _sanitize_ipv4_to_subnet_cidr 255.255.255.255/0" - assert_output "0.0.0.0/0" + # Create a temporary file in the BATS test-case folder: + local TMP_DMS_CONFIG=$(mktemp -p "${BATS_TEST_TMPDIR}" -t 'dms_XXX.cf') + # A file missing a final newline: + echo -en 'line one\nline two' > "${TMP_DMS_CONFIG}" - _run_in_container_bash "source /usr/local/bin/helpers/index.sh; _sanitize_ipv4_to_subnet_cidr 192.168.255.14/20" - assert_output "192.168.240.0/20" + # Confirm missing newline: + run bash -c "tail -c 1 '${TMP_DMS_CONFIG}' | wc -l" + assert_output '0' - _run_in_container_bash "source /usr/local/bin/helpers/index.sh; _sanitize_ipv4_to_subnet_cidr 192.168.255.14/32" - assert_output "192.168.255.14/32" + # Helper method detects and fixes: + _append_final_newline_if_missing "${TMP_DMS_CONFIG}" + run bash -c "tail -c 1 '${TMP_DMS_CONFIG}' | wc -l" + assert_output '1' } diff --git a/test/tests/parallel/set3/scripts/postconf-helper.bats b/test/tests/parallel/set3/scripts/postconf-helper.bats new file mode 100644 index 00000000000..d77d50ac2db --- /dev/null +++ b/test/tests/parallel/set3/scripts/postconf-helper.bats @@ -0,0 +1,48 @@ +load "${REPOSITORY_ROOT}/test/helper/common" +load "${REPOSITORY_ROOT}/test/helper/setup" + +BATS_TEST_NAME_PREFIX='[Scripts] (helper functions) (postfix - _add_to_or_update_postfix_main) ' +CONTAINER_NAME='dms-test_postconf-helper' +# Various tests for the helper function `_add_to_or_update_postfix_main()` +function setup_file() { + _init_with_defaults + _common_container_setup + # Begin tests without 'relayhost' defined in 'main.cf' + _run_in_container postconf -X relayhost + assert_success +} +function teardown_file() { _default_teardown ; } +# Add or modify in Postfix config `main.cf` a parameter key with the provided value. +# When the key already exists, the new value is appended (default), or prepended (explicitly requested). +# NOTE: This test-case helper is hard-coded for testing with the 'relayhost' parameter. +# +# @param ${1} = new value (appended or prepended) +# @param ${2} = action "append" (default) or "prepend" [OPTIONAL] +function _modify_postfix_main_config() { + _run_in_container_bash "source /usr/local/bin/helpers/{postfix,utils}.sh && _add_to_or_update_postfix_main relayhost '${1}' '${2}'" + _run_in_container grep '^relayhost' '/etc/postfix/main.cf' +} +@test "check if initial value is empty" { + _run_in_container postconf -h 'relayhost' + assert_output '' +} +@test "add single value" { + _modify_postfix_main_config 'single-value-test' + assert_output 'relayhost = single-value-test' +} +@test "prepend value" { + _modify_postfix_main_config 'prepend-test' 'prepend' + assert_output 'relayhost = prepend-test single-value-test' +} +@test "append value (explicit)" { + _modify_postfix_main_config 'append-test-explicit' 'append' + assert_output 'relayhost = prepend-test single-value-test append-test-explicit' +} +@test "append value (implicit)" { + _modify_postfix_main_config 'append-test-implicit' + assert_output 'relayhost = prepend-test single-value-test append-test-explicit append-test-implicit' +} +@test "try to append already existing value" { + _modify_postfix_main_config 'append-test-implicit' + assert_output 'relayhost = prepend-test single-value-test append-test-explicit append-test-implicit' +} diff --git a/test/tests/parallel/set3/scripts/setup_cli.bats b/test/tests/parallel/set3/scripts/setup_cli.bats index 737bf53d8ea..68576c352b2 100644 --- a/test/tests/parallel/set3/scripts/setup_cli.bats +++ b/test/tests/parallel/set3/scripts/setup_cli.bats @@ -28,7 +28,7 @@ function teardown_file() { _default_teardown ; } @test "show usage when no arguments provided" { run ./setup.sh assert_success - assert_output --partial "This is the main administration script that you use for all your interactions with" + assert_output --partial "This is the main administration command that you use for all your interactions with" } @test "exit with error when wrong arguments provided" { @@ -117,7 +117,7 @@ function teardown_file() { _default_teardown ; } assert_success # NOTE: Sometimes the directory still exists, possibly from change detection - # of the previous test (`email udpate`) triggering. Therefore, the function + # of the previous test (`email update`) triggering. Therefore, the function # `wait_until_change_detection_event_completes was added to the # `setup.sh email update` test. _repeat_in_container_until_success_or_timeout 60 "${CONTAINER_NAME}" bash -c '[[ ! -d /var/mail/example.com/user ]]' @@ -204,12 +204,12 @@ function teardown_file() { _default_teardown ; } run ./setup.sh -c "${CONTAINER_NAME}" quota set quota_user2 51M assert_failure - run /bin/sh -c "cat ${TEST_TMP_CONFIG}/dovecot-quotas.cf | grep -E '^quota_user@example.com\:12M\$' | wc -l | grep 1" + run /bin/sh -c "cat ${TEST_TMP_CONFIG}/dovecot-quotas.cf | grep -c -E '^quota_user@example.com\:12M\$' | grep 1" assert_success run ./setup.sh -c "${CONTAINER_NAME}" quota set quota_user@example.com 26M assert_success - run /bin/sh -c "cat ${TEST_TMP_CONFIG}/dovecot-quotas.cf | grep -E '^quota_user@example.com\:26M\$' | wc -l | grep 1" + run /bin/sh -c "cat ${TEST_TMP_CONFIG}/dovecot-quotas.cf | grep -c -E '^quota_user@example.com\:26M\$' | grep 1" assert_success run grep "quota_user2@example.com" "${TEST_TMP_CONFIG}/dovecot-quotas.cf" @@ -220,12 +220,12 @@ function teardown_file() { _default_teardown ; } @test "delquota" { run ./setup.sh -c "${CONTAINER_NAME}" quota set quota_user@example.com 12M assert_success - run /bin/sh -c "cat ${TEST_TMP_CONFIG}/dovecot-quotas.cf | grep -E '^quota_user@example.com\:12M\$' | wc -l | grep 1" + run /bin/sh -c "cat ${TEST_TMP_CONFIG}/dovecot-quotas.cf | grep -c -E '^quota_user@example.com\:12M\$' | grep 1" assert_success run ./setup.sh -c "${CONTAINER_NAME}" quota del unknown@domain.com assert_failure - run /bin/sh -c "cat ${TEST_TMP_CONFIG}/dovecot-quotas.cf | grep -E '^quota_user@example.com\:12M\$' | wc -l | grep 1" + run /bin/sh -c "cat ${TEST_TMP_CONFIG}/dovecot-quotas.cf | grep -c -E '^quota_user@example.com\:12M\$' | grep 1" assert_success run ./setup.sh -c "${CONTAINER_NAME}" quota del quota_user@example.com @@ -237,7 +237,7 @@ function teardown_file() { _default_teardown ; } @test "config dkim (help correctly displayed)" { run ./setup.sh -c "${CONTAINER_NAME}" config dkim help assert_success - assert_line --index 3 --partial " open-dkim - configure DomainKeys Identified Mail (DKIM)" + assert_line --index 3 --partial "open-dkim - Configure DKIM (DomainKeys Identified Mail)" } # debug @@ -260,13 +260,13 @@ function teardown_file() { _default_teardown ; } ./setup.sh -c "${CONTAINER_NAME}" relay add-domain example3.org smtp.relay.com 587 # check adding - run /bin/sh -c "cat ${TEST_TMP_CONFIG}/postfix-relaymap.cf | grep -e '^@example1.org\s\+\[smtp.relay1.com\]:2525' | wc -l | grep 1" + run /bin/sh -c "cat ${TEST_TMP_CONFIG}/postfix-relaymap.cf | grep -c -e '^@example1.org\s\+\[smtp.relay1.com\]:2525' | grep 1" assert_success # test default port - run /bin/sh -c "cat ${TEST_TMP_CONFIG}/postfix-relaymap.cf | grep -e '^@example2.org\s\+\[smtp.relay2.com\]:25' | wc -l | grep 1" + run /bin/sh -c "cat ${TEST_TMP_CONFIG}/postfix-relaymap.cf | grep -c -e '^@example2.org\s\+\[smtp.relay2.com\]:25' | grep 1" assert_success # test modifying - run /bin/sh -c "cat ${TEST_TMP_CONFIG}/postfix-relaymap.cf | grep -e '^@example3.org\s\+\[smtp.relay.com\]:587' | wc -l | grep 1" + run /bin/sh -c "cat ${TEST_TMP_CONFIG}/postfix-relaymap.cf | grep -c -e '^@example3.org\s\+\[smtp.relay.com\]:587' | grep 1" assert_success } @@ -276,16 +276,16 @@ function teardown_file() { _default_teardown ; } ./setup.sh -c "${CONTAINER_NAME}" relay add-auth example2.org smtp_user2 smtp_pass_new # test adding - run /bin/sh -c "cat ${TEST_TMP_CONFIG}/postfix-sasl-password.cf | grep -e '^@example.org\s\+smtp_user:smtp_pass' | wc -l | grep 1" + run /bin/sh -c "cat ${TEST_TMP_CONFIG}/postfix-sasl-password.cf | grep -c -e '^@example.org\s\+smtp_user:smtp_pass' | grep 1" assert_success # test updating - run /bin/sh -c "cat ${TEST_TMP_CONFIG}/postfix-sasl-password.cf | grep -e '^@example2.org\s\+smtp_user2:smtp_pass_new' | wc -l | grep 1" + run /bin/sh -c "cat ${TEST_TMP_CONFIG}/postfix-sasl-password.cf | grep -c -e '^@example2.org\s\+smtp_user2:smtp_pass_new' | grep 1" assert_success } @test "relay exclude-domain" { ./setup.sh -c "${CONTAINER_NAME}" relay exclude-domain example.org - run /bin/sh -c "cat ${TEST_TMP_CONFIG}/postfix-relaymap.cf | grep -e '^@example.org\s*$' | wc -l | grep 1" + run /bin/sh -c "cat ${TEST_TMP_CONFIG}/postfix-relaymap.cf | grep -c -e '^@example.org\s*$' | grep 1" assert_success } diff --git a/test/tests/serial/mail_pop3.bats b/test/tests/serial/mail_pop3.bats index cb07484a2d2..95ae14aa123 100644 --- a/test/tests/serial/mail_pop3.bats +++ b/test/tests/serial/mail_pop3.bats @@ -24,18 +24,19 @@ function teardown_file() { _default_teardown ; } } @test 'authentication works' { - _send_email 'auth/pop3-auth' '-w 1 0.0.0.0 110' + _nc_wrapper 'auth/pop3-auth.txt' '-w 1 0.0.0.0 110' + assert_success } @test 'added user authentication works' { - _send_email 'auth/added-pop3-auth' '-w 1 0.0.0.0 110' + _nc_wrapper 'auth/added-pop3-auth.txt' '-w 1 0.0.0.0 110' + assert_success } -@test '/var/log/mail/mail.log is error-free' { - _run_in_container grep 'non-null host address bits in' /var/log/mail/mail.log - assert_failure - _run_in_container grep ': error:' /var/log/mail/mail.log - assert_failure +# TODO: Remove in favor of a common helper method, as described in vmail-id.bats equivalent test-case +@test 'Mail log is error free' { + _service_log_should_not_contain_string 'mail' 'non-null host address bits in' + _service_log_should_not_contain_string 'mail' ': Error:' } @test '(Manage Sieve) disabled per default' { diff --git a/test/tests/serial/mail_with_imap.bats b/test/tests/serial/mail_with_imap.bats index d729c142657..94f1d5199c5 100644 --- a/test/tests/serial/mail_with_imap.bats +++ b/test/tests/serial/mail_with_imap.bats @@ -21,7 +21,8 @@ function setup_file() { function teardown_file() { _default_teardown ; } @test '(Dovecot) LDAP RIMAP connection and authentication works' { - _send_email 'auth/imap-auth' '-w 1 0.0.0.0 143' + _nc_wrapper 'auth/imap-auth.txt' '-w 1 0.0.0.0 143' + assert_success } @test '(SASLauthd) SASL RIMAP authentication works' { @@ -30,13 +31,28 @@ function teardown_file() { _default_teardown ; } } @test '(SASLauthd) RIMAP SMTP authentication works' { - _send_email 'auth/smtp-auth-login' '-w 5 0.0.0.0 25' - assert_output --partial 'Error: authentication not enabled' - - _send_email 'auth/smtp-auth-login' '-w 5 0.0.0.0 465' + _send_email --expect-rejection \ + --auth PLAIN \ + --auth-user user1@localhost.localdomain \ + --auth-password mypassword \ + --quit-after AUTH + assert_failure + assert_output --partial 'Host did not advertise authentication' + + _send_email \ + --port 465 \ + --auth PLAIN \ + --auth-user user1@localhost.localdomain \ + --auth-password mypassword \ + --quit-after AUTH assert_output --partial 'Authentication successful' - _send_email 'auth/smtp-auth-login' '-w 5 0.0.0.0 587' + _send_email \ + --port 587 \ + --auth PLAIN \ + --auth-user user1@localhost.localdomain \ + --auth-password mypassword \ + --quit-after AUTH assert_output --partial 'Authentication successful' } diff --git a/test/tests/serial/mail_with_ldap.bats b/test/tests/serial/mail_with_ldap.bats index 79100f01851..b5dd5ad6694 100644 --- a/test/tests/serial/mail_with_ldap.bats +++ b/test/tests/serial/mail_with_ldap.bats @@ -1,245 +1,442 @@ -load "${REPOSITORY_ROOT}/test/test_helper/common" +load "${REPOSITORY_ROOT}/test/helper/setup" +load "${REPOSITORY_ROOT}/test/helper/common" -function setup_file() { - pushd test/docker-openldap/ || return 1 - docker build -f Dockerfile -t ldap --no-cache . - popd || return 1 +BATS_TEST_NAME_PREFIX='[LDAP] ' +CONTAINER1_NAME='dms-test_ldap' +CONTAINER2_NAME='dms-test_ldap_provider' +# Single test-case specific containers: +CONTAINER3_NAME='dms-test_ldap_custom-config' - export DOMAIN='my-domain.com' - export FQDN_MAIL="mail.${DOMAIN}" - export FQDN_LDAP="ldap.${DOMAIN}" +function setup_file() { + export DMS_TEST_NETWORK='test-network-ldap' + export DMS_DOMAIN='example.test' + export FQDN_MAIL="mail.${DMS_DOMAIN}" + export FQDN_LDAP="ldap.${DMS_DOMAIN}" + # LDAP is provisioned with two domains (via `.ldif` files) unrelated to the FQDN of DMS: export FQDN_LOCALHOST_A='localhost.localdomain' export FQDN_LOCALHOST_B='localhost.otherdomain' - export DMS_TEST_NETWORK='test-network-ldap' + # Link the test containers to separate network: # NOTE: If the network already exists, test will fail to start. docker network create "${DMS_TEST_NETWORK}" - docker run -d --name ldap_for_mail \ - --env LDAP_DOMAIN="${FQDN_LOCALHOST_A}" \ - --network "${DMS_TEST_NETWORK}" \ - --network-alias 'ldap' \ + # Setup local openldap service: + # TODO: Migrate away from `bitnamilegacy/openldap`: https://github.com/docker-mailserver/docker-mailserver/issues/4582 + docker run --rm -d --name "${CONTAINER2_NAME}" \ + --env LDAP_ADMIN_PASSWORD=admin \ + --env LDAP_ROOT='dc=example,dc=test' \ + --env LDAP_PORT_NUMBER=389 \ + --env LDAP_SKIP_DEFAULT_TREE=yes \ + --volume "${REPOSITORY_ROOT}/test/config/ldap/openldap/ldifs/:/ldifs/:ro" \ + --volume "${REPOSITORY_ROOT}/test/config/ldap/openldap/schemas/:/schemas/:ro" \ --hostname "${FQDN_LDAP}" \ - --tty \ - ldap # Image name - - # _setup_ldap uses _replace_by_env_in_file with ENV vars like DOVECOT_TLS with a prefix (eg. DOVECOT_ or LDAP_) - local PRIVATE_CONFIG - PRIVATE_CONFIG=$(duplicate_config_for_container .) - docker run -d --name mail_with_ldap \ - -v "${PRIVATE_CONFIG}:/tmp/docker-mailserver" \ - -v "$(pwd)/test/test-files:/tmp/docker-mailserver-test:ro" \ - -e DOVECOT_PASS_FILTER="(&(objectClass=PostfixBookMailAccount)(uniqueIdentifier=%n))" \ - -e DOVECOT_TLS=no \ - -e DOVECOT_USER_FILTER="(&(objectClass=PostfixBookMailAccount)(uniqueIdentifier=%n))" \ - -e ACCOUNT_PROVISIONER=LDAP \ - -e PFLOGSUMM_TRIGGER=logrotate \ - -e ENABLE_SASLAUTHD=1 \ - -e LDAP_BIND_DN=cn=admin,dc=localhost,dc=localdomain \ - -e LDAP_BIND_PW=admin \ - -e LDAP_QUERY_FILTER_ALIAS="(|(&(mailAlias=%s)(objectClass=PostfixBookMailForward))(&(mailAlias=%s)(objectClass=PostfixBookMailAccount)(mailEnabled=TRUE)))" \ - -e LDAP_QUERY_FILTER_DOMAIN="(|(&(mail=*@%s)(objectClass=PostfixBookMailAccount)(mailEnabled=TRUE))(&(mailGroupMember=*@%s)(objectClass=PostfixBookMailAccount)(mailEnabled=TRUE))(&(mailalias=*@%s)(objectClass=PostfixBookMailForward)))" \ - -e LDAP_QUERY_FILTER_GROUP="(&(mailGroupMember=%s)(mailEnabled=TRUE))" \ - -e LDAP_QUERY_FILTER_SENDERS="(|(&(mail=%s)(mailEnabled=TRUE))(&(mailGroupMember=%s)(mailEnabled=TRUE))(|(&(mailAlias=%s)(objectClass=PostfixBookMailForward))(&(mailAlias=%s)(objectClass=PostfixBookMailAccount)(mailEnabled=TRUE)))(uniqueIdentifier=some.user.id))" \ - -e LDAP_QUERY_FILTER_USER="(&(mail=%s)(mailEnabled=TRUE))" \ - -e LDAP_START_TLS=no \ - -e LDAP_SEARCH_BASE=ou=people,dc=localhost,dc=localdomain \ - -e LDAP_SERVER_HOST=ldap \ - -e PERMIT_DOCKER=container \ - -e POSTMASTER_ADDRESS="postmaster@${FQDN_LOCALHOST_A}" \ - -e REPORT_RECIPIENT=1 \ - -e SASLAUTHD_MECHANISMS=ldap \ - -e SPOOF_PROTECTION=1 \ - -e SSL_TYPE='snakeoil' \ --network "${DMS_TEST_NETWORK}" \ - --hostname "${FQDN_MAIL}" \ - --tty \ - "${NAME}" # Image name - - wait_for_smtp_port_in_container mail_with_ldap + bitnamilegacy/openldap:latest + + _run_until_success_or_timeout 20 sh -c "docker logs ${CONTAINER2_NAME} 2>&1 | grep 'LDAP setup finished'" + + # + # Setup DMS container + # + + # LDAP filter queries explained. + # NOTE: All LDAP configs for Postfix (with the exception of `ldap-senders.cf`), return the `mail` attribute value of matched results. + # This is through the config key `result_attribute`, which the ENV substitution feature can only replace across all configs, not selectively like `query_filter`. + # NOTE: The queries below rely specifically upon attributes and classes defined by the schema `postfix-book.ldif`. These are not compatible with all LDAP setups. + + # `mailAlias`` is supported by both classes provided from the schema `postfix-book.ldif`, but `mailEnabled` is only available to `PostfixBookMailAccount` class: + local QUERY_ALIAS='(&(mailAlias=%s) (| (objectClass=PostfixBookMailForward) (&(objectClass=PostfixBookMailAccount)(mailEnabled=TRUE)) ))' + + # Postfix does domain lookups with the domain of the recipient to check if DMS manages the mail domain. + # For this lookup `%s` only represents the domain, not a full email address. Hence the match pattern using a wildcard prefix `*@`. + # For a breakdown, see QUERY_SENDERS comment. + # NOTE: Although `result_attribute = mail` will return each accounts full email address, Postfix will only compare to domain-part. + local QUERY_DOMAIN='(| (& (|(mail=*@%s) (mailAlias=*@%s) (mailGroupMember=*@%s)) (&(objectClass=PostfixBookMailAccount)(mailEnabled=TRUE)) ) (&(mailAlias=*@%s)(objectClass=PostfixBookMailForward)) )' + + # Simple queries for a single attribute that additionally requires `mailEnabled=TRUE` from the `PostfixBookMailAccount` class: + # NOTE: `mail` attribute is not unique to `PostfixBookMailAccount`. The `mailEnabled` attribute is to further control valid mail accounts. + # TODO: For tests, since `mailEnabled` is not relevant (always configured as TRUE currently), + # a simpler query like `mail=%s` or `mailGroupMember=%s` would be sufficient. The additional constraints could be covered in our docs instead. + local QUERY_GROUP='(&(mailGroupMember=%s) (&(objectClass=PostfixBookMailAccount)(mailEnabled=TRUE)) )' + local QUERY_USER='(&(mail=%s) (&(objectClass=PostfixBookMailAccount)(mailEnabled=TRUE)) )' + + # Given the sender address `%s` from Postfix, query LDAP for accounts that meet the search filter, + # the `result_attribute` is `mail` + `uid` (`userID`) attributes for login names that are authorized to use that sender address. + # One of the result values returned must match the login name of the user trying to send mail with that sender address. + # + # Filter: Search for accounts that meet any of the following conditions: + # - Has any attribute (`mail`, `mailAlias`, `mailGroupMember`) with a value of `%s`, AND is of class `PostfixBookMailAccount` with `mailEnabled=TRUE` attribute set. + # - Has attribute `mailAlias` with value of `%s` AND is of class PostfixBookMailForward + # - Has the attribute `userID` with value `some.user.id` + local QUERY_SENDERS='(| (& (|(mail=%s) (mailAlias=%s) (mailGroupMember=%s)) (&(objectClass=PostfixBookMailAccount)(mailEnabled=TRUE)) ) (&(mailAlias=%s)(objectClass=PostfixBookMailForward)) (userID=some.user.id))' + + # NOTE: The remaining queries below have been left as they were instead of rewritten, `mailEnabled` has no associated class requirement, nor does the class requirement ensure `mailEnabled=TRUE`. + + # When using SASLAuthd for login auth (only valid to Postfix in DMS), + # Postfix will pass on the login username and SASLAuthd will use it's backend to do a lookup. + # The current default is `uniqueIdentifier=%u`, but `%u` is inaccurate currently with LDAP backend + # as SASLAuthd will ignore the domain-part received (if any) and forward only the local-part as the query. + # TODO: Fix this by having supervisor start the service with `-r` option like it does `rimap` backend. + # NOTE: `%u` (full login with realm/domain-part) is presently equivalent to `%U` (local-part) only, + # As the `userID` is not a mail address, we ensure any domain-part is ignored, as a login name is not + # required to match the mail accounts actual `mail` attribute (nor the local-part), they are distinct. + # TODO: Docs should better cover this difference, as it does confuse some users of DMS (and past contributors to our tests..). + local SASLAUTHD_QUERY='(&(userID=%U)(mailEnabled=TRUE))' + + # Dovecot is configured to lookup a user account by their login name (`userID` in this case, but it could be any attribute like `mail`). + # Dovecot syntax token `%n` is the local-part of the full email address supplied as the login name. There must be a unique match on `userID` (which there will be as each account is configured via LDIF to use it in their DN) + # NOTE: We already have a constraint on the LDAP tree to search (`LDAP_SEARCH_BASE`), if all objects in that subtree use `PostfixBookMailAccount` class then there is no benefit in the extra constraint. + # TODO: For tests, that additional constraint is meaningless. We can detail it in our docs instead and just use `userID=%n`. + local DOVECOT_QUERY_PASS='(&(userID=%n)(objectClass=PostfixBookMailAccount))' + local DOVECOT_QUERY_USER='(&(userID=%n)(objectClass=PostfixBookMailAccount))' + + local ENV_LDAP_CONFIG=( + --env ACCOUNT_PROVISIONER=LDAP + + # Common LDAP ENV: + # NOTE: `scripts/startup/setup.d/ldap.sh:_setup_ldap()` uses `_replace_by_env_in_file()` to configure settings (stripping `DOVECOT_` / `LDAP_` prefixes): + --env LDAP_SERVER_HOST="ldap://${FQDN_LDAP}" + --env LDAP_SEARCH_BASE='ou=users,dc=example,dc=test' + --env LDAP_START_TLS=no + # Credentials needed for read access to LDAP_SEARCH_BASE: + --env LDAP_BIND_DN='cn=admin,dc=example,dc=test' + --env LDAP_BIND_PW='admin' + + # Postfix SASL auth provider (SASLAuthd instead of default Dovecot provider): + --env ENABLE_SASLAUTHD=1 + --env SASLAUTHD_MECHANISMS=ldap + --env SASLAUTHD_LDAP_FILTER="${SASLAUTHD_QUERY}" + + # ENV to configure LDAP configs for Dovecot + Postfix: + # Dovecot: + --env DOVECOT_PASS_FILTER="${DOVECOT_QUERY_PASS}" + --env DOVECOT_USER_FILTER="${DOVECOT_QUERY_USER}" + --env DOVECOT_TLS=no + + # Postfix: + --env LDAP_QUERY_FILTER_ALIAS="${QUERY_ALIAS}" + --env LDAP_QUERY_FILTER_DOMAIN="${QUERY_DOMAIN}" + --env LDAP_QUERY_FILTER_GROUP="${QUERY_GROUP}" + --env LDAP_QUERY_FILTER_SENDERS="${QUERY_SENDERS}" + --env LDAP_QUERY_FILTER_USER="${QUERY_USER}" + ) + + # Extra ENV needed to support specific test-cases: + local ENV_SUPPORT=( + # Required for openssl commands to be successful: + # NOTE: snakeoil cert is created (for `docker-mailserver.invalid`) via Debian post-install script for Postfix package. + # TODO: Use proper TLS cert + --env SSL_TYPE='snakeoil' + + # TODO; All below are questionable value to LDAP tests? + --env POSTMASTER_ADDRESS="postmaster@${FQDN_LOCALHOST_A}" # TODO: Only required because LDAP accounts use unrelated domain part. FQDN_LOCALHOST_A / ldif files can be adjusted to FQDN_MAIL + --env PFLOGSUMM_TRIGGER=logrotate + --env REPORT_RECIPIENT=1 # TODO: Invalid value, should be a recipient address (if not default postmaster), remove? + --env SPOOF_PROTECTION=1 + ) + + export CONTAINER_NAME=${CONTAINER1_NAME} + local CUSTOM_SETUP_ARGUMENTS=( + "${ENV_LDAP_CONFIG[@]}" + "${ENV_SUPPORT[@]}" + + --hostname "${FQDN_MAIL}" + --network "${DMS_TEST_NETWORK}" + ) + + _init_with_defaults + _common_container_setup 'CUSTOM_SETUP_ARGUMENTS' + _wait_for_smtp_port_in_container + + # Single test-case containers below cannot be defined in a test-case when expanding arrays + # defined in `setup()`. Those arrays would need to be hoisted up to the top of the file vars. + # Alternatively for ENV overrides, separate `.env` files could be used. Better options + # are available once switching to `compose.yaml` in tests. + + export CONTAINER_NAME=${CONTAINER3_NAME} + local CUSTOM_SETUP_ARGUMENTS=( + "${ENV_LDAP_CONFIG[@]}" + + # `hostname` should be unique when connecting containers via network: + --hostname "custom-config.${DMS_DOMAIN}" + --network "${DMS_TEST_NETWORK}" + ) + _init_with_defaults + # NOTE: `test/config/` has now been duplicated, can move test specific files to host-side `/tmp/docker-mailserver`: + mv "${TEST_TMP_CONFIG}/ldap/overrides/"*.cf "${TEST_TMP_CONFIG}/" + _common_container_setup 'CUSTOM_SETUP_ARGUMENTS' + + + # Set default implicit container fallback for helpers: + export CONTAINER_NAME=${CONTAINER1_NAME} } function teardown_file() { - docker rm -f ldap_for_mail mail_with_ldap + docker rm -f "${CONTAINER1_NAME}" "${CONTAINER2_NAME}" docker network rm "${DMS_TEST_NETWORK}" } -# postfix -@test "checking postfix: ldap lookup works correctly" { - run docker exec mail_with_ldap /bin/sh -c "postmap -q some.user@${FQDN_LOCALHOST_A} ldap:/etc/postfix/ldap-users.cf" - assert_success - assert_output "some.user@${FQDN_LOCALHOST_A}" - run docker exec mail_with_ldap /bin/sh -c "postmap -q postmaster@${FQDN_LOCALHOST_A} ldap:/etc/postfix/ldap-aliases.cf" - assert_success - assert_output "some.user@${FQDN_LOCALHOST_A}" - run docker exec mail_with_ldap /bin/sh -c "postmap -q employees@${FQDN_LOCALHOST_A} ldap:/etc/postfix/ldap-groups.cf" - assert_success - assert_output "some.user@${FQDN_LOCALHOST_A}" - - # Test of the user part of the domain is not the same as the uniqueIdentifier part in the ldap - run docker exec mail_with_ldap /bin/sh -c "postmap -q some.user.email@${FQDN_LOCALHOST_A} ldap:/etc/postfix/ldap-users.cf" - assert_success - assert_output "some.user.email@${FQDN_LOCALHOST_A}" - - # Test email receiving from a other domain then the primary domain of the mailserver - run docker exec mail_with_ldap /bin/sh -c "postmap -q some.other.user@${FQDN_LOCALHOST_B} ldap:/etc/postfix/ldap-users.cf" - assert_success - assert_output "some.other.user@${FQDN_LOCALHOST_B}" - run docker exec mail_with_ldap /bin/sh -c "postmap -q postmaster@${FQDN_LOCALHOST_B} ldap:/etc/postfix/ldap-aliases.cf" - assert_success - assert_output "some.other.user@${FQDN_LOCALHOST_B}" - run docker exec mail_with_ldap /bin/sh -c "postmap -q employees@${FQDN_LOCALHOST_B} ldap:/etc/postfix/ldap-groups.cf" - assert_success - assert_output "some.other.user@${FQDN_LOCALHOST_B}" +# Could optionally call `_default_teardown` in test-cases that have specific containers. +# This will otherwise handle it implicitly which is helpful when the test-case hits a failure, +# As failure will bail early missing teardown, which then prevents network cleanup. This way is safer: +function teardown() { + if [[ ${CONTAINER_NAME} != "${CONTAINER1_NAME}" ]] \ + && [[ ${CONTAINER_NAME} != "${CONTAINER2_NAME}" ]] + then + _default_teardown + fi } -@test "checking postfix: ldap custom config files copied" { - run docker exec mail_with_ldap /bin/sh -c "grep '# Testconfig for ldap integration' /etc/postfix/ldap-users.cf" - assert_success +# postfix +# NOTE: Each of the 3 user accounts tested below are defined in separate LDIF config files, +# Those are bundled into the locally built OpenLDAP Dockerfile. +@test "postfix: ldap lookup works correctly" { + _should_exist_in_ldap_tables "some.user@${FQDN_LOCALHOST_A}" - run docker exec mail_with_ldap /bin/sh -c "grep '# Testconfig for ldap integration' /etc/postfix/ldap-groups.cf" - assert_success + # Test email receiving from a other domain then the primary domain of the mailserver + _should_exist_in_ldap_tables "some.other.user@${FQDN_LOCALHOST_B}" - run docker exec mail_with_ldap /bin/sh -c "grep '# Testconfig for ldap integration' /etc/postfix/ldap-aliases.cf" + # Should not require `userID` / `uid` to match the local part of `mail` (`.ldif` defined settings): + # REF: https://github.com/docker-mailserver/docker-mailserver/pull/642#issuecomment-313916384 + # NOTE: This account has no `mailAlias` or `mailGroupMember` defined in it's `.ldif`. + local MAIL_ACCOUNT="some.user.email@${FQDN_LOCALHOST_A}" + _run_in_container postmap -q "${MAIL_ACCOUNT}" ldap:/etc/postfix/ldap-users.cf assert_success + assert_output "${MAIL_ACCOUNT}" } -@test "checking postfix: ldap config overwrites success" { - run docker exec mail_with_ldap /bin/sh -c "grep 'server_host = ldap' /etc/postfix/ldap-users.cf" - assert_success - - run docker exec mail_with_ldap /bin/sh -c "grep 'start_tls = no' /etc/postfix/ldap-users.cf" - assert_success - - run docker exec mail_with_ldap /bin/sh -c "grep 'search_base = ou=people,dc=localhost,dc=localdomain' /etc/postfix/ldap-users.cf" - assert_success - - run docker exec mail_with_ldap /bin/sh -c "grep 'bind_dn = cn=admin,dc=localhost,dc=localdomain' /etc/postfix/ldap-users.cf" - assert_success +# Custom LDAP config files support: +@test "postfix: ldap custom config files copied" { + # Use the test-case specific container from `setup()` (change only applies to test-case): + export CONTAINER_NAME=${CONTAINER3_NAME} + + local LDAP_CONFIGS_POSTFIX=( + /etc/postfix/ldap-users.cf + /etc/postfix/ldap-groups.cf + /etc/postfix/ldap-aliases.cf + ) + + for LDAP_CONFIG in "${LDAP_CONFIGS_POSTFIX[@]}"; do + _run_in_container grep '# Testconfig for ldap integration' "${LDAP_CONFIG}" + assert_success + done +} - run docker exec mail_with_ldap /bin/sh -c "grep 'server_host = ldap' /etc/postfix/ldap-groups.cf" - assert_success +@test "postfix: ldap config overwrites success" { + local LDAP_SETTINGS_POSTFIX=( + "server_host = ldap://${FQDN_LDAP}" + 'start_tls = no' + 'search_base = ou=users,dc=example,dc=test' + 'bind_dn = cn=admin,dc=example,dc=test' + ) + + for LDAP_SETTING in "${LDAP_SETTINGS_POSTFIX[@]}"; do + # "${LDAP_SETTING%=*}" is to match only the key portion of the var (helpful for assert_output error messages) + # NOTE: `start_tls = no` is a default setting, but the white-space differs when ENV `LDAP_START_TLS` is not set explicitly. + _run_in_container grep "${LDAP_SETTING%=*}" /etc/postfix/ldap-users.cf + assert_output "${LDAP_SETTING}" + assert_success + + _run_in_container grep "${LDAP_SETTING%=*}" /etc/postfix/ldap-groups.cf + assert_output "${LDAP_SETTING}" + assert_success + + _run_in_container grep "${LDAP_SETTING%=*}" /etc/postfix/ldap-aliases.cf + assert_output "${LDAP_SETTING}" + assert_success + done +} - run docker exec mail_with_ldap /bin/sh -c "grep 'start_tls = no' /etc/postfix/ldap-groups.cf" +# dovecot +@test "dovecot: ldap imap connection and authentication works" { + _nc_wrapper 'auth/imap-ldap-auth.txt' '-w 1 0.0.0.0 143' assert_success +} - run docker exec mail_with_ldap /bin/sh -c "grep 'search_base = ou=people,dc=localhost,dc=localdomain' /etc/postfix/ldap-groups.cf" - assert_success +@test "dovecot: ldap mail delivery works" { + _should_successfully_deliver_mail_to "some.user@${FQDN_LOCALHOST_A}" "/var/mail/${FQDN_LOCALHOST_A}/some.user/new/" - run docker exec mail_with_ldap /bin/sh -c "grep 'bind_dn = cn=admin,dc=localhost,dc=localdomain' /etc/postfix/ldap-groups.cf" - assert_success + # Should support delivering to a local recipient with a different domain (and disjoint mail location): + # NOTE: Mail is delivered to location defined in `.ldif` (an account config setting, either `mailHomeDirectory` or `mailStorageDirectory`). + # `some.other.user` has been configured to use a mailbox domain different from it's address domain part, hence the difference here: + _should_successfully_deliver_mail_to "some.other.user@${FQDN_LOCALHOST_B}" "/var/mail/${FQDN_LOCALHOST_A}/some.other.user/new/" +} - run docker exec mail_with_ldap /bin/sh -c "grep 'server_host = ldap' /etc/postfix/ldap-aliases.cf" - assert_success +@test "dovecot: ldap config overwrites success" { + local LDAP_SETTINGS_DOVECOT=( + "uris = ldap://${FQDN_LDAP}" + 'tls = no' + 'base = ou=users,dc=example,dc=test' + 'dn = cn=admin,dc=example,dc=test' + ) + + for LDAP_SETTING in "${LDAP_SETTINGS_DOVECOT[@]}"; do + _run_in_container grep "${LDAP_SETTING%=*}" /etc/dovecot/dovecot-ldap.conf.ext + assert_output "${LDAP_SETTING}" + assert_success + done +} - run docker exec mail_with_ldap /bin/sh -c "grep 'start_tls = no' /etc/postfix/ldap-aliases.cf" +# Requires ENV `POSTMASTER_ADDRESS` +# NOTE: Not important to LDAP feature tests? +@test "dovecot: postmaster address" { + _run_in_container grep "postmaster_address = postmaster@${FQDN_LOCALHOST_A}" /etc/dovecot/conf.d/15-lda.conf assert_success +} - run docker exec mail_with_ldap /bin/sh -c "grep 'search_base = ou=people,dc=localhost,dc=localdomain' /etc/postfix/ldap-aliases.cf" - assert_success +# NOTE: `target/scripts/startup/setup.d/dovecot.sh` should prevent enabling the quotas feature when using LDAP: +@test "dovecot: quota plugin is disabled" { + # Dovecot configs have not enabled the quota plugins: + _run_in_container grep "\$mail_plugins quota" /etc/dovecot/conf.d/10-mail.conf + assert_failure + _run_in_container grep "\$mail_plugins imap_quota" /etc/dovecot/conf.d/20-imap.conf + assert_failure - run docker exec mail_with_ldap /bin/sh -c "grep 'bind_dn = cn=admin,dc=localhost,dc=localdomain' /etc/postfix/ldap-aliases.cf" + # Dovecot Quota config only present with disabled extension: + _run_in_container_bash '[[ -f /etc/dovecot/conf.d/90-quota.conf ]]' + assert_failure + _run_in_container_bash '[[ -f /etc/dovecot/conf.d/90-quota.conf.disab ]]' assert_success -} -# dovecot -@test "checking dovecot: ldap imap connection and authentication works" { - run docker exec mail_with_ldap /bin/sh -c "nc -w 1 0.0.0.0 143 < /tmp/docker-mailserver-test/auth/imap-ldap-auth.txt" - assert_success + # Postfix quotas policy service not configured in `main.cf`: + _run_in_container postconf smtpd_recipient_restrictions + refute_output --partial 'check_policy_service inet:localhost:65265' } -@test "checking dovecot: ldap mail delivery works" { - run docker exec mail_with_ldap /bin/sh -c "sendmail -f user@external.tld some.user@${FQDN_LOCALHOST_A} < /tmp/docker-mailserver-test/email-templates/test-email.txt" - sleep 10 - run docker exec mail_with_ldap /bin/sh -c "ls -A /var/mail/${FQDN_LOCALHOST_A}/some.user/new | wc -l" +@test "saslauthd: sasl ldap authentication works" { + _run_in_container testsaslauthd -u some.user -p secret assert_success - assert_output 1 } -@test "checking dovecot: ldap mail delivery works for a different domain then the mailserver" { - run docker exec mail_with_ldap /bin/sh -c "sendmail -f user@external.tld some.other.user@${FQDN_LOCALHOST_B} < /tmp/docker-mailserver-test/email-templates/test-email.txt" - sleep 10 - run docker exec mail_with_ldap /bin/sh -c "ls -A /var/mail/${FQDN_LOCALHOST_A}/some.other.user/new | wc -l" +# Requires ENV `PFLOGSUMM_TRIGGER=logrotate` +@test "pflogsumm delivery" { + # Verify default sender is `mailserver-report` when ENV `PFLOGSUMM_SENDER` + `REPORT_SENDER` are unset: + # NOTE: Mail is sent from Postfix (configured hostname used as domain part) + _run_in_container grep "mailserver-report@${FQDN_MAIL}" /etc/logrotate.d/maillog assert_success - assert_output 1 -} -@test "checking dovecot: ldap config overwrites success" { - run docker exec mail_with_ldap /bin/sh -c "grep 'uris = ldap://ldap' /etc/dovecot/dovecot-ldap.conf.ext" - assert_success - run docker exec mail_with_ldap /bin/sh -c "grep 'tls = no' /etc/dovecot/dovecot-ldap.conf.ext" - assert_success - run docker exec mail_with_ldap /bin/sh -c "grep 'base = ou=people,dc=localhost,dc=localdomain' /etc/dovecot/dovecot-ldap.conf.ext" - assert_success - run docker exec mail_with_ldap /bin/sh -c "grep 'dn = cn=admin,dc=localhost,dc=localdomain' /etc/dovecot/dovecot-ldap.conf.ext" + # When `LOGROTATE_INTERVAL` is unset, the default should be configured as `weekly`: + _run_in_container grep 'weekly' /etc/logrotate.d/maillog assert_success } -@test "checking dovecot: postmaster address" { - run docker exec mail_with_ldap /bin/sh -c "grep 'postmaster_address = postmaster@${FQDN_LOCALHOST_A}' /etc/dovecot/conf.d/15-lda.conf" - assert_success -} +# ATTENTION: Remaining tests must come after "dovecot: ldap mail delivery works" since the below tests would affect the expected count (by delivering extra mail), +# Thus not friendly for running testcases in this file in parallel -@test "checking dovecot: quota plugin is disabled" { - run docker exec mail_with_ldap /bin/sh -c "grep '\$mail_plugins quota' /etc/dovecot/conf.d/10-mail.conf" - assert_failure - run docker exec mail_with_ldap /bin/sh -c "grep '\$mail_plugins imap_quota' /etc/dovecot/conf.d/20-imap.conf" - assert_failure - run docker exec mail_with_ldap ls /etc/dovecot/conf.d/90-quota.conf - assert_failure - run docker exec mail_with_ldap ls /etc/dovecot/conf.d/90-quota.conf.disab - assert_success -} +# Requires ENV `SPOOF_PROTECTION=1` for the expected assert_output +@test "spoofing (with LDAP): rejects sender forging" { + _wait_for_smtp_port_in_container_to_respond dms-test_ldap -@test "checking postfix: dovecot quota absent in postconf" { - run docker exec mail_with_ldap /bin/bash -c "postconf | grep 'check_policy_service inet:localhost:65265'" + _send_email --expect-rejection \ + --port 465 -tlsc --auth PLAIN \ + --auth-user some.user@localhost.localdomain \ + --auth-password secret \ + --ehlo mail \ + --from ldap@localhost.localdomain \ + --data 'auth/ldap-smtp-auth-spoofed.txt' assert_failure + assert_output --partial 'Sender address rejected: not owned by user' } -@test "checking spoofing (with LDAP): rejects sender forging" { - wait_for_smtp_port_in_container_to_respond mail_with_ldap - run docker exec mail_with_ldap /bin/sh -c "openssl s_client -quiet -connect 0.0.0.0:465 < /tmp/docker-mailserver-test/auth/ldap-smtp-auth-spoofed.txt | grep 'Sender address rejected: not owned by user'" - assert_success +@test "spoofing (with LDAP): accepts sending as alias" { + _send_email \ + --port 465 -tlsc --auth PLAIN \ + --auth-user some.user@localhost.localdomain \ + --auth-password secret \ + --ehlo mail \ + --from postmaster@localhost.localdomain \ + --to some.user@localhost.localdomain \ + --data 'auth/ldap-smtp-auth-spoofed-alias.txt' + assert_output --partial 'End data with' } -# ATTENTION: these tests must come after "checking dovecot: ldap mail delivery works" since they will deliver an email which skews the count in said test, leading to failure -@test "checking spoofing: accepts sending as alias (with LDAP)" { - run docker exec mail_with_ldap /bin/sh -c "openssl s_client -quiet -connect 0.0.0.0:465 < /tmp/docker-mailserver-test/auth/ldap-smtp-auth-spoofed-alias.txt | grep 'End data with'" - assert_success -} -@test "checking spoofing: uses senders filter" { +@test "spoofing (with LDAP): uses senders filter" { # skip introduced with #3006, changing port 25 to 465 + # Template used has invalid AUTH: https://github.com/docker-mailserver/docker-mailserver/pull/3006#discussion_r1073321432 skip 'TODO: This test seems to have been broken from the start (?)' - - run docker exec mail_with_ldap /bin/sh -c "openssl s_client -quiet -connect 0.0.0.0:465 < /tmp/docker-mailserver-test/auth/ldap-smtp-auth-spoofed-sender-with-filter-exception.txt | grep 'Sender address rejected: not owned by user'" - assert_success + + _send_email --expect-rejection \ + --port 465 -tlsc --auth PLAIN \ + --auth-user some.user.email@localhost.localdomain \ + --auth-password secret \ + --ehlo mail \ + --from randomspoofedaddress@localhost.localdomain \ + --to some.user@localhost.localdomain \ + --data 'auth/ldap-smtp-auth-spoofed-sender-with-filter-exception.txt' + assert_failure + assert_output --partial 'Sender address rejected: not owned by user' } -# saslauthd -@test "checking saslauthd: sasl ldap authentication works" { - run docker exec mail_with_ldap bash -c "testsaslauthd -u some.user -p secret" - assert_success +@test "saslauthd: ldap smtp authentication" { + _send_email --expect-rejection \ + --auth PLAIN \ + --auth-user some.user@localhost.localdomain \ + --auth-password wrongpassword \ + --quit-after AUTH + assert_failure + assert_output --partial 'Host did not advertise authentication' + + _send_email \ + --port 465 -tlsc \ + --auth LOGIN \ + --auth-user some.user@localhost.localdomain \ + --auth-password secret \ + --quit-after AUTH + assert_output --partial 'Authentication successful' + + _send_email \ + --port 587 -tls \ + --auth PLAIN \ + --auth-user some.user@localhost.localdomain \ + --auth-password secret \ + --quit-after AUTH + assert_success + assert_output --partial 'Authentication successful' } -@test "checking saslauthd: ldap smtp authentication" { - run docker exec mail_with_ldap /bin/sh -c "nc -w 5 0.0.0.0 25 < /tmp/docker-mailserver-test/auth/sasl-ldap-smtp-auth.txt | grep 'Error: authentication not enabled'" +# +# Test helper methods: +# + +function _should_exist_in_ldap_tables() { + local MAIL_ACCOUNT=${1:?Mail account is required} + local DOMAIN_PART="${MAIL_ACCOUNT#*@}" + + # Each LDAP config file sets `query_filter` to lookup a key in LDAP (values defined in `.ldif` test files) + # `mail` (ldap-users), `mailAlias` (ldap-aliases), `mailGroupMember` (ldap-groups) + # `postmap` is queried with the mail account address, and the LDAP service should respond with + # `result_attribute` which is the LDAP `mail` value (should match what we'r'e querying `postmap` with) + + _run_in_container postmap -q "${MAIL_ACCOUNT}" ldap:/etc/postfix/ldap-users.cf assert_success - run docker exec mail_with_ldap /bin/sh -c "openssl s_client -quiet -connect 0.0.0.0:465 < /tmp/docker-mailserver-test/auth/sasl-ldap-smtp-auth.txt | grep 'Authentication successful'" + assert_output "${MAIL_ACCOUNT}" + + # Check which account has the `postmaster` virtual alias: + _run_in_container postmap -q "postmaster@${DOMAIN_PART}" ldap:/etc/postfix/ldap-aliases.cf assert_success - run docker exec mail_with_ldap /bin/sh -c "openssl s_client -quiet -starttls smtp -connect 0.0.0.0:587 < /tmp/docker-mailserver-test/auth/sasl-ldap-smtp-auth.txt | grep 'Authentication successful'" + assert_output "${MAIL_ACCOUNT}" + + _run_in_container postmap -q "employees@${DOMAIN_PART}" ldap:/etc/postfix/ldap-groups.cf assert_success + assert_output "${MAIL_ACCOUNT}" } -# -# Pflogsumm delivery check -# +# NOTE: `test-email.txt` is only used for these two LDAP tests with `sendmail` command. +# The file excludes sender/recipient addresses, thus not usable with `_send_email()` helper (`nc` command)? +# TODO: Could probably adapt? +function _should_successfully_deliver_mail_to() { + local SENDER_ADDRESS='user@external.tld' + local RECIPIENT_ADDRESS=${1:?Recipient address is required} + local MAIL_STORAGE_RECIPIENT=${2:?Recipient storage location is required} + local MAIL_TEMPLATE='/tmp/docker-mailserver-test/emails/test-email.txt' -@test "checking pflogsum delivery" { - # checking default sender is correctly set when env variable not defined - run docker exec mail_with_ldap grep "mailserver-report@${FQDN_MAIL}" /etc/logrotate.d/maillog - assert_success + _run_in_container_bash "sendmail -f ${SENDER_ADDRESS} ${RECIPIENT_ADDRESS} < ${MAIL_TEMPLATE}" + _wait_for_empty_mail_queue_in_container - # checking default logrotation setup - run docker exec mail_with_ldap grep "weekly" /etc/logrotate.d/maillog + _run_in_container grep -R 'This is a test mail.' "${MAIL_STORAGE_RECIPIENT}" assert_success + _should_output_number_of_lines 1 + + # NOTE: Prevents compatibility for running testcases in parallel (for same container) when the count could become racey: + _count_files_in_directory_in_container "${MAIL_STORAGE_RECIPIENT}" 1 } diff --git a/test/tests/serial/mail_with_oauth2.bats b/test/tests/serial/mail_with_oauth2.bats new file mode 100644 index 00000000000..968d63abad4 --- /dev/null +++ b/test/tests/serial/mail_with_oauth2.bats @@ -0,0 +1,125 @@ +load "${REPOSITORY_ROOT}/test/helper/setup" +load "${REPOSITORY_ROOT}/test/helper/common" + +BATS_TEST_NAME_PREFIX='[OAuth2] ' +CONTAINER1_NAME='dms-test_oauth2' +CONTAINER2_NAME='dms-test_oauth2_provider' + +function setup_file() { + export DMS_TEST_NETWORK='test-network-oauth2' + export DMS_DOMAIN='example.test' + export FQDN_MAIL="mail.${DMS_DOMAIN}" + export FQDN_OAUTH2="auth.${DMS_DOMAIN}" + + # Link the test containers to separate network: + # NOTE: If the network already exists, test will fail to start. + docker network create "${DMS_TEST_NETWORK}" + + # Setup local oauth2 provider service: + docker run --rm -d --name "${CONTAINER2_NAME}" \ + --hostname "${FQDN_OAUTH2}" \ + --network "${DMS_TEST_NETWORK}" \ + --volume "${REPOSITORY_ROOT}/test/config/oauth2/Caddyfile:/etc/caddy/Caddyfile:ro" \ + caddy:2.7 + + _run_until_success_or_timeout 20 bash -c "docker logs ${CONTAINER2_NAME} 2>&1 | grep 'serving initial configuration'" + + # + # Setup DMS container + # + + # Add OAuth2 configuration so that Dovecot can query our mocked identity provider (CONTAINER2) + local ENV_OAUTH2_CONFIG=( + --env ENABLE_OAUTH2=1 + --env OAUTH2_INTROSPECTION_URL=http://auth.example.test/userinfo + ) + + export CONTAINER_NAME=${CONTAINER1_NAME} + local CUSTOM_SETUP_ARGUMENTS=( + "${ENV_OAUTH2_CONFIG[@]}" + + --hostname "${FQDN_MAIL}" + --network "${DMS_TEST_NETWORK}" + ) + + _init_with_defaults + _common_container_setup 'CUSTOM_SETUP_ARGUMENTS' + _wait_for_tcp_port_in_container 143 + + # Set default implicit container fallback for helpers: + export CONTAINER_NAME=${CONTAINER1_NAME} + + # An initial connection needs to be made first, otherwise the auth attempts fail + _run_in_container_bash 'nc -vz 0.0.0.0 143' +} + +function teardown_file() { + docker rm -f "${CONTAINER1_NAME}" "${CONTAINER2_NAME}" + docker network rm "${DMS_TEST_NETWORK}" +} + +@test "should authenticate with XOAUTH2" { + # curl 7.80.0 (Nov 2021) broke XOAUTH2 support (DMS v14 release with Debian 12 packages curl 7.88.1) + # https://github.com/docker-mailserver/docker-mailserver/pull/3403#issuecomment-1907100624 + # + # Fixed in curl 8.6.0 (Jan 31 2024): + # - https://github.com/curl/curl/issues/10259 + # - https://github.com/curl/curl/commit/7b2d98dfadf209108aa7772ee21ae42e3dab219f (referenced in release changelog by commit title) + # - https://github.com/curl/curl/releases/tag/curl-8_6_0 + skip 'unable to test XOAUTH2 mechanism due to bug in curl versions 7.80.0 --> 8.5.0' + + __should_login_successfully_with 'XOAUTH2' +} + +@test "should authenticate with OAUTHBEARER" { + __should_login_successfully_with 'OAUTHBEARER' +} + +function __should_login_successfully_with() { + local AUTH_METHOD=${1} + # These values are the auth credentials checked against the Caddy `/userinfo` endpoint: + local USER_ACCOUNT='user1@localhost.localdomain' + local ACCESS_TOKEN='DMS_YWNjZXNzX3Rva2Vu' + + __verify_auth_with_imap + __verify_auth_with_smtp +} + +# Dovecot direct auth verification via IMAP: +function __verify_auth_with_imap() { + # NOTE: Include the `--verbose` option if you're troubleshooting and want to see the protocol exchange messages + # NOTE: `--user username:password` is valid for testing `PLAIN` auth mechanism, but you should prefer swaks instead. + _run_in_container curl --silent \ + --login-options "AUTH=${AUTH_METHOD}" --oauth2-bearer "${ACCESS_TOKEN}" --user "${USER_ACCOUNT}" \ + --url 'imap://localhost:143' -X 'LOGOUT' + + __dovecot_logs_should_verify_success +} + +# Postfix delegates by default to Dovecot via SASL: +# NOTE: This won't be compatible with LDAP if `ENABLE_SASLAUTHD=1` with `ldap` SASL mechanism: +function __verify_auth_with_smtp() { + # NOTE: `--upload-file` with some mail content seems required for using curl to test OAuth2 authentication. + # TODO: Replace with swaks and early exit option when it supports XOAUTH2 + OAUTHBEARER: + _run_in_container curl --silent \ + --login-options "AUTH=${AUTH_METHOD}" --oauth2-bearer "${ACCESS_TOKEN}" --user "${USER_ACCOUNT}" \ + --url 'smtp://localhost:587' --mail-from "${USER_ACCOUNT}" --mail-rcpt "${USER_ACCOUNT}" --upload-file - <<< 'RFC 5322 content - not important' + + # Postfix specific auth logs: + _run_in_container grep 'postfix/submission/smtpd' /var/log/mail.log + assert_output --partial "sasl_method=${AUTH_METHOD}, sasl_username=${USER_ACCOUNT}" + + # Dovecot logs should still be checked as it is handling the actual auth process under the hood: + __dovecot_logs_should_verify_success +} + +function __dovecot_logs_should_verify_success() { + # Inspect the relevant Dovecot logs to catch failure / success: + _service_log_should_contain_string 'mail' 'dovecot:' + refute_output --partial 'oauth2 failed: Introspection failed' + assert_output --partial "dovecot: imap-login: Login: user=<${USER_ACCOUNT}>, method=${AUTH_METHOD}" + + # If another PassDB is enabled, it should not have been attempted with the XOAUTH2 / OAUTHBEARER mechanisms: + # dovecot: auth: passwd-file(${USER_ACCOUNT},127.0.0.1): Password mismatch (SHA1 of given password: d390c1) - trying the next passdb + refute_output --partial 'trying the next passdb' +} diff --git a/test/tests/serial/open_dkim.bats b/test/tests/serial/open_dkim.bats index a71cb3e7025..b43b850f627 100644 --- a/test/tests/serial/open_dkim.bats +++ b/test/tests/serial/open_dkim.bats @@ -62,7 +62,7 @@ function teardown() { _default_teardown ; } __init_container_without_waiting - __should_generate_dkim_key 6 + __should_generate_dkim_key 7 __assert_outputs_common_dkim_logs __should_have_tables_trustedhosts_for_domain @@ -78,7 +78,7 @@ function teardown() { _default_teardown ; } # Only mount single config file (postfix-virtual.cf): __init_container_without_waiting "${PWD}/test/config/postfix-virtual.cf:/tmp/docker-mailserver/postfix-virtual.cf:ro" - __should_generate_dkim_key 5 + __should_generate_dkim_key 6 __assert_outputs_common_dkim_logs __should_have_tables_trustedhosts_for_domain @@ -95,7 +95,7 @@ function teardown() { _default_teardown ; } # Only mount single config file (postfix-accounts.cf): __init_container_without_waiting "${PWD}/test/config/postfix-accounts.cf:/tmp/docker-mailserver/postfix-accounts.cf:ro" - __should_generate_dkim_key 5 + __should_generate_dkim_key 6 __assert_outputs_common_dkim_logs __should_have_tables_trustedhosts_for_domain @@ -113,14 +113,14 @@ function teardown() { _default_teardown ; } __init_container_without_waiting '/tmp/docker-mailserver' # generate first key (with a custom selector) - __should_generate_dkim_key 4 '2048' 'domain1.tld' 'mailer' + __should_generate_dkim_key 5 '1024' 'domain1.tld' 'mailer' __assert_outputs_common_dkim_logs # generate two additional keys different to the previous one - __should_generate_dkim_key 2 '2048' 'domain2.tld,domain3.tld' + __should_generate_dkim_key 2 '1024' 'domain2.tld,domain3.tld' __assert_logged_dkim_creation 'domain2.tld' __assert_logged_dkim_creation 'domain3.tld' # generate an additional key whilst providing already existing domains - __should_generate_dkim_key 1 '2048' 'domain3.tld,domain4.tld' + __should_generate_dkim_key 1 '1024' 'domain3.tld,domain4.tld' __assert_logged_dkim_creation 'domain4.tld' __should_have_tables_trustedhosts_for_domain @@ -183,21 +183,21 @@ function __assert_logged_dkim_creation() { function __assert_outputs_common_dkim_logs() { refute_output --partial 'No entries found, no keys to make' - assert_output --partial 'Creating DKIM KeyTable' - assert_output --partial 'Creating DKIM SigningTable' - assert_output --partial 'Creating DKIM TrustedHosts' + assert_output --partial "Creating OpenDKIM config '/tmp/docker-mailserver/opendkim/KeyTable'" + assert_output --partial "Creating OpenDKIM config '/tmp/docker-mailserver/opendkim/SigningTable'" + assert_output --partial "Creating OpenDKIM config '/tmp/docker-mailserver/opendkim/TrustedHosts'" } function __should_support_creating_key_of_size() { local EXPECTED_KEYSIZE=${1:-} - __should_generate_dkim_key 6 "${EXPECTED_KEYSIZE}" + __should_generate_dkim_key 7 "${EXPECTED_KEYSIZE}" __assert_outputs_common_dkim_logs __assert_logged_dkim_creation 'localdomain2.com' __assert_logged_dkim_creation 'localhost.localdomain' __assert_logged_dkim_creation 'otherdomain.tld' - __should_have_expected_files "${EXPECTED_KEYSIZE:-4096}" + __should_have_expected_files "${EXPECTED_KEYSIZE:-2048}" _run_in_container rm -r /tmp/docker-mailserver/opendkim } @@ -226,7 +226,7 @@ function __should_have_expected_files() { # DKIM private key for signing, parse it to verify private key size is correct: _run_in_container_bash "openssl rsa -in '${TARGET_DIR}/mail.private' -noout -text" assert_success - assert_line --index 0 "RSA Private-Key: (${EXPECTED_KEYSIZE} bit, 2 primes)" + assert_line --index 0 "Private-Key: (${EXPECTED_KEYSIZE} bit, 2 primes)" # DKIM record, extract public key (base64 encoded, potentially multi-line) # - tail to exclude first line, @@ -240,7 +240,7 @@ function __should_have_expected_files() { ) | openssl enc -base64 -d | openssl pkey -inform DER -pubin -noout -text " assert_success - assert_line --index 0 "RSA Public-Key: (${EXPECTED_KEYSIZE} bit)" + assert_line --index 0 "Public-Key: (${EXPECTED_KEYSIZE} bit)" # Contents is for expected DKIM_DOMAIN and selector (mail): _run_in_container cat "${TARGET_DIR}/mail.txt" diff --git a/test/tests/serial/permit_docker.bats b/test/tests/serial/permit_docker.bats index 85f00484174..2ebf5e3e70d 100644 --- a/test/tests/serial/permit_docker.bats +++ b/test/tests/serial/permit_docker.bats @@ -13,7 +13,7 @@ setup_file() { PRIVATE_CONFIG=$(duplicate_config_for_container . mail_smtponly_second_network) docker create --name mail_smtponly_second_network \ -v "${PRIVATE_CONFIG}":/tmp/docker-mailserver \ - -v "$(pwd)/test/test-files":/tmp/docker-mailserver-test:ro \ + -v "$(pwd)/test/files":/tmp/docker-mailserver-test:ro \ -e SMTP_ONLY=1 \ -e PERMIT_DOCKER=connected-networks \ -e OVERRIDE_HOSTNAME=mail.my-domain.com \ @@ -26,7 +26,7 @@ setup_file() { PRIVATE_CONFIG=$(duplicate_config_for_container . mail_smtponly_second_network_sender) docker run -d --name mail_smtponly_second_network_sender \ -v "${PRIVATE_CONFIG}":/tmp/docker-mailserver \ - -v "$(pwd)/test/test-files":/tmp/docker-mailserver-test:ro \ + -v "$(pwd)/test/files":/tmp/docker-mailserver-test:ro \ -e SMTP_ONLY=1 \ -e PERMIT_DOCKER=connected-networks \ -e OVERRIDE_HOSTNAME=mail.my-domain.com \ @@ -39,7 +39,7 @@ setup_file() { # create another container that enforces authentication even on local connections docker run -d --name mail_smtponly_force_authentication \ -v "${PRIVATE_CONFIG}":/tmp/docker-mailserver \ - -v "$(pwd)/test/test-files":/tmp/docker-mailserver-test:ro \ + -v "$(pwd)/test/files":/tmp/docker-mailserver-test:ro \ -e SMTP_ONLY=1 \ -e PERMIT_DOCKER=none \ -e OVERRIDE_HOSTNAME=mail.my-domain.com \ @@ -68,7 +68,7 @@ teardown_file() { _reload_postfix mail_smtponly_second_network # we should be able to send from the other container on the second network! - run docker exec mail_smtponly_second_network_sender /bin/sh -c "nc mail_smtponly_second_network 25 < /tmp/docker-mailserver-test/email-templates/smtp-only.txt" + run docker exec mail_smtponly_second_network_sender /bin/sh -c "nc mail_smtponly_second_network 25 < /tmp/docker-mailserver-test/emails/nc_raw/smtp-only.txt" assert_output --partial "250 2.0.0 Ok: queued as " repeat_in_container_until_success_or_timeout 60 mail_smtponly_second_network /bin/sh -c 'grep -cE "to=.*status\=sent" /var/log/mail/mail.log' } @@ -80,7 +80,7 @@ teardown_file() { _reload_postfix mail_smtponly_force_authentication # the mailserver should require authentication and a protocol error should occur when using TLS - run docker exec mail_smtponly_force_authentication /bin/sh -c "nc localhost 25 < /tmp/docker-mailserver-test/email-templates/smtp-only.txt" + run docker exec mail_smtponly_force_authentication /bin/sh -c "nc localhost 25 < /tmp/docker-mailserver-test/emails/nc_raw/smtp-only.txt" assert_output --partial "550 5.5.1 Protocol error" [[ ${status} -ge 0 ]] } diff --git a/test/tests/serial/sedfile.bats b/test/tests/serial/sedfile.bats index bef2e6a8475..09cfe48299c 100644 --- a/test/tests/serial/sedfile.bats +++ b/test/tests/serial/sedfile.bats @@ -68,7 +68,7 @@ function setup() { assert_output 'foo bar' } -@test 'checking sedfile substitude failure (strict)' { +@test 'checking sedfile substitute failure (strict)' { # try to change 'baz' to 'something' and fail _run_in_container sedfile --strict -i 's|baz|something|' "${TEST_FILE}" assert_failure diff --git a/test/tests/serial/test_helper.bats b/test/tests/serial/test_helper.bats index 9b8f93abaa5..a3ffa6cfa0e 100644 --- a/test/tests/serial/test_helper.bats +++ b/test/tests/serial/test_helper.bats @@ -1,3 +1,5 @@ +# shellcheck disable=SC2314,SC2317 + load "${REPOSITORY_ROOT}/test/test_helper/common" BATS_TEST_NAME_PREFIX='test helper functions:' @@ -169,7 +171,7 @@ BATS_TEST_NAME_PREFIX='test helper functions:' # enable ClamAV to make message delivery slower, so we can detect it CONTAINER_NAME=$(docker run -d --rm \ -v "${PRIVATE_CONFIG}":/tmp/docker-mailserver \ - -v "$(pwd)/test/test-files":/tmp/docker-mailserver-test:ro \ + -v "$(pwd)/test/files":/tmp/docker-mailserver-test:ro \ -e ENABLE_CLAMAV=1 \ -h mail.my-domain.com \ -t "${NAME}") @@ -184,7 +186,7 @@ BATS_TEST_NAME_PREFIX='test helper functions:' [[ ${SECONDS} -lt 5 ]] # fill the queue with a message - docker exec "${CONTAINER_NAME}" /bin/sh -c "nc 0.0.0.0 25 < /tmp/docker-mailserver-test/email-templates/amavis-virus.txt" + docker exec "${CONTAINER_NAME}" /bin/sh -c "nc 0.0.0.0 25 < /tmp/docker-mailserver-test/emails/amavis-virus.txt" # that should still be stuck in the queue ! TEST_TIMEOUT_IN_SECONDS=0 wait_for_empty_mail_queue_in_container "${CONTAINER_NAME}" @@ -201,7 +203,7 @@ BATS_TEST_NAME_PREFIX='test helper functions:' # enable ClamAV to make message delivery slower, so we can detect it CONTAINER_NAME=$(docker run -d --rm \ -v "${PRIVATE_CONFIG}":/tmp/docker-mailserver \ - -v "$(pwd)/test/test-files":/tmp/docker-mailserver-test:ro \ + -v "$(pwd)/test/files":/tmp/docker-mailserver-test:ro \ -e ENABLE_CLAMAV=1 \ -h mail.my-domain.com \ -t "${NAME}") @@ -211,7 +213,7 @@ BATS_TEST_NAME_PREFIX='test helper functions:' wait_for_smtp_port_in_container "${CONTAINER_NAME}" || docker logs "${CONTAINER_NAME}" # fill the queue with a message - docker exec "${CONTAINER_NAME}" /bin/sh -c "nc 0.0.0.0 25 < /tmp/docker-mailserver-test/email-templates/amavis-virus.txt" + docker exec "${CONTAINER_NAME}" /bin/sh -c "nc 0.0.0.0 25 < /tmp/docker-mailserver-test/emails/amavis-virus.txt" # give it some time to clear the queue SECONDS=0 diff --git a/test/tests/serial/tests.bats b/test/tests/serial/tests.bats index f28787f8816..fee7830694d 100644 --- a/test/tests/serial/tests.bats +++ b/test/tests/serial/tests.bats @@ -17,7 +17,6 @@ function setup_file() { local CONTAINER_ARGS_ENV_CUSTOM=( --env ENABLE_AMAVIS=1 --env AMAVIS_LOGLEVEL=2 - --env ENABLE_QUOTAS=1 --env ENABLE_SRS=1 --env PERMIT_DOCKER=host --env PFLOGSUMM_TRIGGER=logrotate @@ -26,7 +25,7 @@ function setup_file() { --env SPOOF_PROTECTION=1 --env SSL_TYPE='snakeoil' --ulimit "nofile=$(ulimit -Sn):$(ulimit -Hn)" - --health-cmd "ss --listening --tcp | grep -P 'LISTEN.+:smtp' || exit 1" + --health-cmd "ss --listening --ipv4 --tcp | grep --silent ':smtp' || exit 1" ) _common_container_setup 'CONTAINER_ARGS_ENV_CUSTOM' @@ -81,11 +80,13 @@ function teardown_file() { _default_teardown ; } } @test "imap: authentication works" { - _send_email 'auth/imap-auth' '-w 1 0.0.0.0 143' + _nc_wrapper 'auth/imap-auth.txt' '-w 1 0.0.0.0 143' + assert_success } @test "imap: added user authentication works" { - _send_email 'auth/added-imap-auth' '-w 1 0.0.0.0 143' + _nc_wrapper 'auth/added-imap-auth.txt' '-w 1 0.0.0.0 143' + assert_success } # @@ -164,6 +165,7 @@ function teardown_file() { _default_teardown ; } } @test "amavis: old virusmail is wipped by cron" { + # shellcheck disable=SC2016 _exec_in_container_bash 'touch -d "`date --date=2000-01-01`" /var/lib/amavis/virusmails/should-be-deleted' _run_in_container_bash '/usr/local/bin/virus-wiper' assert_success @@ -172,6 +174,7 @@ function teardown_file() { _default_teardown ; } } @test "amavis: recent virusmail is not wipped by cron" { + # shellcheck disable=SC2016 _exec_in_container_bash 'touch -d "`date`" /var/lib/amavis/virusmails/should-not-be-deleted' _run_in_container_bash '/usr/local/bin/virus-wiper' assert_success @@ -179,23 +182,16 @@ function teardown_file() { _default_teardown ; } assert_success } -@test "system: /var/log/mail/mail.log is error free" { - _run_in_container grep 'non-null host address bits in' /var/log/mail/mail.log - assert_failure - _run_in_container grep 'mail system configuration error' /var/log/mail/mail.log - assert_failure - _run_in_container grep ': error:' /var/log/mail/mail.log - assert_failure - _run_in_container grep -i 'is not writable' /var/log/mail/mail.log - assert_failure - _run_in_container grep -i 'permission denied' /var/log/mail/mail.log - assert_failure - _run_in_container grep -i '(!)connect' /var/log/mail/mail.log - assert_failure - _run_in_container grep -i 'using backwards-compatible default setting' /var/log/mail/mail.log - assert_failure - _run_in_container grep -i 'connect to 127.0.0.1:10023: Connection refused' /var/log/mail/mail.log - assert_failure +# TODO: Remove in favor of a common helper method, as described in vmail-id.bats equivalent test-case +@test "system: Mail log is error free" { + _service_log_should_not_contain_string 'mail' 'non-null host address bits in' + _service_log_should_not_contain_string 'mail' 'mail system configuration error' + _service_log_should_not_contain_string 'mail' ': Error:' + _service_log_should_not_contain_string 'mail' 'is not writable' + _service_log_should_not_contain_string 'mail' 'Permission denied' + _service_log_should_not_contain_string 'mail' '(!)connect' + _service_log_should_not_contain_string 'mail' 'using backwards-compatible default setting' + _service_log_should_not_contain_string 'mail' 'connect to 127.0.0.1:10023: Connection refused' } @test "system: /var/log/auth.log is error free" { @@ -209,7 +205,8 @@ function teardown_file() { _default_teardown ; } } @test "system: amavis decoders installed and available" { - _run_in_container_bash "grep -E '.*(Internal decoder|Found decoder) for\s+\..*' /var/log/mail/mail.log*|grep -Eo '(mail|Z|gz|bz2|xz|lzma|lrz|lzo|lz4|rpm|cpio|tar|deb|rar|arj|arc|zoo|doc|cab|tnef|zip|kmz|7z|jar|swf|lha|iso|exe)' | sort | uniq" + _service_log_should_contain_string_regexp 'mail' '.*(Internal decoder|Found decoder) for\s+\..*' + run bash -c "grep -Eo '(mail|Z|gz|bz2|xz|lzma|lrz|lzo|lz4|rpm|cpio|tar|deb|rar|arj|arc|zoo|doc|cab|tnef|zip|kmz|7z|jar|swf|lha|iso|exe)' <<< '${output}' | sort | uniq" assert_success # Support for doc and zoo removed in buster cat <<'EOF' | assert_output @@ -242,198 +239,6 @@ zip EOF } -@test "quota: setquota user must be existing" { - _add_mail_account_then_wait_until_ready 'quota_user@domain.tld' - - _run_in_container_bash "setquota quota_user 50M" - assert_failure - _run_in_container_bash "setquota quota_user@domain.tld 50M" - assert_success - - _run_in_container_bash "setquota username@fulldomain 50M" - assert_failure - - _run_in_container_bash "delmailuser -y quota_user@domain.tld" - assert_success -} - -@test "quota: setquota must be well formatted" { - _add_mail_account_then_wait_until_ready 'quota_user@domain.tld' - - _run_in_container_bash "setquota quota_user@domain.tld 26GIGOTS" - assert_failure - _run_in_container_bash "setquota quota_user@domain.tld 123" - assert_failure - _run_in_container_bash "setquota quota_user@domain.tld M" - assert_failure - _run_in_container_bash "setquota quota_user@domain.tld -60M" - assert_failure - - - _run_in_container_bash "setquota quota_user@domain.tld 10B" - assert_success - _run_in_container_bash "setquota quota_user@domain.tld 10k" - assert_success - _run_in_container_bash "setquota quota_user@domain.tld 10M" - assert_success - _run_in_container_bash "setquota quota_user@domain.tld 10G" - assert_success - _run_in_container_bash "setquota quota_user@domain.tld 10T" - assert_success - - - _run_in_container_bash "delmailuser -y quota_user@domain.tld" - assert_success -} - -@test "quota: delquota user must be existing" { - _add_mail_account_then_wait_until_ready 'quota_user@domain.tld' - - _run_in_container_bash "delquota uota_user@domain.tld" - assert_failure - _run_in_container_bash "delquota quota_user" - assert_failure - _run_in_container_bash "delquota dontknowyou@domain.tld" - assert_failure - - _run_in_container_bash "setquota quota_user@domain.tld 10T" - assert_success - _run_in_container_bash "delquota quota_user@domain.tld" - assert_success - _run_in_container_bash "grep -i 'quota_user@domain.tld' /tmp/docker-mailserver/dovecot-quotas.cf" - assert_failure - - _run_in_container_bash "delmailuser -y quota_user@domain.tld" - assert_success -} - -@test "quota: delquota allow when no quota for existing user" { - _add_mail_account_then_wait_until_ready 'quota_user@domain.tld' - - _run_in_container_bash "grep -i 'quota_user@domain.tld' /tmp/docker-mailserver/dovecot-quotas.cf" - assert_failure - - _run_in_container_bash "delquota quota_user@domain.tld" - assert_success - _run_in_container_bash "delquota quota_user@domain.tld" - assert_success - - _run_in_container_bash "delmailuser -y quota_user@domain.tld" - assert_success -} - -@test "quota: dovecot quota present in postconf" { - _run_in_container_bash "postconf | grep 'check_policy_service inet:localhost:65265'" - assert_success -} - - -@test "quota: dovecot mailbox max size must be equal to postfix mailbox max size" { - postfix_mailbox_size=$(_exec_in_container_bash "postconf | grep -Po '(?<=mailbox_size_limit = )[0-9]+'") - run echo "${postfix_mailbox_size}" - refute_output "" - - # dovecot relies on virtual_mailbox_size by default - postfix_virtual_mailbox_size=$(_exec_in_container_bash "postconf | grep -Po '(?<=virtual_mailbox_limit = )[0-9]+'") - assert_equal "${postfix_virtual_mailbox_size}" "${postfix_mailbox_size}" - - postfix_mailbox_size_mb=$(( postfix_mailbox_size / 1000000)) - - dovecot_mailbox_size_mb=$(_exec_in_container_bash "doveconf | grep -oP '(?<=quota_rule \= \*\:storage=)[0-9]+'") - run echo "${dovecot_mailbox_size_mb}" - refute_output "" - - assert_equal "${postfix_mailbox_size_mb}" "${dovecot_mailbox_size_mb}" -} - - -@test "quota: dovecot message max size must be equal to postfix messsage max size" { - postfix_message_size=$(_exec_in_container_bash "postconf | grep -Po '(?<=message_size_limit = )[0-9]+'") - run echo "${postfix_message_size}" - refute_output "" - - postfix_message_size_mb=$(( postfix_message_size / 1000000)) - - dovecot_message_size_mb=$(_exec_in_container_bash "doveconf | grep -oP '(?<=quota_max_mail_size = )[0-9]+'") - run echo "${dovecot_message_size_mb}" - refute_output "" - - assert_equal "${postfix_message_size_mb}" "${dovecot_message_size_mb}" -} - -@test "quota: quota directive is removed when mailbox is removed" { - _add_mail_account_then_wait_until_ready 'quserremoved@domain.tld' - - _run_in_container_bash "setquota quserremoved@domain.tld 12M" - assert_success - - _run_in_container_bash 'cat /tmp/docker-mailserver/dovecot-quotas.cf | grep -E "^quserremoved@domain.tld\:12M\$" | wc -l | grep 1' - assert_success - - _run_in_container_bash "delmailuser -y quserremoved@domain.tld" - assert_success - - _run_in_container_bash 'cat /tmp/docker-mailserver/dovecot-quotas.cf | grep -E "^quserremoved@domain.tld\:12M\$"' - assert_failure -} - -@test "quota: dovecot applies user quota" { - _run_in_container_bash "doveadm quota get -u 'user1@localhost.localdomain' | grep 'User quota STORAGE'" - assert_output --partial "- 0" - - _run_in_container_bash "setquota user1@localhost.localdomain 50M" - assert_success - - # wait until quota has been updated - run _repeat_until_success_or_timeout 20 _exec_in_container_bash 'doveadm quota get -u user1@localhost.localdomain | grep -oP "(User quota STORAGE\s+[0-9]+\s+)51200(.*)"' - assert_success - - _run_in_container_bash "delquota user1@localhost.localdomain" - assert_success - - # wait until quota has been updated - run _repeat_until_success_or_timeout 20 _exec_in_container_bash 'doveadm quota get -u user1@localhost.localdomain | grep -oP "(User quota STORAGE\s+[0-9]+\s+)-(.*)"' - assert_success -} - -@test "quota: warn message received when quota exceeded" { - skip 'disabled as it fails randomly: https://github.com/docker-mailserver/docker-mailserver/pull/2511' - - # create user - _add_mail_account_then_wait_until_ready 'quotauser@otherdomain.tld' - _run_in_container_bash 'setquota quotauser@otherdomain.tld 10k' - assert_success - - # wait until quota has been updated - run _repeat_until_success_or_timeout 20 _exec_in_container_bash 'doveadm quota get -u quotauser@otherdomain.tld | grep -oP \"(User quota STORAGE\s+[0-9]+\s+)10(.*)\"' - assert_success - - # dovecot and postfix has been restarted - _wait_for_service postfix - _wait_for_service dovecot - sleep 10 - - # send some big emails - _send_email 'email-templates/quota-exceeded' '0.0.0.0 25' - _send_email 'email-templates/quota-exceeded' '0.0.0.0 25' - _send_email 'email-templates/quota-exceeded' '0.0.0.0 25' - - # check for quota warn message existence - run _repeat_until_success_or_timeout 20 _exec_in_container_bash 'grep \"Subject: quota warning\" /var/mail/otherdomain.tld/quotauser/new/ -R' - assert_success - - run _repeat_until_success_or_timeout 20 sh -c "docker logs mail | grep 'Quota exceeded (mailbox for user is full)'" - assert_success - - # ensure only the first big message and the warn message are present (other messages are rejected: mailbox is full) - _run_in_container sh -c 'ls /var/mail/otherdomain.tld/quotauser/new/ | wc -l' - assert_success - assert_output "2" - - _run_in_container_bash "delmailuser -y quotauser@otherdomain.tld" - assert_success -} - # # PERMIT_DOCKER mynetworks # @@ -453,7 +258,7 @@ EOF # @test "amavis: config overrides" { - _run_in_container_bash "grep 'Test Verification' /etc/amavis/conf.d/50-user | wc -l" + _run_in_container_bash "grep -c 'Test Verification' /etc/amavis/conf.d/50-user" assert_success assert_output 1 } @@ -479,13 +284,34 @@ EOF @test "spoofing: rejects sender forging" { # rejection of spoofed sender _wait_for_smtp_port_in_container_to_respond - _run_in_container_bash "openssl s_client -quiet -connect 0.0.0.0:465 < /tmp/docker-mailserver-test/auth/added-smtp-auth-spoofed.txt" + + # An authenticated user cannot use an envelope sender (MAIL FROM) + # address they do not own according to `main.cf:smtpd_sender_login_maps` lookup + _send_email --expect-rejection \ + --port 465 -tlsc --auth PLAIN \ + --auth-user added@localhost.localdomain \ + --auth-password mypassword \ + --ehlo mail \ + --from user2@localhost.localdomain \ + --data 'auth/added-smtp-auth-spoofed.txt' assert_output --partial 'Sender address rejected: not owned by user' } @test "spoofing: accepts sending as alias" { - _run_in_container_bash "openssl s_client -quiet -connect 0.0.0.0:465 < /tmp/docker-mailserver-test/auth/added-smtp-auth-spoofed-alias.txt | grep 'End data with'" - assert_success + # An authenticated account should be able to send mail from an alias, + # Verifies `main.cf:smtpd_sender_login_maps` includes /etc/postfix/virtual + # The envelope sender address (MAIL FROM) is the lookup key + # to each table. Address is authorized when a result that maps to + # the DMS account is returned. + _send_email \ + --port 465 -tlsc --auth PLAIN \ + --auth-user user1@localhost.localdomain \ + --auth-password mypassword \ + --ehlo mail \ + --from alias1@localhost.localdomain \ + --data 'auth/added-smtp-auth-spoofed-alias.txt' + assert_success + assert_output --partial 'End data with' } # diff --git a/test/tests/serial/vmail-id.bats b/test/tests/serial/vmail-id.bats new file mode 100644 index 00000000000..a0fb85372d2 --- /dev/null +++ b/test/tests/serial/vmail-id.bats @@ -0,0 +1,67 @@ +load "${REPOSITORY_ROOT}/test/helper/common" +load "${REPOSITORY_ROOT}/test/helper/setup" + +BATS_TEST_NAME_PREFIX='[ENV] (DMS_VMAIL_UID + DMS_VMAIL_GID) ' +CONTAINER_NAME='dms-test_env-change-vmail-id' + +function setup_file() { + _init_with_defaults + + local CUSTOM_SETUP_ARGUMENTS=( + --env PERMIT_DOCKER=container + --env DMS_VMAIL_UID=9042 + --env DMS_VMAIL_GID=9042 + ) + + _common_container_setup 'CUSTOM_SETUP_ARGUMENTS' + _wait_for_smtp_port_in_container +} + +function teardown_file() { _default_teardown ; } + +@test 'should successfully deliver mail' { + _send_email --header 'Subject: Test Message existing-user1' + _wait_for_empty_mail_queue_in_container + + # Should be successfully sent (received) by Postfix: + _service_log_should_contain_string 'mail' 'to=' + assert_output --partial 'status=sent' + _should_output_number_of_lines 1 + + # Verify successful delivery via Dovecot to `/var/mail` account by searching for the subject: + _repeat_in_container_until_success_or_timeout 20 "${CONTAINER_NAME}" grep -R \ + 'Subject: Test Message existing-user1' \ + '/var/mail/localhost.localdomain/user1/new/' + assert_success + _should_output_number_of_lines 1 +} + +# TODO: Migrate to test/helper/common.bash +# This test case is shared with tests.bats, but provides context on errors + some minor edits +# TODO: Could improve in future with keywords from https://github.com/docker-mailserver/docker-mailserver/pull/3550#issuecomment-1738509088 +# Potentially via a helper that allows an optional fixed number of errors to be present if they were intentional +@test 'Mail log is error free' { + # Postfix: https://serverfault.com/questions/934703/postfix-451-4-3-0-temporary-lookup-failure + _service_log_should_not_contain_string 'mail' 'non-null host address bits in' + + # Postfix delivery failure: https://github.com/docker-mailserver/docker-mailserver/issues/230 + _service_log_should_not_contain_string 'mail' 'mail system configuration error' + + # Unknown error source: https://github.com/docker-mailserver/docker-mailserver/pull/85 + _service_log_should_not_contain_string 'mail' ': Error:' + + # Unknown error source: https://github.com/docker-mailserver/docker-mailserver/pull/320 + _service_log_should_not_contain_string 'mail' 'not writable' + _service_log_should_not_contain_string 'mail' 'Permission denied' + + # Amavis: https://forum.howtoforge.com/threads/postfix-smtp-error-caused-by-clamav-cant-connect-to-a-unix-socket-var-run-clamav-clamd-ctl.81002/ + _service_log_should_not_contain_string 'mail' '(!)connect' + + # Postfix: https://github.com/docker-mailserver/docker-mailserver/pull/2597 + # Log line match example: https://github.com/docker-mailserver/docker-mailserver/pull/2598#issuecomment-1141176633 + _service_log_should_not_contain_string 'mail' 'using backwards-compatible default setting' + + # Postgrey: https://github.com/docker-mailserver/docker-mailserver/pull/612#discussion_r117635774 + _service_log_should_not_contain_string 'mail' 'connect to 127.0.0.1:10023: Connection refused' +} +