mirror of
https://github.com/chatmail/relay.git
synced 2026-05-19 20:38:05 +00:00
Compare commits
3 Commits
hpk/fix-ns
...
three-tier
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20f7aafcff | ||
|
|
7f58c8b38a | ||
|
|
df3c460f38 |
4
.github/workflows/ci.yaml
vendored
4
.github/workflows/ci.yaml
vendored
@@ -9,8 +9,6 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches: [ "main" ]
|
branches: [ "main" ]
|
||||||
|
|
||||||
permissions: {}
|
|
||||||
|
|
||||||
# Newest push wins: Prevents multiple runs from clashing and wasting runner efforts
|
# Newest push wins: Prevents multiple runs from clashing and wasting runner efforts
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
@@ -27,7 +25,6 @@ jobs:
|
|||||||
# Otherwise `test_deployed_state` will be unhappy.
|
# Otherwise `test_deployed_state` will be unhappy.
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
persist-credentials: false
|
|
||||||
- name: download filtermail
|
- name: download filtermail
|
||||||
run: curl -L https://github.com/chatmail/filtermail/releases/download/v0.6.1/filtermail-x86_64 -o /usr/local/bin/filtermail && chmod +x /usr/local/bin/filtermail
|
run: curl -L https://github.com/chatmail/filtermail/releases/download/v0.6.1/filtermail-x86_64 -o /usr/local/bin/filtermail && chmod +x /usr/local/bin/filtermail
|
||||||
- name: run chatmaild tests
|
- name: run chatmaild tests
|
||||||
@@ -41,7 +38,6 @@ jobs:
|
|||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: initenv
|
- name: initenv
|
||||||
run: scripts/initenv.sh
|
run: scripts/initenv.sh
|
||||||
|
|||||||
37
.github/workflows/docker-dispatch.yaml
vendored
37
.github/workflows/docker-dispatch.yaml
vendored
@@ -1,37 +0,0 @@
|
|||||||
# Notify the docker repo to build and test a new image after relay CI passes.
|
|
||||||
#
|
|
||||||
# Sends a repository_dispatch event to chatmail/docker with the relay ref
|
|
||||||
# and short SHA, which triggers docker-ci.yaml to build, push to GHCR,
|
|
||||||
# and run integration tests via cmlxc.
|
|
||||||
|
|
||||||
name: Trigger Docker build
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions: {}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
dispatch:
|
|
||||||
name: Dispatch build to chatmail/docker
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: github.repository == 'chatmail/relay'
|
|
||||||
steps:
|
|
||||||
- name: Compute short SHA
|
|
||||||
id: sha
|
|
||||||
run: echo "short=$(echo '${{ github.sha }}' | cut -c1-7)" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
- name: Send repository_dispatch
|
|
||||||
uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.CHATMAIL_DOCKER_DISPATCH_TOKEN }}
|
|
||||||
repository: chatmail/docker
|
|
||||||
event-type: relay-updated
|
|
||||||
client-payload: >-
|
|
||||||
{
|
|
||||||
"relay_ref": "${{ github.ref_name }}",
|
|
||||||
"relay_sha": "${{ github.sha }}",
|
|
||||||
"relay_sha_short": "${{ steps.sha.outputs.short }}"
|
|
||||||
}
|
|
||||||
14
.github/workflows/docs-preview.yaml
vendored
14
.github/workflows/docs-preview.yaml
vendored
@@ -7,8 +7,6 @@ on:
|
|||||||
- 'scripts/build-docs.sh'
|
- 'scripts/build-docs.sh'
|
||||||
- '.github/workflows/docs-preview.yaml'
|
- '.github/workflows/docs-preview.yaml'
|
||||||
|
|
||||||
permissions: {}
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
scripts:
|
scripts:
|
||||||
name: build
|
name: build
|
||||||
@@ -18,8 +16,6 @@ jobs:
|
|||||||
url: https://staging.chatmail.at/doc/relay/${{ steps.prepare.outputs.prid }}
|
url: https://staging.chatmail.at/doc/relay/${{ steps.prepare.outputs.prid }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: initenv
|
- name: initenv
|
||||||
run: scripts/initenv.sh
|
run: scripts/initenv.sh
|
||||||
@@ -38,22 +34,18 @@ jobs:
|
|||||||
- name: Get Pullrequest ID
|
- name: Get Pullrequest ID
|
||||||
id: prepare
|
id: prepare
|
||||||
run: |
|
run: |
|
||||||
export PULLREQUEST_ID=$(echo "${GITHUB_REF}" | cut -d "/" -f3)
|
export PULLREQUEST_ID=$(echo "${{ github.ref }}" | cut -d "/" -f3)
|
||||||
echo "prid=$PULLREQUEST_ID" >> $GITHUB_OUTPUT
|
echo "prid=$PULLREQUEST_ID" >> $GITHUB_OUTPUT
|
||||||
if [ $(expr length "${{ secrets.USERNAME }}") -gt "1" ]; then echo "uploadtoserver=true" >> $GITHUB_OUTPUT; fi
|
if [ $(expr length "${{ secrets.USERNAME }}") -gt "1" ]; then echo "uploadtoserver=true" >> $GITHUB_OUTPUT; fi
|
||||||
- run: |
|
- run: |
|
||||||
echo "baseurl: /${STEPS_PREPARE_OUTPUTS_PRID}" >> _config.yml
|
echo "baseurl: /${{ steps.prepare.outputs.prid }}" >> _config.yml
|
||||||
env:
|
|
||||||
STEPS_PREPARE_OUTPUTS_PRID: ${{ steps.prepare.outputs.prid }}
|
|
||||||
|
|
||||||
- name: Upload preview
|
- name: Upload preview
|
||||||
run: |
|
run: |
|
||||||
mkdir -p "$HOME/.ssh"
|
mkdir -p "$HOME/.ssh"
|
||||||
echo "${{ secrets.CHATMAIL_STAGING_SSHKEY }}" > "$HOME/.ssh/key"
|
echo "${{ secrets.CHATMAIL_STAGING_SSHKEY }}" > "$HOME/.ssh/key"
|
||||||
chmod 600 "$HOME/.ssh/key"
|
chmod 600 "$HOME/.ssh/key"
|
||||||
rsync -rILvh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/doc/build/ "${{ secrets.USERNAME }}@chatmail.at:/var/www/html/staging.chatmail.at/doc/relay/${STEPS_PREPARE_OUTPUTS_PRID}/"
|
rsync -rILvh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/doc/build/ "${{ secrets.USERNAME }}@chatmail.at:/var/www/html/staging.chatmail.at/doc/relay/${{ steps.prepare.outputs.prid }}/"
|
||||||
env:
|
|
||||||
STEPS_PREPARE_OUTPUTS_PRID: ${{ steps.prepare.outputs.prid }}
|
|
||||||
|
|
||||||
- name: check links
|
- name: check links
|
||||||
working-directory: doc
|
working-directory: doc
|
||||||
|
|||||||
4
.github/workflows/docs.yaml
vendored
4
.github/workflows/docs.yaml
vendored
@@ -10,8 +10,6 @@ on:
|
|||||||
- 'scripts/build-docs.sh'
|
- 'scripts/build-docs.sh'
|
||||||
- '.github/workflows/docs.yaml'
|
- '.github/workflows/docs.yaml'
|
||||||
|
|
||||||
permissions: {}
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
scripts:
|
scripts:
|
||||||
name: build
|
name: build
|
||||||
@@ -21,8 +19,6 @@ jobs:
|
|||||||
url: https://chatmail.at/doc/relay/
|
url: https://chatmail.at/doc/relay/
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: initenv
|
- name: initenv
|
||||||
run: scripts/initenv.sh
|
run: scripts/initenv.sh
|
||||||
|
|||||||
26
.github/workflows/zizmor-scan.yml
vendored
26
.github/workflows/zizmor-scan.yml
vendored
@@ -1,26 +0,0 @@
|
|||||||
name: GitHub Actions Security Analysis with zizmor
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: ["main"]
|
|
||||||
pull_request:
|
|
||||||
branches: ["**"]
|
|
||||||
|
|
||||||
permissions: {}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
zizmor:
|
|
||||||
name: Run zizmor
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
security-events: write # Required for upload-sarif (used by zizmor-action) to upload SARIF files.
|
|
||||||
contents: read
|
|
||||||
actions: read
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Run zizmor
|
|
||||||
uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3
|
|
||||||
7
.github/zizmor.yml
vendored
7
.github/zizmor.yml
vendored
@@ -1,7 +0,0 @@
|
|||||||
rules:
|
|
||||||
unpinned-uses:
|
|
||||||
config:
|
|
||||||
policies:
|
|
||||||
actions/*: ref-pin
|
|
||||||
dependabot/*: ref-pin
|
|
||||||
chatmail/*: ref-pin
|
|
||||||
84
CHANGELOG.md
84
CHANGELOG.md
@@ -1,89 +1,5 @@
|
|||||||
# Changelog for chatmail deployment
|
# Changelog for chatmail deployment
|
||||||
|
|
||||||
## 1.10.0 2026-04-30
|
|
||||||
|
|
||||||
* start mtail after networking is fully up <https://github.com/chatmail/relay/pull/942>
|
|
||||||
* support specifying custom filtermail binary through environment variable <https://github.com/chatmail/relay/pull/941>
|
|
||||||
* add automated zizmor scanning of github workflows <https://github.com/chatmail/relay/pull/938>
|
|
||||||
* added dispatch for *automated builds of chatmail relay docker images* <https://github.com/chatmail/relay/pull/934>
|
|
||||||
* do not bind SMTP client sockets to public addresses <https://github.com/chatmail/relay/pull/932>
|
|
||||||
* underline in docs that scripts/initenv.sh should be used for building the docs <https://github.com/chatmail/relay/pull/933>
|
|
||||||
* automatic oldest-first message removal from mailboxes to always stay under max_mailbox_size <https://github.com/chatmail/relay/pull/929>
|
|
||||||
* remove --slow from cmdeploy test <https://github.com/chatmail/relay/pull/931>
|
|
||||||
* handle missing inotify sysctl keys in containers <https://github.com/chatmail/relay/pull/930>
|
|
||||||
* replace resolvconf with static resolv.conf <https://github.com/chatmail/relay/pull/928>
|
|
||||||
* disable fsync for LMTP and IMAP services <https://github.com/chatmail/relay/pull/925>
|
|
||||||
* re-use cmlxc workflow, replacing CI with hetzner staging servers with local lxc containers <https://github.com/chatmail/relay/pull/917>
|
|
||||||
* explicitly install resolvconf <https://github.com/chatmail/relay/pull/924>
|
|
||||||
* detect stale dovecot binary and force restart in activate() <https://github.com/chatmail/relay/pull/922>
|
|
||||||
* Rename filtermail_http_port to filtermail_http_port_incoming <https://github.com/chatmail/relay/pull/921>
|
|
||||||
* consolidated is_in_container() check https://github.com/chatmail/relay/pull/920>
|
|
||||||
* restart dovecot after package replacement (rebase, test condense) <https://github.com/chatmail/relay/pull/913>
|
|
||||||
* Set permissions on dovecot pin prefs <https://github.com/chatmail/relay/pull/915>
|
|
||||||
* Route `/mxdeliv/` to configurable port <https://github.com/chatmail/relay/pull/901>
|
|
||||||
* fix VM detection, automated testing fixes, use newer chatmail-turn and move to standard BIND DNS zone format <https://github.com/chatmail/relay/pull/912>
|
|
||||||
* Upgrade to filtermail 0.6.1 <https://github.com/chatmail/relay/pull/910>
|
|
||||||
* pin dovecot packages to prevent apt upgrades <https://github.com/chatmail/relay/pull/908>
|
|
||||||
* add rpc server to cmdeploy along with client <https://github.com/chatmail/relay/pull/906>
|
|
||||||
* remove unused deps from chatmaild <https://github.com/chatmail/relay/pull/905>
|
|
||||||
* set default smtp_tls_security_level to "verify" unconditionally <https://github.com/chatmail/relay/pull/902>
|
|
||||||
* featprefer IPv4 in SMTP client <https://github.com/chatmail/relay/pull/900>
|
|
||||||
* Install dovecot .deb packages atomically <https://github.com/chatmail/relay/pull/899>
|
|
||||||
* stop installing cron package <https://github.com/chatmail/relay/pull/898>
|
|
||||||
* Rewrite dovecot install logic, update <https://github.com/chatmail/relay/pull/862>
|
|
||||||
* fix a test and some linting fixes <https://github.com/chatmail/relay/pull/897>
|
|
||||||
* Disable IP verification on domain-literal addresses <https://github.com/chatmail/relay/pull/895>
|
|
||||||
* disable installing recommended packages globally on the relay <https://github.com/chatmail/relay/pull/887>
|
|
||||||
* multiple bug fixes across chatmaild and cmdeploy <https://github.com/chatmail/relay/pull/883>
|
|
||||||
* remove /metrics from the website <https://github.com/chatmail/relay/pull/703>
|
|
||||||
* add Prometheus textfile output to fsreport <https://github.com/chatmail/relay/pull/881>
|
|
||||||
* chown opendkim: private key <https://github.com/chatmail/relay/pull/879>
|
|
||||||
* make sure chatmail-metadata was started <https://github.com/chatmail/relay/pull/882>
|
|
||||||
* dovecot update url <https://github.com/chatmail/relay/pull/880>
|
|
||||||
* upgrade to filtermail v0.5.2 <https://github.com/chatmail/relay/pull/876>
|
|
||||||
* download dovecot packages from github release <https://github.com/chatmail/relay/pull/875>
|
|
||||||
* replace DKIM verification with filtermail v0.5 <https://github.com/chatmail/relay/pull/831>
|
|
||||||
* remove CFFI deltachat bindings usage, and consolidate test support with rpc-bindings <https://github.com/chatmail/relay/pull/872>
|
|
||||||
* prepare chatmaild/cmdeploy changes for Docker support <https://github.com/chatmail/relay/pull/857>
|
|
||||||
* stabilize online benchmark timing adding rate-limit-aware cooldown between iterations <https://github.com/chatmail/relay/pull/867>
|
|
||||||
* move rate-limit cooldown to benchmark fixture <https://github.com/chatmail/relay/pull/868>
|
|
||||||
* reconfigure acmetool from redirector to proxy mode <https://github.com/chatmail/relay/pull/861>
|
|
||||||
* make tests work with `--ssh-host localhost` <https://github.com/chatmail/relay/pull/856>
|
|
||||||
* mark f-string with f prefix in test_expunged <https://github.com/chatmail/relay/pull/863>
|
|
||||||
* install also if dovecot.service=False in SystemdEnabled Fact <https://github.com/chatmail/relay/pull/841>
|
|
||||||
* Introduce support for self-signed chatmail relays <https://github.com/chatmail/relay/pull/855>
|
|
||||||
* Strip Received headers before delivery <https://github.com/chatmail/relay/pull/849>
|
|
||||||
* upgrade to filtermail v0.3 <https://github.com/chatmail/relay/pull/850>
|
|
||||||
* fix link to Maddy and update madmail URL <https://github.com/chatmail/relay/pull/847>
|
|
||||||
* accept self-signed certificates for IP-only relays <https://github.com/chatmail/relay/pull/846>
|
|
||||||
* enforce sending from public IP addresses <https://github.com/chatmail/relay/pull/845>
|
|
||||||
* port check: check addresses, fix single services <https://github.com/chatmail/relay/pull/844>
|
|
||||||
* remediates issue with improper concat on resolver injection <https://github.com/chatmail/relay/pull/834>
|
|
||||||
* ipv6 boolean not being respected during operations <https://github.com/chatmail/relay/pull/832>
|
|
||||||
* upgrade to filtermail v0.2 by <https://github.com/chatmail/relay/pull/825>
|
|
||||||
* fix link to filtermail <https://github.com/chatmail/relay/pull/824>
|
|
||||||
* print timestamps when sending messages <https://github.com/chatmail/relay/pull/823>
|
|
||||||
* fix flaky test_exceed_rate_limit <https://github.com/chatmail/relay/pull/822>
|
|
||||||
* Replace filtermail with rust reimplementation <https://github.com/chatmail/relay/pull/808>
|
|
||||||
* Set default internal SMTP ports in Config <https://github.com/chatmail/relay/pull/819>
|
|
||||||
* separate metrics for incoming and outgoing messages <https://github.com/chatmail/relay/pull/820>
|
|
||||||
* disable appending the Received header <https://github.com/chatmail/relay/pull/815>
|
|
||||||
* fail on errors in postfix/dovecot config <https://github.com/chatmail/relay/pull/813>
|
|
||||||
* tweak idle/hibernate metrics some more <https://github.com/chatmail/relay/pull/811>
|
|
||||||
* add config flag to export statistics <https://github.com/chatmail/relay/pull/806>
|
|
||||||
* add --website-only option to run subcommand <https://github.com/chatmail/relay/pull/768>
|
|
||||||
* Strip DKIM-Signature header before LMTP <https://github.com/chatmail/relay/pull/803>
|
|
||||||
* properly make sure that postfix gets restarted on failure <https://github.com/chatmail/relay/pull/802>
|
|
||||||
* expire.py: use absolute path to maildirsize <https://github.com/chatmail/relay/pull/807>
|
|
||||||
* pin Dovecot documentation URLs to version 2.3 <https://github.com/chatmail/relay/pull/800>
|
|
||||||
* try to use "build machine" and "deployment server" consistently <https://github.com/chatmail/relay/pull/797>
|
|
||||||
* adds instructions for migrating control machines <https://github.com/chatmail/relay/pull/795>
|
|
||||||
* use consistent naming schema in getting started <https://github.com/chatmail/relay/pull/793>
|
|
||||||
* remove jsok/serialize-workflow-action dependency <https://github.com/chatmail/relay/pull/790>
|
|
||||||
* streamline migration guide wording, provide titled steps <https://github.com/chatmail/relay/pull/789>
|
|
||||||
* increases default max mailbox size <https://github.com/chatmail/relay/pull/792>
|
|
||||||
* use daemon_name for OpenDKIM sign-verify decision instead of IP <https://github.com/chatmail/relay/pull/784>
|
|
||||||
|
|
||||||
## 1.9.0 2025-12-18
|
## 1.9.0 2025-12-18
|
||||||
|
|
||||||
### Documentation
|
### Documentation
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import iniconfig
|
import iniconfig
|
||||||
@@ -46,6 +47,8 @@ class Config:
|
|||||||
)
|
)
|
||||||
self.mtail_address = params.get("mtail_address")
|
self.mtail_address = params.get("mtail_address")
|
||||||
self.disable_ipv6 = params.get("disable_ipv6", "false").lower() == "true"
|
self.disable_ipv6 = params.get("disable_ipv6", "false").lower() == "true"
|
||||||
|
self.addr_v4 = os.environ.get("CHATMAIL_ADDR_V4", "")
|
||||||
|
self.addr_v6 = os.environ.get("CHATMAIL_ADDR_V6", "")
|
||||||
self.acme_email = params.get("acme_email", "")
|
self.acme_email = params.get("acme_email", "")
|
||||||
self.imap_rawlog = params.get("imap_rawlog", "false").lower() == "true"
|
self.imap_rawlog = params.get("imap_rawlog", "false").lower() == "true"
|
||||||
self.imap_compress = params.get("imap_compress", "false").lower() == "true"
|
self.imap_compress = params.get("imap_compress", "false").lower() == "true"
|
||||||
|
|||||||
@@ -99,19 +99,31 @@ def scan_mailbox_messages(mbox):
|
|||||||
return messages
|
return messages
|
||||||
|
|
||||||
|
|
||||||
|
# Within this window, large messages are deleted before small ones.
|
||||||
|
DELETE_LARGE_FIRST_DAYS = 7
|
||||||
|
|
||||||
|
|
||||||
def expire_to_target(mbox, target_bytes):
|
def expire_to_target(mbox, target_bytes):
|
||||||
|
cutoff = time.time() - DELETE_LARGE_FIRST_DAYS * 86400
|
||||||
messages = scan_mailbox_messages(mbox)
|
messages = scan_mailbox_messages(mbox)
|
||||||
|
|
||||||
|
def sort_key(msg):
|
||||||
|
# prio 0: Older than cutoff -> remove oldest first
|
||||||
|
if msg.mtime < cutoff:
|
||||||
|
return (0, msg.mtime)
|
||||||
|
|
||||||
|
# prio 1: more recent than cutoff, large -> remove largest first
|
||||||
|
if msg.quota_size > 200000:
|
||||||
|
return (1, -msg.quota_size, msg.mtime)
|
||||||
|
|
||||||
|
# prio 2: more recent than cutoff, small -> remove oldest first
|
||||||
|
return (2, msg.mtime)
|
||||||
|
|
||||||
total_size = sum(m.quota_size for m in messages)
|
total_size = sum(m.quota_size for m in messages)
|
||||||
# Keep recent 24 hours of messages protected from expiry because
|
|
||||||
# likely something is wrong with interactions on that address
|
|
||||||
# and quota-full signal can help the address owner's device to notice it
|
|
||||||
undeletable_messages_cutoff = time.time() - (3600 * 24)
|
|
||||||
removed = 0
|
removed = 0
|
||||||
for entry in sorted(messages):
|
for entry in sorted(messages, key=sort_key):
|
||||||
if total_size <= target_bytes:
|
if total_size <= target_bytes:
|
||||||
break
|
break
|
||||||
if entry.mtime > undeletable_messages_cutoff:
|
|
||||||
break
|
|
||||||
(mbox / entry.path).unlink(missing_ok=True)
|
(mbox / entry.path).unlink(missing_ok=True)
|
||||||
total_size -= entry.quota_size
|
total_size -= entry.quota_size
|
||||||
removed += 1
|
removed += 1
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import itertools
|
import itertools
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
|
import shutil
|
||||||
import time
|
import time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from fnmatch import fnmatch
|
from fnmatch import fnmatch
|
||||||
@@ -233,15 +234,38 @@ def test_parse_dovecot_filename():
|
|||||||
def test_expire_to_target(tmp_path):
|
def test_expire_to_target(tmp_path):
|
||||||
_create_message(tmp_path, "cur", MB, days_old=10, disk_size=100)
|
_create_message(tmp_path, "cur", MB, days_old=10, disk_size=100)
|
||||||
_create_message(tmp_path, "new", MB, days_old=5)
|
_create_message(tmp_path, "new", MB, days_old=5)
|
||||||
_create_message(tmp_path, "cur", MB, days_old=0) # undeletable (<1 hour)
|
assert len(scan_mailbox_messages(tmp_path)) == 2
|
||||||
assert len(scan_mailbox_messages(tmp_path)) == 3
|
|
||||||
# removes oldest first, uses S= size not disk size
|
# removes oldest first, uses S= size not disk size
|
||||||
removed = expire_to_target(tmp_path, MB)
|
removed = expire_to_target(tmp_path, MB)
|
||||||
assert removed == 2
|
assert removed == 1
|
||||||
|
assert len(scan_mailbox_messages(tmp_path)) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_expire_to_target_prioritization(tmp_path):
|
||||||
|
def create_messages():
|
||||||
|
for sub in ("cur", "new"):
|
||||||
|
if (tmp_path / sub).exists():
|
||||||
|
shutil.rmtree(tmp_path / sub)
|
||||||
|
# prio 0: older than 7 days
|
||||||
|
_create_message(tmp_path, "cur", 5 * MB, days_old=10)
|
||||||
|
# prio 1: last 7 days, large (>200KB)
|
||||||
|
_create_message(tmp_path, "cur", 5 * MB, days_old=1)
|
||||||
|
# prio 2: last 7 days, small
|
||||||
|
_create_message(tmp_path, "cur", 1000, days_old=2)
|
||||||
|
|
||||||
|
# Shrink to 6MB: only the old message (prio 0) is removed.
|
||||||
|
create_messages()
|
||||||
|
assert expire_to_target(tmp_path, 6 * MB) == 1
|
||||||
|
msgs = scan_mailbox_messages(tmp_path)
|
||||||
|
assert len(msgs) == 2
|
||||||
|
assert all(m.mtime > time.time() - 7 * 86400 for m in msgs)
|
||||||
|
|
||||||
|
# Shrink to 1KB: old and recent-large removed, small survives.
|
||||||
|
create_messages()
|
||||||
|
assert expire_to_target(tmp_path, 1024) == 2
|
||||||
msgs = scan_mailbox_messages(tmp_path)
|
msgs = scan_mailbox_messages(tmp_path)
|
||||||
assert len(msgs) == 1
|
assert len(msgs) == 1
|
||||||
# the surviving message is the fresh undeletable one
|
assert msgs[0].quota_size == 1000
|
||||||
assert msgs[0].mtime > time.time() - 3600
|
|
||||||
|
|
||||||
|
|
||||||
def test_quota_expire_main(tmp_path, capsys):
|
def test_quota_expire_main(tmp_path, capsys):
|
||||||
|
|||||||
@@ -101,6 +101,9 @@ def run_cmd(args, out):
|
|||||||
env["CHATMAIL_WEBSITE_ONLY"] = "True" if args.website_only else ""
|
env["CHATMAIL_WEBSITE_ONLY"] = "True" if args.website_only else ""
|
||||||
env["CHATMAIL_DISABLE_MAIL"] = "True" if args.disable_mail else ""
|
env["CHATMAIL_DISABLE_MAIL"] = "True" if args.disable_mail else ""
|
||||||
env["CHATMAIL_REQUIRE_IROH"] = "True" if require_iroh else ""
|
env["CHATMAIL_REQUIRE_IROH"] = "True" if require_iroh else ""
|
||||||
|
if not args.dns_check_disabled:
|
||||||
|
env["CHATMAIL_ADDR_V4"] = remote_data.get("A") or ""
|
||||||
|
env["CHATMAIL_ADDR_V6"] = remote_data.get("AAAA") or ""
|
||||||
deploy_path = importlib.resources.files(__package__).joinpath("run.py").resolve()
|
deploy_path = importlib.resources.files(__package__).joinpath("run.py").resolve()
|
||||||
pyinf = "pyinfra --dry" if args.dry_run else "pyinfra"
|
pyinf = "pyinfra --dry" if args.dry_run else "pyinfra"
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import os
|
|
||||||
|
|
||||||
from pyinfra import facts, host
|
from pyinfra import facts, host
|
||||||
from pyinfra.operations import files, systemd
|
from pyinfra.operations import files, systemd
|
||||||
|
|
||||||
@@ -15,16 +13,6 @@ class FiltermailDeployer(Deployer):
|
|||||||
self.need_restart = False
|
self.need_restart = False
|
||||||
|
|
||||||
def install(self):
|
def install(self):
|
||||||
local_bin = os.environ.get("CHATMAIL_FILTERMAIL_BINARY")
|
|
||||||
if local_bin:
|
|
||||||
self.need_restart |= files.put(
|
|
||||||
name="Upload locally built filtermail",
|
|
||||||
src=local_bin,
|
|
||||||
dest=self.bin_path,
|
|
||||||
mode="755",
|
|
||||||
).changed
|
|
||||||
return
|
|
||||||
|
|
||||||
arch = host.get_fact(facts.server.Arch)
|
arch = host.get_fact(facts.server.Arch)
|
||||||
url = f"https://github.com/chatmail/filtermail/releases/download/v0.6.1/filtermail-{arch}"
|
url = f"https://github.com/chatmail/filtermail/releases/download/v0.6.1/filtermail-{arch}"
|
||||||
sha256sum = {
|
sha256sum = {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=mtail
|
Description=mtail
|
||||||
After=multi-user.target
|
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
|
|||||||
@@ -69,6 +69,15 @@ mynetworks = 127.0.0.0/8
|
|||||||
{% else %}
|
{% else %}
|
||||||
mynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128
|
mynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if config.addr_v4 %}
|
||||||
|
smtp_bind_address = {{ config.addr_v4 }}
|
||||||
|
{% endif %}
|
||||||
|
{% if config.addr_v6 %}
|
||||||
|
smtp_bind_address6 = {{ config.addr_v6 }}
|
||||||
|
{% endif %}
|
||||||
|
{% if config.addr_v4 or config.addr_v6 %}
|
||||||
|
smtp_bind_address_enforce = yes
|
||||||
|
{% endif %}
|
||||||
mailbox_size_limit = 0
|
mailbox_size_limit = 0
|
||||||
message_size_limit = {{config.max_message_size}}
|
message_size_limit = {{config.max_message_size}}
|
||||||
recipient_delimiter = +
|
recipient_delimiter = +
|
||||||
|
|||||||
@@ -64,13 +64,11 @@ def get_dkim_entry(mail_domain, pre_command, dkim_selector):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def query_dns(typ, domain, shell_exec=None):
|
def query_dns(typ, domain):
|
||||||
if shell_exec is None:
|
|
||||||
shell_exec = shell
|
|
||||||
# Get autoritative nameserver from the SOA record.
|
# Get autoritative nameserver from the SOA record.
|
||||||
soa_answers = [
|
soa_answers = [
|
||||||
x.split()
|
x.split()
|
||||||
for x in shell_exec(
|
for x in shell(
|
||||||
f"dig -r -q {domain} -t SOA +noall +authority +answer", print=log_progress
|
f"dig -r -q {domain} -t SOA +noall +authority +answer", print=log_progress
|
||||||
).split("\n")
|
).split("\n")
|
||||||
]
|
]
|
||||||
@@ -80,27 +78,8 @@ def query_dns(typ, domain, shell_exec=None):
|
|||||||
ns = soa[0][4]
|
ns = soa[0][4]
|
||||||
|
|
||||||
# Query authoritative nameserver directly to bypass DNS cache.
|
# Query authoritative nameserver directly to bypass DNS cache.
|
||||||
return _dig_authoritative(ns, domain, typ, shell_exec=shell_exec)
|
res = shell(f"dig @{ns} -r -q {domain} -t {typ} +short", print=log_progress)
|
||||||
|
return next((line for line in res.split("\n") if not line.startswith(";")), "")
|
||||||
|
|
||||||
def _parse_dig_result(output):
|
|
||||||
"""Return first non-comment, non-empty line from dig output, or empty string."""
|
|
||||||
lines = [line for line in output.split("\n") if not line.startswith(";")]
|
|
||||||
return next((line for line in lines if line.strip()), "")
|
|
||||||
|
|
||||||
|
|
||||||
def _dig_authoritative(ns, domain, typ, shell_exec=None):
|
|
||||||
"""Query authoritative NS, falling back to IPv4-only if default fails."""
|
|
||||||
if shell_exec is None:
|
|
||||||
shell_exec = shell
|
|
||||||
|
|
||||||
# limit timeout and tries to not hang on a broken default NS
|
|
||||||
cmd = f"dig @{ns} -r -q {domain} -t {typ} +short +timeout=10 +tries=2"
|
|
||||||
result = _parse_dig_result(shell_exec(cmd, print=log_progress))
|
|
||||||
if result:
|
|
||||||
return result
|
|
||||||
# Fallback: force IPv4 transport (handles broken IPv6 to NS)
|
|
||||||
return _parse_dig_result(shell_exec(cmd + " -4", print=log_progress))
|
|
||||||
|
|
||||||
|
|
||||||
def check_zonefile(zonefile, verbose=True):
|
def check_zonefile(zonefile, verbose=True):
|
||||||
|
|||||||
@@ -214,59 +214,3 @@ class TestZonefileChecks:
|
|||||||
assert not mockout.captured_red
|
assert not mockout.captured_red
|
||||||
assert "correct" in mockout.captured_green[0]
|
assert "correct" in mockout.captured_green[0]
|
||||||
assert not mockout.captured_red
|
assert not mockout.captured_red
|
||||||
|
|
||||||
|
|
||||||
class TestDigAuthoritative:
|
|
||||||
@pytest.fixture
|
|
||||||
def dig_auth(self):
|
|
||||||
"""Helper that calls _dig_authoritative with a recording shell function."""
|
|
||||||
calls = []
|
|
||||||
|
|
||||||
def run(first_result, ipv4_result=None):
|
|
||||||
def shell(cmd, print=print):
|
|
||||||
calls.append(cmd)
|
|
||||||
if "-4" in cmd:
|
|
||||||
return ipv4_result or ""
|
|
||||||
return first_result
|
|
||||||
|
|
||||||
result = remote.rdns._dig_authoritative(
|
|
||||||
"ns1.example.", "example.com", "A", shell_exec=shell
|
|
||||||
)
|
|
||||||
return result, calls
|
|
||||||
|
|
||||||
return run
|
|
||||||
|
|
||||||
def test_ipv4_fallback_on_error(self, dig_auth):
|
|
||||||
"""Fallback to -4 when first query returns only error lines."""
|
|
||||||
result, calls = dig_auth(
|
|
||||||
first_result=(
|
|
||||||
";; communications error to 2a01:4f8:10a:1044::80#53: timed out\n"
|
|
||||||
";; communications error to 2a01:4f8:10a:1044::80#53: timed out\n"
|
|
||||||
),
|
|
||||||
ipv4_result="1.2.3.4",
|
|
||||||
)
|
|
||||||
assert len(calls) == 2
|
|
||||||
assert "-4" not in calls[0]
|
|
||||||
assert "-4" in calls[1]
|
|
||||||
assert result == "1.2.3.4"
|
|
||||||
|
|
||||||
def test_no_fallback_on_success(self, dig_auth):
|
|
||||||
"""No -4 fallback when first query returns a valid answer."""
|
|
||||||
result, calls = dig_auth(first_result="1.2.3.4")
|
|
||||||
assert result == "1.2.3.4"
|
|
||||||
assert len(calls) == 1
|
|
||||||
|
|
||||||
def test_fallback_with_mixed_error_lines(self, dig_auth):
|
|
||||||
"""Fallback triggers when output has only error lines mixed with empty."""
|
|
||||||
result, calls = dig_auth(
|
|
||||||
first_result=(
|
|
||||||
";; communications error to 2a01:4f8::80#53: timed out\n"
|
|
||||||
"\n"
|
|
||||||
";; communications error to 2a01:4f8::80#53: timed out\n"
|
|
||||||
),
|
|
||||||
ipv4_result=";; some warning\n1.2.3.4",
|
|
||||||
)
|
|
||||||
assert len(calls) == 2
|
|
||||||
assert result == "1.2.3.4"
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,14 +4,12 @@
|
|||||||
|
|
||||||
You can use the `make` command and `make html` to build web pages.
|
You can use the `make` command and `make html` to build web pages.
|
||||||
|
|
||||||
You need a Python environment with `sphinx` and other
|
You need a Python environment where the following install was excuted:
|
||||||
dependencies, you can create it by running `scripts/initenv.sh`
|
|
||||||
from the repository root.
|
pip install furo sphinx-autobuild
|
||||||
|
|
||||||
To develop/change documentation, you can then do:
|
To develop/change documentation, you can then do:
|
||||||
|
|
||||||
. venv/bin/activate
|
|
||||||
cd doc
|
|
||||||
make auto
|
make auto
|
||||||
|
|
||||||
A page will open at https://127.0.0.1:8000/ serving the docs and it will
|
A page will open at https://127.0.0.1:8000/ serving the docs and it will
|
||||||
|
|||||||
@@ -16,6 +16,5 @@ Contributions and feedback welcome through the https://github.com/chatmail/relay
|
|||||||
proxy
|
proxy
|
||||||
migrate
|
migrate
|
||||||
overview
|
overview
|
||||||
reverse_dns
|
|
||||||
related
|
related
|
||||||
faq
|
faq
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
Configuring reverse DNS
|
|
||||||
=======================
|
|
||||||
|
|
||||||
Some email servers reject the emails
|
|
||||||
if they don't pass `FCrDNS`_ check, also known as `iprev`_ check.
|
|
||||||
|
|
||||||
.. _FCrDNS: https://en.wikipedia.org/wiki/Forward-confirmed_reverse_DNS
|
|
||||||
.. _iprev: https://datatracker.ietf.org/doc/html/rfc8601#section-3
|
|
||||||
|
|
||||||
Passing the check requires that the IP address that email is sent from
|
|
||||||
should have a ``PTR`` record pointing to the domain name of the server,
|
|
||||||
and domain name record should have an ``A/AAAA`` record
|
|
||||||
pointing to the IP address.
|
|
||||||
|
|
||||||
Modern email relies on DKIM and SPF for authentication,
|
|
||||||
while iprev check exists for
|
|
||||||
`historical reasons <https://datatracker.ietf.org/doc/html/draft-ietf-dnsop-reverse-mapping-considerations-06#section-2.1>`_.
|
|
||||||
Chatmail relays don't resolve ``PTR`` records,
|
|
||||||
so you can ignore this section if configuring ``PTR`` records
|
|
||||||
is difficult and federation with legacy email servers that don't accept
|
|
||||||
valid DKIM signature for authentication is not important.
|
|
||||||
|
|
||||||
Multi-homed setups
|
|
||||||
------------------
|
|
||||||
|
|
||||||
If you have a server with multiple IP addresses,
|
|
||||||
also known as multi-homed setup,
|
|
||||||
and don't publish all IP addresses in DNS,
|
|
||||||
you need to make sure you are using
|
|
||||||
the published address when making outgoing connections.
|
|
||||||
|
|
||||||
For example, your server may have a static IP
|
|
||||||
address, and a so-called Floating IP or Virtual IP
|
|
||||||
that can be moved between servers in case of
|
|
||||||
migration or for failover.
|
|
||||||
By using Floating IP you can avoid downtime
|
|
||||||
and keep the IP address reputation
|
|
||||||
for destinatinons that rely on IP reputation and IP blocklists.
|
|
||||||
In this case you will only publish
|
|
||||||
the Floating IP to DNS and only use the static IP
|
|
||||||
to SSH into the server.
|
|
||||||
|
|
||||||
If you have such setup, make sure that
|
|
||||||
you not only set ``PTR`` records for the Floating IP,
|
|
||||||
but make outgoing connections using the Floating IP.
|
|
||||||
Otherwise reverse DNS check succeed,
|
|
||||||
but forward check making sure your domain name points
|
|
||||||
to the IP address will fail.
|
|
||||||
Such setup is indistinguishable from someone
|
|
||||||
setting IP address ``PTR`` with the domain they don't own
|
|
||||||
and as a result don't succeed.
|
|
||||||
|
|
||||||
On Linux you can configure source IP address with ``ip route`` command,
|
|
||||||
for example:
|
|
||||||
::
|
|
||||||
|
|
||||||
ip route change default via <default-gateway> dev eth0 src <source-address>
|
|
||||||
|
|
||||||
Make sure to persist the change after verifying it is working.
|
|
||||||
You can check what your outgoing IP address is
|
|
||||||
with ``curl icanhazip.com``.
|
|
||||||
Check both the IPv4 and IPv6 addresses.
|
|
||||||
For IPv4 address use ``curl ipv4.icanhazip.com`` or ``curl -4 icanhazip.com``
|
|
||||||
and similarly for IPv6 if you have it.
|
|
||||||
Reference in New Issue
Block a user