mirror of
https://github.com/chatmail/relay.git
synced 2026-05-16 20:08:58 +00:00
Compare commits
1 Commits
ipv4-only-
...
fix-cmdepl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d32b2497ed |
35
.github/workflows/ci-no-dns.yaml
vendored
35
.github/workflows/ci-no-dns.yaml
vendored
@@ -1,35 +0,0 @@
|
|||||||
name: No-DNS
|
|
||||||
|
|
||||||
on:
|
|
||||||
# Triggers when a PR is merged into main or a direct push occurs
|
|
||||||
push:
|
|
||||||
branches: [ "main" ]
|
|
||||||
|
|
||||||
# Triggers for any PR (and its subsequent commits) targeting the main branch
|
|
||||||
pull_request:
|
|
||||||
branches: [ "main" ]
|
|
||||||
|
|
||||||
# Newest push wins: Prevents multiple runs from clashing and wasting runner efforts
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
no-dns:
|
|
||||||
name: LXC deploy and test
|
|
||||||
uses: chatmail/cmlxc/.github/workflows/lxc-test.yml@d39fe34c39cee6d760c3479325e8dc82b66a8928
|
|
||||||
with:
|
|
||||||
cmlxc_commands: |
|
|
||||||
cmlxc init
|
|
||||||
# single cmdeploy relay test
|
|
||||||
cmlxc -v deploy-cmdeploy --source ./repo --ipv4-only --no-dns cm0
|
|
||||||
cmlxc -v test-cmdeploy --no-dns cm0
|
|
||||||
|
|
||||||
# cross cmdeploy relay test
|
|
||||||
cmlxc -v deploy-cmdeploy --source ./repo cm1
|
|
||||||
cmlxc -v test-cmdeploy --no-dns cm0 cm1
|
|
||||||
|
|
||||||
# cross cmdeploy/madmail relay tests
|
|
||||||
cmlxc -v deploy-madmail mad0
|
|
||||||
cmlxc -v test-cmdeploy --no-dns cm0 mad0
|
|
||||||
10
.github/workflows/ci.yaml
vendored
10
.github/workflows/ci.yaml
vendored
@@ -1,4 +1,4 @@
|
|||||||
name: CI
|
name: Run unit-tests and container-based deploy+test verification
|
||||||
|
|
||||||
on:
|
on:
|
||||||
# Triggers when a PR is merged into main or a direct push occurs
|
# Triggers when a PR is merged into main or a direct push occurs
|
||||||
@@ -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,9 +25,8 @@ 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.4/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
|
||||||
working-directory: chatmaild
|
working-directory: chatmaild
|
||||||
run: pipx run tox
|
run: pipx run tox
|
||||||
@@ -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
|
||||||
@@ -57,7 +53,7 @@ jobs:
|
|||||||
|
|
||||||
lxc-test:
|
lxc-test:
|
||||||
name: LXC deploy and test
|
name: LXC deploy and test
|
||||||
uses: chatmail/cmlxc/.github/workflows/lxc-test.yml@d39fe34c39cee6d760c3479325e8dc82b66a8928
|
uses: chatmail/cmlxc/.github/workflows/lxc-test.yml@v0.10.0
|
||||||
with:
|
with:
|
||||||
cmlxc_commands: |
|
cmlxc_commands: |
|
||||||
cmlxc init
|
cmlxc init
|
||||||
|
|||||||
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
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ dependencies = [
|
|||||||
"filelock",
|
"filelock",
|
||||||
"requests",
|
"requests",
|
||||||
"crypt-r >= 3.13.1 ; python_version >= '3.11'",
|
"crypt-r >= 3.13.1 ; python_version >= '3.11'",
|
||||||
"domain-validator",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.setuptools]
|
[tool.setuptools]
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import ipaddress
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import iniconfig
|
import iniconfig
|
||||||
from domain_validator import DomainValidator
|
|
||||||
|
|
||||||
from chatmaild.user import User
|
from chatmaild.user import User
|
||||||
|
|
||||||
@@ -21,19 +19,7 @@ def read_config(inipath):
|
|||||||
class Config:
|
class Config:
|
||||||
def __init__(self, inipath, params):
|
def __init__(self, inipath, params):
|
||||||
self._inipath = inipath
|
self._inipath = inipath
|
||||||
raw_domain = params["mail_domain"]
|
self.mail_domain = params["mail_domain"]
|
||||||
self.mail_domain_bare = raw_domain
|
|
||||||
|
|
||||||
if is_valid_ipv4(raw_domain):
|
|
||||||
self.ipv4_relay = raw_domain
|
|
||||||
self.mail_domain = f"[{raw_domain}]"
|
|
||||||
self.postfix_myhostname = ipaddress.IPv4Address(raw_domain).reverse_pointer
|
|
||||||
else:
|
|
||||||
DomainValidator().validate_domain_re(raw_domain)
|
|
||||||
self.ipv4_relay = None
|
|
||||||
self.mail_domain = raw_domain
|
|
||||||
self.postfix_myhostname = raw_domain
|
|
||||||
|
|
||||||
self.max_user_send_per_minute = int(params.get("max_user_send_per_minute", 60))
|
self.max_user_send_per_minute = int(params.get("max_user_send_per_minute", 60))
|
||||||
self.max_user_send_burst_size = int(params.get("max_user_send_burst_size", 10))
|
self.max_user_send_burst_size = int(params.get("max_user_send_burst_size", 10))
|
||||||
self.max_mailbox_size = params["max_mailbox_size"]
|
self.max_mailbox_size = params["max_mailbox_size"]
|
||||||
@@ -54,9 +40,6 @@ class Config:
|
|||||||
self.filtermail_http_port_incoming = int(
|
self.filtermail_http_port_incoming = int(
|
||||||
params.get("filtermail_http_port_incoming", "10082")
|
params.get("filtermail_http_port_incoming", "10082")
|
||||||
)
|
)
|
||||||
self.filtermail_lmtp_port_transport = int(
|
|
||||||
params.get("filtermail_lmtp_port_transport", "10083")
|
|
||||||
)
|
|
||||||
self.postfix_reinject_port = int(params.get("postfix_reinject_port", "10025"))
|
self.postfix_reinject_port = int(params.get("postfix_reinject_port", "10025"))
|
||||||
self.postfix_reinject_port_incoming = int(
|
self.postfix_reinject_port_incoming = int(
|
||||||
params.get("postfix_reinject_port_incoming", "10026")
|
params.get("postfix_reinject_port_incoming", "10026")
|
||||||
@@ -67,7 +50,7 @@ class Config:
|
|||||||
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"
|
||||||
if "iroh_relay" not in params:
|
if "iroh_relay" not in params:
|
||||||
self.iroh_relay = "https://" + raw_domain
|
self.iroh_relay = "https://" + params["mail_domain"]
|
||||||
self.enable_iroh_relay = True
|
self.enable_iroh_relay = True
|
||||||
else:
|
else:
|
||||||
self.iroh_relay = params["iroh_relay"].strip()
|
self.iroh_relay = params["iroh_relay"].strip()
|
||||||
@@ -93,17 +76,17 @@ class Config:
|
|||||||
)
|
)
|
||||||
self.tls_cert_mode = "external"
|
self.tls_cert_mode = "external"
|
||||||
self.tls_cert_path, self.tls_key_path = parts
|
self.tls_cert_path, self.tls_key_path = parts
|
||||||
elif raw_domain.startswith("_") or self.ipv4_relay:
|
elif self.mail_domain.startswith("_"):
|
||||||
self.tls_cert_mode = "self"
|
self.tls_cert_mode = "self"
|
||||||
self.tls_cert_path = "/etc/ssl/certs/mailserver.pem"
|
self.tls_cert_path = "/etc/ssl/certs/mailserver.pem"
|
||||||
self.tls_key_path = "/etc/ssl/private/mailserver.key"
|
self.tls_key_path = "/etc/ssl/private/mailserver.key"
|
||||||
else:
|
else:
|
||||||
self.tls_cert_mode = "acme"
|
self.tls_cert_mode = "acme"
|
||||||
self.tls_cert_path = f"/var/lib/acme/live/{raw_domain}/fullchain"
|
self.tls_cert_path = f"/var/lib/acme/live/{self.mail_domain}/fullchain"
|
||||||
self.tls_key_path = f"/var/lib/acme/live/{raw_domain}/privkey"
|
self.tls_key_path = f"/var/lib/acme/live/{self.mail_domain}/privkey"
|
||||||
|
|
||||||
# deprecated option
|
# deprecated option
|
||||||
mbdir = params.get("mailboxes_dir", f"/home/vmail/mail/{raw_domain}")
|
mbdir = params.get("mailboxes_dir", f"/home/vmail/mail/{self.mail_domain}")
|
||||||
self.mailboxes_dir = Path(mbdir.strip())
|
self.mailboxes_dir = Path(mbdir.strip())
|
||||||
|
|
||||||
# old unused option (except for first migration from sqlite to maildir store)
|
# old unused option (except for first migration from sqlite to maildir store)
|
||||||
@@ -189,27 +172,3 @@ def get_default_config_content(mail_domain, **overrides):
|
|||||||
lines.append(line)
|
lines.append(line)
|
||||||
content = "\n".join(lines)
|
content = "\n".join(lines)
|
||||||
return content
|
return content
|
||||||
|
|
||||||
|
|
||||||
def is_valid_ipv4(address: str) -> bool:
|
|
||||||
"""Check if a mail_domain is an IPv4 address."""
|
|
||||||
try:
|
|
||||||
ipaddress.IPv4Address(address)
|
|
||||||
return True
|
|
||||||
except ValueError:
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def format_arpa_address(address: str) -> str:
|
|
||||||
if is_valid_ipv4(address):
|
|
||||||
return ipaddress.IPv4Address(address).reverse_pointer
|
|
||||||
DomainValidator().validate_domain_re(address)
|
|
||||||
return address
|
|
||||||
|
|
||||||
|
|
||||||
def format_mail_domain(raw_domain: str) -> str:
|
|
||||||
if is_valid_ipv4(raw_domain):
|
|
||||||
return f"[{raw_domain}]"
|
|
||||||
DomainValidator().validate_domain_re(raw_domain)
|
|
||||||
return raw_domain
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
"""CGI script for creating new accounts."""
|
"""CGI script for creating new accounts."""
|
||||||
|
|
||||||
|
import ipaddress
|
||||||
import json
|
import json
|
||||||
import secrets
|
import secrets
|
||||||
import string
|
import string
|
||||||
@@ -14,6 +15,16 @@ ALPHANUMERIC = string.ascii_lowercase + string.digits
|
|||||||
ALPHANUMERIC_PUNCT = string.ascii_letters + string.digits + string.punctuation
|
ALPHANUMERIC_PUNCT = string.ascii_letters + string.digits + string.punctuation
|
||||||
|
|
||||||
|
|
||||||
|
def wrap_ip(host):
|
||||||
|
if host.startswith("[") and host.endswith("]"):
|
||||||
|
return host
|
||||||
|
try:
|
||||||
|
ipaddress.ip_address(host)
|
||||||
|
return f"[{host}]"
|
||||||
|
except ValueError:
|
||||||
|
return host
|
||||||
|
|
||||||
|
|
||||||
def create_newemail_dict(config: Config):
|
def create_newemail_dict(config: Config):
|
||||||
user = "".join(
|
user = "".join(
|
||||||
secrets.choice(ALPHANUMERIC) for _ in range(config.username_max_length)
|
secrets.choice(ALPHANUMERIC) for _ in range(config.username_max_length)
|
||||||
@@ -22,22 +33,16 @@ def create_newemail_dict(config: Config):
|
|||||||
secrets.choice(ALPHANUMERIC_PUNCT)
|
secrets.choice(ALPHANUMERIC_PUNCT)
|
||||||
for _ in range(config.password_min_length + 3)
|
for _ in range(config.password_min_length + 3)
|
||||||
)
|
)
|
||||||
return dict(email=f"{user}@{config.mail_domain}", password=f"{password}")
|
return dict(email=f"{user}@{wrap_ip(config.mail_domain)}", password=f"{password}")
|
||||||
|
|
||||||
|
|
||||||
def create_dclogin_url(config, email, password):
|
def create_dclogin_url(email, password):
|
||||||
"""Build a dclogin: URL with credentials and self-signed cert acceptance.
|
"""Build a dclogin: URL with credentials and self-signed cert acceptance.
|
||||||
|
|
||||||
Uses ic=3 (AcceptInvalidCertificates) so chatmail clients
|
Uses ic=3 (AcceptInvalidCertificates) so chatmail clients
|
||||||
can connect to servers with self-signed TLS certificates.
|
can connect to servers with self-signed TLS certificates.
|
||||||
"""
|
"""
|
||||||
if config.ipv4_relay:
|
return f"dclogin:{quote(email, safe='@')}?p={quote(password, safe='')}&v=1&ic=3"
|
||||||
imap_host = "&ih=" + config.ipv4_relay
|
|
||||||
smtp_host = "&sh=" + config.ipv4_relay
|
|
||||||
else:
|
|
||||||
imap_host = ""
|
|
||||||
smtp_host = ""
|
|
||||||
return f"dclogin:{quote(email, safe='@[]')}?p={quote(password, safe='')}&v=1{imap_host}{smtp_host}&ic=3"
|
|
||||||
|
|
||||||
|
|
||||||
def print_new_account():
|
def print_new_account():
|
||||||
@@ -46,9 +51,7 @@ def print_new_account():
|
|||||||
|
|
||||||
result = dict(email=creds["email"], password=creds["password"])
|
result = dict(email=creds["email"], password=creds["password"])
|
||||||
if config.tls_cert_mode == "self":
|
if config.tls_cert_mode == "self":
|
||||||
result["dclogin_url"] = create_dclogin_url(
|
result["dclogin_url"] = create_dclogin_url(creds["email"], creds["password"])
|
||||||
config, creds["email"], creds["password"]
|
|
||||||
)
|
|
||||||
|
|
||||||
print("Content-Type: application/json")
|
print("Content-Type: application/json")
|
||||||
print("")
|
print("")
|
||||||
|
|||||||
@@ -31,11 +31,6 @@ def example_config(make_config):
|
|||||||
return make_config("chat.example.org")
|
return make_config("chat.example.org")
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def ipv4_config(make_config):
|
|
||||||
return make_config("1.3.3.7")
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def maildomain(example_config):
|
def maildomain(example_config):
|
||||||
return example_config.mail_domain
|
return example_config.mail_domain
|
||||||
|
|||||||
@@ -1,14 +1,6 @@
|
|||||||
from contextlib import nullcontext as does_not_raise
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from chatmaild.config import (
|
from chatmaild.config import parse_size_mb, read_config
|
||||||
format_arpa_address,
|
|
||||||
format_mail_domain,
|
|
||||||
is_valid_ipv4,
|
|
||||||
parse_size_mb,
|
|
||||||
read_config,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_read_config_basic(example_config):
|
def test_read_config_basic(example_config):
|
||||||
@@ -21,12 +13,6 @@ def test_read_config_basic(example_config):
|
|||||||
example_config = read_config(inipath)
|
example_config = read_config(inipath)
|
||||||
assert example_config.max_user_send_per_minute == 37
|
assert example_config.max_user_send_per_minute == 37
|
||||||
assert example_config.mail_domain == "chat.example.org"
|
assert example_config.mail_domain == "chat.example.org"
|
||||||
assert example_config.ipv4_relay is None
|
|
||||||
|
|
||||||
|
|
||||||
def test_read_config_ipv4(ipv4_config):
|
|
||||||
assert ipv4_config.ipv4_relay == "1.3.3.7"
|
|
||||||
assert ipv4_config.mail_domain == "[1.3.3.7]"
|
|
||||||
|
|
||||||
|
|
||||||
def test_read_config_basic_using_defaults(tmp_path, maildomain):
|
def test_read_config_basic_using_defaults(tmp_path, maildomain):
|
||||||
@@ -149,45 +135,3 @@ def test_max_mailbox_size_mb(make_config):
|
|||||||
config = make_config("chat.example.org")
|
config = make_config("chat.example.org")
|
||||||
assert config.max_mailbox_size == "500M"
|
assert config.max_mailbox_size == "500M"
|
||||||
assert config.max_mailbox_size_mb == 500
|
assert config.max_mailbox_size_mb == 500
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
["input", "result"],
|
|
||||||
[
|
|
||||||
("example.org", False),
|
|
||||||
("1.3.3.7", True),
|
|
||||||
("fe::1", False),
|
|
||||||
("ad.1e.dag.adf", False),
|
|
||||||
("12394142", False),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test_is_valid_ipv4(input, result):
|
|
||||||
assert result == is_valid_ipv4(input)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
["input", "result", "exception"],
|
|
||||||
[
|
|
||||||
("example.org", "example.org", does_not_raise()),
|
|
||||||
("1.3.3.7", "7.3.3.1.in-addr.arpa", does_not_raise()),
|
|
||||||
("fe::1", None, pytest.raises(ValueError)),
|
|
||||||
("12394142", None, pytest.raises(ValueError)),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test_format_arpa_address(input, result, exception):
|
|
||||||
with exception:
|
|
||||||
assert result == format_arpa_address(input)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
["input", "result", "exception"],
|
|
||||||
[
|
|
||||||
("example.org", "example.org", does_not_raise()),
|
|
||||||
("1.3.3.7", "[1.3.3.7]", does_not_raise()),
|
|
||||||
("fe::1", None, pytest.raises(ValueError)),
|
|
||||||
("12394142", None, pytest.raises(ValueError)),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test_format_mail_domain(input, result, exception):
|
|
||||||
with exception:
|
|
||||||
assert result == format_mail_domain(input)
|
|
||||||
|
|||||||
@@ -19,35 +19,24 @@ def test_create_newemail_dict(example_config):
|
|||||||
assert ac1["password"] != ac2["password"]
|
assert ac1["password"] != ac2["password"]
|
||||||
|
|
||||||
|
|
||||||
def test_create_newemail_dict_ip(ipv4_config):
|
def test_create_newemail_dict_ip(make_config):
|
||||||
ac = create_newemail_dict(ipv4_config)
|
config = make_config("1.2.3.4")
|
||||||
assert ac["email"].endswith("@[1.3.3.7]")
|
ac = create_newemail_dict(config)
|
||||||
|
assert ac["email"].endswith("@[1.2.3.4]")
|
||||||
|
|
||||||
|
|
||||||
def test_create_dclogin_url(example_config):
|
def test_create_dclogin_url():
|
||||||
addr = "user@example.org"
|
url = create_dclogin_url("user@example.org", "p@ss w+rd")
|
||||||
password = "p@ss w+rd"
|
|
||||||
url = create_dclogin_url(example_config, addr, password)
|
|
||||||
assert url.startswith("dclogin:")
|
assert url.startswith("dclogin:")
|
||||||
assert "v=1" in url
|
assert "v=1" in url
|
||||||
assert "ic=3" in url
|
assert "ic=3" in url
|
||||||
|
|
||||||
assert addr in url
|
assert "user@example.org" in url
|
||||||
# password special chars must be encoded
|
# password special chars must be encoded
|
||||||
assert "p%40ss" in url
|
assert "p%40ss" in url
|
||||||
assert "w%2Brd" in url
|
assert "w%2Brd" in url
|
||||||
|
|
||||||
|
|
||||||
def test_create_dclogin_url_ipv4(ipv4_config):
|
|
||||||
addr = "user@[1.3.3.7]"
|
|
||||||
password = "p@ss w+rd"
|
|
||||||
url = create_dclogin_url(ipv4_config, addr, password)
|
|
||||||
assert url.startswith("dclogin:")
|
|
||||||
assert "v=1" in url
|
|
||||||
assert "ic=3" in url
|
|
||||||
assert addr in url
|
|
||||||
|
|
||||||
|
|
||||||
def test_print_new_account(capsys, monkeypatch, maildomain, tmpdir, example_config):
|
def test_print_new_account(capsys, monkeypatch, maildomain, tmpdir, example_config):
|
||||||
monkeypatch.setattr(chatmaild.newemail, "CONFIG_PATH", str(example_config._inipath))
|
monkeypatch.setattr(chatmaild.newemail, "CONFIG_PATH", str(example_config._inipath))
|
||||||
print_new_account()
|
print_new_account()
|
||||||
|
|||||||
@@ -87,15 +87,15 @@ def run_cmd_options(parser):
|
|||||||
def run_cmd(args, out):
|
def run_cmd(args, out):
|
||||||
"""Deploy chatmail services on the remote server."""
|
"""Deploy chatmail services on the remote server."""
|
||||||
|
|
||||||
ssh_host = args.ssh_host if args.ssh_host else args.config.mail_domain_bare
|
ssh_host = args.ssh_host if args.ssh_host else args.config.mail_domain
|
||||||
sshexec = get_sshexec(ssh_host)
|
sshexec = get_sshexec(ssh_host)
|
||||||
require_iroh = args.config.enable_iroh_relay
|
require_iroh = args.config.enable_iroh_relay
|
||||||
strict_tls = args.config.tls_cert_mode == "acme"
|
strict_tls = args.config.tls_cert_mode == "acme"
|
||||||
if args.config.ipv4_relay:
|
|
||||||
args.dns_check_disabled = True
|
|
||||||
if not args.dns_check_disabled:
|
if not args.dns_check_disabled:
|
||||||
remote_data = dns.get_initial_remote_data(sshexec, args.config.mail_domain)
|
remote_data = dns.get_initial_remote_data(sshexec, args.config.mail_domain)
|
||||||
if not dns.check_initial_remote_data(remote_data, strict_tls=strict_tls, print=out.red):
|
if not dns.check_initial_remote_data(
|
||||||
|
remote_data, strict_tls=strict_tls, print=out.red
|
||||||
|
):
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
@@ -118,11 +118,13 @@ def run_cmd(args, out):
|
|||||||
out.check_call(cmd, env=env)
|
out.check_call(cmd, env=env)
|
||||||
if args.website_only:
|
if args.website_only:
|
||||||
out.green("Website deployment completed.")
|
out.green("Website deployment completed.")
|
||||||
elif not args.dns_check_disabled and strict_tls and not remote_data["acme_account_url"]:
|
elif (
|
||||||
|
not args.dns_check_disabled
|
||||||
|
and strict_tls
|
||||||
|
and not remote_data["acme_account_url"]
|
||||||
|
):
|
||||||
out.red("Deploy completed but letsencrypt not configured")
|
out.red("Deploy completed but letsencrypt not configured")
|
||||||
out.red("Run 'cmdeploy run' again")
|
out.red("Run 'cmdeploy run' again")
|
||||||
elif args.config.ipv4_relay:
|
|
||||||
out.green("Deploy completed.")
|
|
||||||
else:
|
else:
|
||||||
out.green("Deploy completed, call `cmdeploy dns` next.")
|
out.green("Deploy completed, call `cmdeploy dns` next.")
|
||||||
return 0
|
return 0
|
||||||
@@ -144,10 +146,6 @@ def dns_cmd_options(parser):
|
|||||||
|
|
||||||
def dns_cmd(args, out):
|
def dns_cmd(args, out):
|
||||||
"""Check DNS entries and optionally generate dns zone file."""
|
"""Check DNS entries and optionally generate dns zone file."""
|
||||||
if args.config.ipv4_relay:
|
|
||||||
ipv4 = args.config.ipv4_relay
|
|
||||||
print(f"[WARNING] {ipv4} is not a domain, skipping DNS checks.")
|
|
||||||
return 0
|
|
||||||
ssh_host = args.ssh_host if args.ssh_host else args.config.mail_domain
|
ssh_host = args.ssh_host if args.ssh_host else args.config.mail_domain
|
||||||
sshexec = get_sshexec(ssh_host, verbose=args.verbose)
|
sshexec = get_sshexec(ssh_host, verbose=args.verbose)
|
||||||
tls_cert_mode = args.config.tls_cert_mode
|
tls_cert_mode = args.config.tls_cert_mode
|
||||||
@@ -185,7 +183,7 @@ def status_cmd_options(parser):
|
|||||||
def status_cmd(args, out):
|
def status_cmd(args, out):
|
||||||
"""Display status for online chatmail instance."""
|
"""Display status for online chatmail instance."""
|
||||||
|
|
||||||
ssh_host = args.ssh_host if args.ssh_host else args.config.mail_domain_bare
|
ssh_host = args.ssh_host if args.ssh_host else args.config.mail_domain
|
||||||
sshexec = get_sshexec(ssh_host, verbose=args.verbose)
|
sshexec = get_sshexec(ssh_host, verbose=args.verbose)
|
||||||
|
|
||||||
out.green(f"chatmail domain: {args.config.mail_domain}")
|
out.green(f"chatmail domain: {args.config.mail_domain}")
|
||||||
|
|||||||
@@ -468,7 +468,7 @@ class ChatmailVenvDeployer(Deployer):
|
|||||||
|
|
||||||
def configure(self):
|
def configure(self):
|
||||||
_configure_remote_venv_with_chatmaild(self.config)
|
_configure_remote_venv_with_chatmaild(self.config)
|
||||||
configure_remote_units(self.config.mail_domain_bare, self.units)
|
configure_remote_units(self.config.mail_domain, self.units)
|
||||||
|
|
||||||
def activate(self):
|
def activate(self):
|
||||||
activate_remote_units(self.units)
|
activate_remote_units(self.units)
|
||||||
@@ -584,18 +584,24 @@ def deploy_chatmail(config_path: Path, disable_mail: bool, website_only: bool) -
|
|||||||
"""
|
"""
|
||||||
config = read_config(config_path)
|
config = read_config(config_path)
|
||||||
check_config(config)
|
check_config(config)
|
||||||
bare_host = config.mail_domain_bare
|
mail_domain = config.mail_domain
|
||||||
|
|
||||||
if website_only:
|
if website_only:
|
||||||
Deployment().perform_stages([WebsiteDeployer(config)])
|
Deployment().perform_stages([WebsiteDeployer(config)])
|
||||||
return
|
return
|
||||||
|
|
||||||
# Check if mtail_address interface is available (if configured)
|
# Check if mtail_address interface is available (if configured)
|
||||||
if config.mtail_address and config.mtail_address not in ('127.0.0.1', '::1', 'localhost'):
|
if config.mtail_address and config.mtail_address not in (
|
||||||
|
"127.0.0.1",
|
||||||
|
"::1",
|
||||||
|
"localhost",
|
||||||
|
):
|
||||||
ipv4_addrs = host.get_fact(hardware.Ipv4Addrs)
|
ipv4_addrs = host.get_fact(hardware.Ipv4Addrs)
|
||||||
all_addresses = [addr for addrs in ipv4_addrs.values() for addr in addrs]
|
all_addresses = [addr for addrs in ipv4_addrs.values() for addr in addrs]
|
||||||
if config.mtail_address not in all_addresses:
|
if config.mtail_address not in all_addresses:
|
||||||
Out().red(f"Deploy failed: mtail_address {config.mtail_address} is not available (VPN up?).\n")
|
Out().red(
|
||||||
|
f"Deploy failed: mtail_address {config.mtail_address} is not available (VPN up?).\n"
|
||||||
|
)
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
if not is_in_container():
|
if not is_in_container():
|
||||||
@@ -635,7 +641,7 @@ def deploy_chatmail(config_path: Path, disable_mail: bool, website_only: bool) -
|
|||||||
)
|
)
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
tls_deployer = get_tls_deployer(config, bare_host)
|
tls_deployer = get_tls_deployer(config, mail_domain)
|
||||||
|
|
||||||
all_deployers = [
|
all_deployers = [
|
||||||
ChatmailDeployer(config),
|
ChatmailDeployer(config),
|
||||||
@@ -643,13 +649,13 @@ def deploy_chatmail(config_path: Path, disable_mail: bool, website_only: bool) -
|
|||||||
FiltermailDeployer(),
|
FiltermailDeployer(),
|
||||||
JournaldDeployer(),
|
JournaldDeployer(),
|
||||||
UnboundDeployer(config),
|
UnboundDeployer(config),
|
||||||
TurnDeployer(bare_host),
|
TurnDeployer(mail_domain),
|
||||||
IrohDeployer(config.enable_iroh_relay),
|
IrohDeployer(config.enable_iroh_relay),
|
||||||
tls_deployer,
|
tls_deployer,
|
||||||
WebsiteDeployer(config),
|
WebsiteDeployer(config),
|
||||||
ChatmailVenvDeployer(config),
|
ChatmailVenvDeployer(config),
|
||||||
MtastsDeployer(),
|
MtastsDeployer(),
|
||||||
OpendkimDeployer(config.mail_domain),
|
OpendkimDeployer(mail_domain),
|
||||||
# Dovecot should be started before Postfix
|
# Dovecot should be started before Postfix
|
||||||
# because it creates authentication socket
|
# because it creates authentication socket
|
||||||
# required by Postfix.
|
# required by Postfix.
|
||||||
|
|||||||
@@ -20,12 +20,30 @@ DOVECOT_ARCHIVE_VERSION = "2.3.21+dfsg1-3"
|
|||||||
DOVECOT_PACKAGE_VERSION = f"1:{DOVECOT_ARCHIVE_VERSION}"
|
DOVECOT_PACKAGE_VERSION = f"1:{DOVECOT_ARCHIVE_VERSION}"
|
||||||
|
|
||||||
DOVECOT_SHA256 = {
|
DOVECOT_SHA256 = {
|
||||||
("core", "amd64"): "dd060706f52a306fa863d874717210b9fe10536c824afe1790eec247ded5b27d",
|
(
|
||||||
("core", "arm64"): "e7548e8a82929722e973629ecc40fcfa886894cef3db88f23535149e7f730dc9",
|
"core",
|
||||||
("imapd", "amd64"): "8d8dc6fc00bbb6cdb25d345844f41ce2f1c53f764b79a838eb2a03103eebfa86",
|
"amd64",
|
||||||
("imapd", "arm64"): "178fa877ddd5df9930e8308b518f4b07df10e759050725f8217a0c1fb3fd707f",
|
): "dd060706f52a306fa863d874717210b9fe10536c824afe1790eec247ded5b27d",
|
||||||
("lmtpd", "amd64"): "2f69ba5e35363de50962d42cccbfe4ed8495265044e244007d7ccddad77513ab",
|
(
|
||||||
("lmtpd", "arm64"): "89f52fb36524f5877a177dff4a713ba771fd3f91f22ed0af7238d495e143b38f",
|
"core",
|
||||||
|
"arm64",
|
||||||
|
): "e7548e8a82929722e973629ecc40fcfa886894cef3db88f23535149e7f730dc9",
|
||||||
|
(
|
||||||
|
"imapd",
|
||||||
|
"amd64",
|
||||||
|
): "8d8dc6fc00bbb6cdb25d345844f41ce2f1c53f764b79a838eb2a03103eebfa86",
|
||||||
|
(
|
||||||
|
"imapd",
|
||||||
|
"arm64",
|
||||||
|
): "178fa877ddd5df9930e8308b518f4b07df10e759050725f8217a0c1fb3fd707f",
|
||||||
|
(
|
||||||
|
"lmtpd",
|
||||||
|
"amd64",
|
||||||
|
): "2f69ba5e35363de50962d42cccbfe4ed8495265044e244007d7ccddad77513ab",
|
||||||
|
(
|
||||||
|
"lmtpd",
|
||||||
|
"arm64",
|
||||||
|
): "89f52fb36524f5877a177dff4a713ba771fd3f91f22ed0af7238d495e143b38f",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -61,11 +79,7 @@ class DovecotDeployer(Deployer):
|
|||||||
self.need_restart = True
|
self.need_restart = True
|
||||||
files.put(
|
files.put(
|
||||||
name="Pin dovecot packages to block Debian dist-upgrades",
|
name="Pin dovecot packages to block Debian dist-upgrades",
|
||||||
src=io.StringIO(
|
src=io.StringIO("Package: dovecot-*\nPin: version *\nPin-Priority: -1\n"),
|
||||||
"Package: dovecot-*\n"
|
|
||||||
"Pin: version *\n"
|
|
||||||
"Pin-Priority: -1\n"
|
|
||||||
),
|
|
||||||
dest="/etc/apt/preferences.d/pin-dovecot",
|
dest="/etc/apt/preferences.d/pin-dovecot",
|
||||||
user="root",
|
user="root",
|
||||||
group="root",
|
group="root",
|
||||||
@@ -73,7 +87,7 @@ class DovecotDeployer(Deployer):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def configure(self):
|
def configure(self):
|
||||||
configure_remote_units(self.config.mail_domain_bare, self.units)
|
configure_remote_units(self.config.mail_domain, self.units)
|
||||||
config_restart, self.daemon_reload = _configure_dovecot(self.config)
|
config_restart, self.daemon_reload = _configure_dovecot(self.config)
|
||||||
self.need_restart |= config_restart
|
self.need_restart |= config_restart
|
||||||
|
|
||||||
@@ -84,7 +98,7 @@ class DovecotDeployer(Deployer):
|
|||||||
if not self.disable_mail and not self.need_restart:
|
if not self.disable_mail and not self.need_restart:
|
||||||
stale = host.get_fact(
|
stale = host.get_fact(
|
||||||
Command,
|
Command,
|
||||||
'pid=$(systemctl show -p MainPID --value dovecot.service 2>/dev/null);'
|
"pid=$(systemctl show -p MainPID --value dovecot.service 2>/dev/null);"
|
||||||
' [ "${pid:-0}" != "0" ] && readlink "/proc/$pid/exe" 2>/dev/null | grep -q "(deleted)"'
|
' [ "${pid:-0}" != "0" ] && readlink "/proc/$pid/exe" 2>/dev/null | grep -q "(deleted)"'
|
||||||
" && echo STALE || true",
|
" && echo STALE || true",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ listen = 0.0.0.0
|
|||||||
protocols = imap lmtp
|
protocols = imap lmtp
|
||||||
|
|
||||||
auth_mechanisms = plain
|
auth_mechanisms = plain
|
||||||
auth_username_chars = abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890.-_@[]
|
|
||||||
|
|
||||||
{% if debug == true %}
|
{% if debug == true %}
|
||||||
auth_verbose = yes
|
auth_verbose = yes
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
@@ -7,7 +5,7 @@ from cmdeploy.basedeploy import Deployer, get_resource
|
|||||||
|
|
||||||
|
|
||||||
class FiltermailDeployer(Deployer):
|
class FiltermailDeployer(Deployer):
|
||||||
services = ["filtermail", "filtermail-incoming", "filtermail-transport"]
|
services = ["filtermail", "filtermail-incoming"]
|
||||||
bin_path = "/usr/local/bin/filtermail"
|
bin_path = "/usr/local/bin/filtermail"
|
||||||
config_path = "/usr/local/lib/chatmaild/chatmail.ini"
|
config_path = "/usr/local/lib/chatmaild/chatmail.ini"
|
||||||
|
|
||||||
@@ -15,21 +13,11 @@ 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.4/filtermail-{arch}"
|
url = f"https://github.com/chatmail/filtermail/releases/download/v0.6.1/filtermail-{arch}"
|
||||||
sha256sum = {
|
sha256sum = {
|
||||||
"x86_64": "5295115952c72e4c4ec3c85546e094b4155a4c702c82bd71fcdcb744dc73adf6",
|
"x86_64": "48b3fb80c092d00b9b0a0ef77a8673496da3b9aed5ec1851e1df936d5589d62f",
|
||||||
"aarch64": "6892244f17b8f26ccb465766e96028e7222b3c8adefca9fc6bfe9ff332ca8dff",
|
"aarch64": "c65bd5f45df187d3d65d6965a285583a3be0f44a6916ff12909ff9a8d702c22e",
|
||||||
}[arch]
|
}[arch]
|
||||||
self.need_restart |= files.download(
|
self.need_restart |= files.download(
|
||||||
name="Download filtermail",
|
name="Download filtermail",
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=Chatmail transport service
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
ExecStart={{ bin_path }} {{ config_path }} transport
|
|
||||||
Restart=always
|
|
||||||
RestartSec=30
|
|
||||||
User=vmail
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=mtail
|
Description=mtail
|
||||||
After=multi-user.target
|
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ http {
|
|||||||
|
|
||||||
access_log syslog:server=unix:/dev/log,facility=local7;
|
access_log syslog:server=unix:/dev/log,facility=local7;
|
||||||
|
|
||||||
location /mxdeliv {
|
location /mxdeliv/ {
|
||||||
proxy_pass http://127.0.0.1:{{ config.filtermail_http_port_incoming }};
|
proxy_pass http://127.0.0.1:{{ config.filtermail_http_port_incoming }};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -54,16 +54,14 @@ smtpd_tls_exclude_ciphers = aNULL, RC4, MD5, DES
|
|||||||
tls_preempt_cipherlist = yes
|
tls_preempt_cipherlist = yes
|
||||||
|
|
||||||
smtpd_relay_restrictions = permit_mynetworks permit_sasl_authenticated defer_unauth_destination
|
smtpd_relay_restrictions = permit_mynetworks permit_sasl_authenticated defer_unauth_destination
|
||||||
myhostname = {{ config.postfix_myhostname }}
|
myhostname = {{ config.mail_domain }}
|
||||||
alias_maps = hash:/etc/aliases
|
alias_maps = hash:/etc/aliases
|
||||||
alias_database = hash:/etc/aliases
|
alias_database = hash:/etc/aliases
|
||||||
|
|
||||||
# When postfix receives mail for $mydestination,
|
# Postfix does not deliver mail for any domain by itself.
|
||||||
# it hands it over to dovecot via $local_transport.
|
# Primary domain is listed in `virtual_mailbox_domains` instead
|
||||||
mydestination = {{ config.mail_domain }}
|
# and handed over to Dovecot.
|
||||||
local_transport = lmtp:unix:private/dovecot-lmtp
|
mydestination =
|
||||||
# postfix doesn't check whether local users exist or not:
|
|
||||||
local_recipient_maps =
|
|
||||||
|
|
||||||
relayhost =
|
relayhost =
|
||||||
{% if disable_ipv6 %}
|
{% if disable_ipv6 %}
|
||||||
@@ -81,6 +79,24 @@ inet_protocols = ipv4
|
|||||||
inet_protocols = all
|
inet_protocols = all
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
# Postfix does not try IPv4 and IPv6 connections
|
||||||
|
# concurrently as of version 3.7.11.
|
||||||
|
#
|
||||||
|
# When relay has both A (IPv4) and AAAA (IPv6) records,
|
||||||
|
# but broken IPv6 connectivity,
|
||||||
|
# every second message is delayed by the connection timeout
|
||||||
|
# <https://www.postfix.org/postconf.5.html#smtp_connect_timeout>
|
||||||
|
# which defaults to 30 seconds. Reducing timeouts is not a solution
|
||||||
|
# as this will result in a failure to connect to slow servers.
|
||||||
|
#
|
||||||
|
# As a workaround we always prefer IPv4 when it is available.
|
||||||
|
#
|
||||||
|
# The setting is documented at
|
||||||
|
# <https://www.postfix.org/postconf.5.html#smtp_address_preference>
|
||||||
|
smtp_address_preference=ipv4
|
||||||
|
|
||||||
|
virtual_transport = lmtp:unix:private/dovecot-lmtp
|
||||||
|
virtual_mailbox_domains = {{ config.mail_domain }}
|
||||||
lmtp_header_checks = regexp:/etc/postfix/lmtp_header_cleanup
|
lmtp_header_checks = regexp:/etc/postfix/lmtp_header_cleanup
|
||||||
|
|
||||||
mua_client_restrictions = permit_sasl_authenticated, reject
|
mua_client_restrictions = permit_sasl_authenticated, reject
|
||||||
@@ -93,12 +109,3 @@ smtpd_sender_login_maps = regexp:/etc/postfix/login_map
|
|||||||
# Do not lookup SMTP client hostnames to reduce delays
|
# Do not lookup SMTP client hostnames to reduce delays
|
||||||
# and avoid unnecessary DNS requests.
|
# and avoid unnecessary DNS requests.
|
||||||
smtpd_peername_lookup = no
|
smtpd_peername_lookup = no
|
||||||
|
|
||||||
# Use filtermail-transport to relay messages.
|
|
||||||
# We can't force postfix to split messages per destination,
|
|
||||||
# when specifying a custom next-hop,
|
|
||||||
# so instead this is handled in filtermail.
|
|
||||||
# We use LMTP instead SMTP so we can communicate per-recipient errors back to postfix.
|
|
||||||
default_transport = lmtp-filtermail:inet:[127.0.0.1]:{{ config.filtermail_lmtp_port_transport }}
|
|
||||||
lmtp-filtermail_initial_destination_concurrency=10000
|
|
||||||
lmtp-filtermail_destination_concurrency_limit=10000
|
|
||||||
|
|||||||
@@ -80,9 +80,8 @@ filter unix - n n - - lmtp
|
|||||||
127.0.0.1:{{ config.postfix_reinject_port }} inet n - n - 100 smtpd
|
127.0.0.1:{{ config.postfix_reinject_port }} inet n - n - 100 smtpd
|
||||||
-o syslog_name=postfix/reinject
|
-o syslog_name=postfix/reinject
|
||||||
-o milter_macro_daemon_name=ORIGINATING
|
-o milter_macro_daemon_name=ORIGINATING
|
||||||
|
-o smtpd_milters=unix:opendkim/opendkim.sock
|
||||||
-o cleanup_service_name=authclean
|
-o cleanup_service_name=authclean
|
||||||
{% if not config.ipv4_relay %} -o smtpd_milters=unix:opendkim/opendkim.sock
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
# Local SMTP server for reinjecting incoming filtered mail
|
# Local SMTP server for reinjecting incoming filtered mail
|
||||||
127.0.0.1:{{ config.postfix_reinject_port_incoming }} inet n - n - 100 smtpd
|
127.0.0.1:{{ config.postfix_reinject_port_incoming }} inet n - n - 100 smtpd
|
||||||
@@ -101,8 +100,3 @@ filter unix - n n - - lmtp
|
|||||||
# cannot send unprotected Subject.
|
# cannot send unprotected Subject.
|
||||||
authclean unix n - - - 0 cleanup
|
authclean unix n - - - 0 cleanup
|
||||||
-o header_checks=regexp:/etc/postfix/submission_header_cleanup
|
-o header_checks=regexp:/etc/postfix/submission_header_cleanup
|
||||||
|
|
||||||
lmtp-filtermail unix - - y - 10000 lmtp
|
|
||||||
-o syslog_name=postfix/lmtp-filtermail
|
|
||||||
-o lmtp_header_checks=
|
|
||||||
-o lmtp_tls_security_level=none
|
|
||||||
|
|||||||
@@ -12,15 +12,27 @@ def openssl_selfsigned_args(domain, cert_path, key_path, days=36500):
|
|||||||
``www.<domain>`` and ``mta-sts.<domain>``.
|
``www.<domain>`` and ``mta-sts.<domain>``.
|
||||||
"""
|
"""
|
||||||
return [
|
return [
|
||||||
"openssl", "req", "-x509",
|
"openssl",
|
||||||
"-newkey", "ec", "-pkeyopt", "ec_paramgen_curve:P-256",
|
"req",
|
||||||
"-noenc", "-days", str(days),
|
"-x509",
|
||||||
"-keyout", str(key_path),
|
"-newkey",
|
||||||
"-out", str(cert_path),
|
"ec",
|
||||||
"-subj", f"/CN={domain}",
|
"-pkeyopt",
|
||||||
|
"ec_paramgen_curve:P-256",
|
||||||
|
"-noenc",
|
||||||
|
"-days",
|
||||||
|
str(days),
|
||||||
|
"-keyout",
|
||||||
|
str(key_path),
|
||||||
|
"-out",
|
||||||
|
str(cert_path),
|
||||||
|
"-subj",
|
||||||
|
f"/CN={domain}",
|
||||||
# Mark as end-entity cert so it cannot be used as a CA to sign others.
|
# Mark as end-entity cert so it cannot be used as a CA to sign others.
|
||||||
"-addext", "basicConstraints=critical,CA:FALSE",
|
"-addext",
|
||||||
"-addext", "extendedKeyUsage=serverAuth,clientAuth",
|
"basicConstraints=critical,CA:FALSE",
|
||||||
|
"-addext",
|
||||||
|
"extendedKeyUsage=serverAuth,clientAuth",
|
||||||
"-addext",
|
"-addext",
|
||||||
f"subjectAltName=DNS:{domain},DNS:www.{domain},DNS:mta-sts.{domain}",
|
f"subjectAltName=DNS:{domain},DNS:www.{domain},DNS:mta-sts.{domain}",
|
||||||
]
|
]
|
||||||
@@ -42,7 +54,9 @@ class SelfSignedTlsDeployer(Deployer):
|
|||||||
|
|
||||||
def configure(self):
|
def configure(self):
|
||||||
args = openssl_selfsigned_args(
|
args = openssl_selfsigned_args(
|
||||||
self.mail_domain, self.cert_path, self.key_path,
|
self.mail_domain,
|
||||||
|
self.cert_path,
|
||||||
|
self.key_path,
|
||||||
)
|
)
|
||||||
cmd = shlex.join(args)
|
cmd = shlex.join(args)
|
||||||
server.shell(
|
server.shell(
|
||||||
|
|||||||
@@ -89,11 +89,12 @@ def test_concurrent_logins_same_account(
|
|||||||
assert login_results.get()
|
assert login_results.get()
|
||||||
|
|
||||||
|
|
||||||
def test_no_vrfy(cmfactory, chatmail_config, maildomain):
|
def test_no_vrfy(cmfactory, chatmail_config):
|
||||||
ac = cmfactory.get_online_account()
|
ac = cmfactory.get_online_account()
|
||||||
addr = ac.get_config("addr")
|
addr = ac.get_config("addr")
|
||||||
|
domain = chatmail_config.mail_domain
|
||||||
|
|
||||||
s = smtplib.SMTP(maildomain)
|
s = smtplib.SMTP(domain)
|
||||||
s.starttls()
|
s.starttls()
|
||||||
|
|
||||||
s.putcmd("vrfy", f"wrongaddress@{chatmail_config.mail_domain}")
|
s.putcmd("vrfy", f"wrongaddress@{chatmail_config.mail_domain}")
|
||||||
|
|||||||
@@ -30,12 +30,15 @@ def test_newemail_configure(maildomain, rpc, chatmail_config):
|
|||||||
# set_config_from_qr, so fetch credentials via requests instead
|
# set_config_from_qr, so fetch credentials via requests instead
|
||||||
res = requests.post(f"https://{maildomain}/new", verify=False)
|
res = requests.post(f"https://{maildomain}/new", verify=False)
|
||||||
data = res.json()
|
data = res.json()
|
||||||
rpc.add_or_update_transport(account_id, {
|
rpc.add_or_update_transport(
|
||||||
"addr": data["email"],
|
account_id,
|
||||||
"password": data["password"],
|
{
|
||||||
"imapServer": maildomain,
|
"addr": data["email"],
|
||||||
"smtpServer": maildomain,
|
"password": data["password"],
|
||||||
"certificateChecks": "acceptInvalidCertificates",
|
"imapServer": maildomain,
|
||||||
})
|
"smtpServer": maildomain,
|
||||||
|
"certificateChecks": "acceptInvalidCertificates",
|
||||||
|
},
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
rpc.add_transport_from_qr(account_id, url)
|
rpc.add_transport_from_qr(account_id, url)
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import pytest
|
|||||||
|
|
||||||
from cmdeploy import remote
|
from cmdeploy import remote
|
||||||
from cmdeploy.cmdeploy import get_sshexec
|
from cmdeploy.cmdeploy import get_sshexec
|
||||||
from chatmaild.config import is_valid_ipv4
|
|
||||||
|
|
||||||
|
|
||||||
class TestSSHExecutor:
|
class TestSSHExecutor:
|
||||||
@@ -22,8 +21,6 @@ class TestSSHExecutor:
|
|||||||
assert out == out2
|
assert out == out2
|
||||||
|
|
||||||
def test_perform_initial(self, sshexec, maildomain):
|
def test_perform_initial(self, sshexec, maildomain):
|
||||||
if is_valid_ipv4(maildomain):
|
|
||||||
pytest.skip(f"{maildomain} is not a domain")
|
|
||||||
res = sshexec(
|
res = sshexec(
|
||||||
remote.rdns.perform_initial_checks, kwargs=dict(mail_domain=maildomain)
|
remote.rdns.perform_initial_checks, kwargs=dict(mail_domain=maildomain)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ def imap_mailbox(cmfactory, ssl_context):
|
|||||||
(ac1,) = cmfactory.get_online_accounts(1)
|
(ac1,) = cmfactory.get_online_accounts(1)
|
||||||
user = ac1.get_config("addr")
|
user = ac1.get_config("addr")
|
||||||
password = ac1.get_config("mail_pw")
|
password = ac1.get_config("mail_pw")
|
||||||
host = user.split("@")[1].strip("[").strip("]")
|
host = user.split("@")[1]
|
||||||
mailbox = imap_tools.MailBox(host, ssl_context=ssl_context)
|
mailbox = imap_tools.MailBox(host, ssl_context=ssl_context)
|
||||||
mailbox.login(user, password)
|
mailbox.login(user, password)
|
||||||
mailbox.dc_ac = ac1
|
mailbox.dc_ac = ac1
|
||||||
@@ -178,7 +178,7 @@ def test_hide_senders_ip_address(cmfactory, ssl_context):
|
|||||||
chat.send_text("testing submission header cleanup")
|
chat.send_text("testing submission header cleanup")
|
||||||
user2.wait_for_incoming_msg()
|
user2.wait_for_incoming_msg()
|
||||||
addr = user2.get_config("addr")
|
addr = user2.get_config("addr")
|
||||||
host = addr.split("@")[1].strip("[").strip("]")
|
host = addr.split("@")[1]
|
||||||
pw = user2.get_config("mail_pw")
|
pw = user2.get_config("mail_pw")
|
||||||
mailbox = imap_tools.MailBox(host, ssl_context=ssl_context)
|
mailbox = imap_tools.MailBox(host, ssl_context=ssl_context)
|
||||||
mailbox.login(addr, pw)
|
mailbox.login(addr, pw)
|
||||||
|
|||||||
@@ -10,8 +10,7 @@ import time
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from chatmaild.config import read_config, format_mail_domain, is_valid_ipv4
|
from chatmaild.config import read_config
|
||||||
|
|
||||||
|
|
||||||
conftestdir = Path(__file__).parent
|
conftestdir = Path(__file__).parent
|
||||||
|
|
||||||
@@ -59,12 +58,7 @@ def chatmail_config(pytestconfig):
|
|||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def maildomain(chatmail_config):
|
def maildomain(chatmail_config):
|
||||||
return chatmail_config.mail_domain_bare
|
return chatmail_config.mail_domain
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
|
||||||
def maildomain_deliverable(maildomain):
|
|
||||||
return format_mail_domain(maildomain)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
@@ -284,6 +278,7 @@ def gencreds(chatmail_config):
|
|||||||
|
|
||||||
def gen(domain=None):
|
def gen(domain=None):
|
||||||
domain = domain if domain else chatmail_config.mail_domain
|
domain = domain if domain else chatmail_config.mail_domain
|
||||||
|
addr_domain = f"[{domain}]" if _is_ip(domain) else domain
|
||||||
while 1:
|
while 1:
|
||||||
num = next(count)
|
num = next(count)
|
||||||
alphanumeric = "abcdefghijklmnopqrstuvwxyz1234567890"
|
alphanumeric = "abcdefghijklmnopqrstuvwxyz1234567890"
|
||||||
@@ -297,7 +292,7 @@ def gencreds(chatmail_config):
|
|||||||
password = "".join(
|
password = "".join(
|
||||||
random.choices(alphanumeric, k=chatmail_config.password_min_length)
|
random.choices(alphanumeric, k=chatmail_config.password_min_length)
|
||||||
)
|
)
|
||||||
yield f"{user}@{domain}", f"{password}"
|
yield f"{user}@{addr_domain}", f"{password}"
|
||||||
|
|
||||||
return lambda domain=None: next(gen(domain))
|
return lambda domain=None: next(gen(domain))
|
||||||
|
|
||||||
@@ -322,8 +317,7 @@ class ChatmailACFactory:
|
|||||||
|
|
||||||
def _make_transport(self, domain):
|
def _make_transport(self, domain):
|
||||||
"""Build a transport config dict for the given domain."""
|
"""Build a transport config dict for the given domain."""
|
||||||
domain_deliverable = format_mail_domain(domain)
|
addr, password = self.gencreds(domain)
|
||||||
addr, password = self.gencreds(domain_deliverable)
|
|
||||||
transport = {
|
transport = {
|
||||||
"addr": addr,
|
"addr": addr,
|
||||||
"password": password,
|
"password": password,
|
||||||
@@ -332,7 +326,7 @@ class ChatmailACFactory:
|
|||||||
"imapServer": domain,
|
"imapServer": domain,
|
||||||
"smtpServer": domain,
|
"smtpServer": domain,
|
||||||
}
|
}
|
||||||
if domain.startswith("_") or is_valid_ipv4(domain):
|
if self.chatmail_config.tls_cert_mode == "self":
|
||||||
transport["certificateChecks"] = "acceptInvalidCertificates"
|
transport["certificateChecks"] = "acceptInvalidCertificates"
|
||||||
return transport
|
return transport
|
||||||
|
|
||||||
@@ -347,8 +341,7 @@ class ChatmailACFactory:
|
|||||||
accounts = []
|
accounts = []
|
||||||
for _ in range(num):
|
for _ in range(num):
|
||||||
account = self.dc.add_account()
|
account = self.dc.add_account()
|
||||||
domain_deliverable = format_mail_domain(domain)
|
addr, password = self.gencreds(domain)
|
||||||
addr, password = self.gencreds(domain_deliverable)
|
|
||||||
if _is_ip(domain):
|
if _is_ip(domain):
|
||||||
# Use DCLOGIN scheme with explicit server hosts,
|
# Use DCLOGIN scheme with explicit server hosts,
|
||||||
# matching how madmail presents its addresses to users.
|
# matching how madmail presents its addresses to users.
|
||||||
@@ -424,9 +417,12 @@ class Remote:
|
|||||||
getjournal = "journalctl -f" if not logcmd else logcmd
|
getjournal = "journalctl -f" if not logcmd else logcmd
|
||||||
print(self.sshdomain)
|
print(self.sshdomain)
|
||||||
match self.sshdomain:
|
match self.sshdomain:
|
||||||
case "@local": command = []
|
case "@local":
|
||||||
case "localhost": command = []
|
command = []
|
||||||
case _: command = ["ssh", f"root@{self.sshdomain}"]
|
case "localhost":
|
||||||
|
command = []
|
||||||
|
case _:
|
||||||
|
command = ["ssh", f"root@{self.sshdomain}"]
|
||||||
[command.append(arg) for arg in getjournal.split()]
|
[command.append(arg) for arg in getjournal.split()]
|
||||||
popen = subprocess.Popen(
|
popen = subprocess.Popen(
|
||||||
command,
|
command,
|
||||||
|
|||||||
@@ -39,14 +39,6 @@ class TestCmdline:
|
|||||||
out, err = capsys.readouterr()
|
out, err = capsys.readouterr()
|
||||||
assert "deleting config file" in out.lower()
|
assert "deleting config file" in out.lower()
|
||||||
|
|
||||||
def test_dns_skip_on_ip(self, capsys, tmp_path, monkeypatch):
|
|
||||||
monkeypatch.delenv("CHATMAIL_INI", raising=False)
|
|
||||||
inipath = tmp_path / "chatmail.ini"
|
|
||||||
assert main(["init", "--config", str(inipath), "1.3.3.7"]) == 0
|
|
||||||
assert main(["dns", "--config", str(inipath)]) == 0
|
|
||||||
out, err = capsys.readouterr()
|
|
||||||
assert out == "[WARNING] 1.3.3.7 is not a domain, skipping DNS checks.\n"
|
|
||||||
|
|
||||||
|
|
||||||
def test_www_folder(example_config, tmp_path):
|
def test_www_folder(example_config, tmp_path):
|
||||||
reporoot = importlib.resources.files(__package__).joinpath("../../../../").resolve()
|
reporoot = importlib.resources.files(__package__).joinpath("../../../../").resolve()
|
||||||
|
|||||||
@@ -23,8 +23,7 @@ def make_host(*fact_pairs):
|
|||||||
if cls not in facts:
|
if cls not in facts:
|
||||||
registered = ", ".join(c.__name__ for c in facts)
|
registered = ", ".join(c.__name__ for c in facts)
|
||||||
raise LookupError(
|
raise LookupError(
|
||||||
f"unexpected get_fact({cls.__name__}); "
|
f"unexpected get_fact({cls.__name__}); only registered: {registered}"
|
||||||
f"only registered: {registered}"
|
|
||||||
)
|
)
|
||||||
return facts[cls]
|
return facts[cls]
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
from pathlib import Path
|
import importlib.resources
|
||||||
|
|
||||||
from cmdeploy.www import build_webpages
|
from cmdeploy.www import build_webpages
|
||||||
|
|
||||||
|
|
||||||
def test_build_webpages(tmp_path, make_config):
|
def test_build_webpages(tmp_path, make_config):
|
||||||
src_dir = (Path(__file__).resolve() / "../../../../../www/src").resolve()
|
pkgroot = importlib.resources.files("cmdeploy")
|
||||||
|
src_dir = pkgroot.joinpath("../../../www/src").resolve()
|
||||||
assert src_dir.exists(), src_dir
|
assert src_dir.exists(), src_dir
|
||||||
config = make_config("chat.example.org")
|
config = make_config("chat.example.org")
|
||||||
build_dir = tmp_path.joinpath("build")
|
build_dir = tmp_path.joinpath("build")
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import hashlib
|
import hashlib
|
||||||
|
import importlib.resources
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
@@ -36,7 +37,7 @@ def prepare_template(source):
|
|||||||
|
|
||||||
|
|
||||||
def get_paths(config) -> (Path, Path, Path):
|
def get_paths(config) -> (Path, Path, Path):
|
||||||
reporoot = (Path(__file__).resolve() / "../../../../").resolve()
|
reporoot = importlib.resources.files(__package__).joinpath("../../../").resolve()
|
||||||
www_path = Path(config.www_folder)
|
www_path = Path(config.www_folder)
|
||||||
# if www_folder was not set, use default directory
|
# if www_folder was not set, use default directory
|
||||||
if config.www_folder == "":
|
if config.www_folder == "":
|
||||||
@@ -132,7 +133,8 @@ def find_merge_conflict(src_dir) -> Path:
|
|||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
reporoot = (Path(__file__).resolve() / "../../../../").resolve()
|
path = importlib.resources.files(__package__)
|
||||||
|
reporoot = path.joinpath("../../../").resolve()
|
||||||
inipath = reporoot.joinpath("chatmail.ini")
|
inipath = reporoot.joinpath("chatmail.ini")
|
||||||
config = read_config(inipath)
|
config = read_config(inipath)
|
||||||
config.webdev = True
|
config.webdev = True
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ Minimal requirements and prerequisites
|
|||||||
|
|
||||||
You will need the following:
|
You will need the following:
|
||||||
|
|
||||||
|
- Control over a domain through a DNS provider of your choice.
|
||||||
|
|
||||||
- A Debian 12 **deployment server** with reachable SMTP/SUBMISSIONS/IMAPS/HTTPS ports.
|
- A Debian 12 **deployment server** with reachable SMTP/SUBMISSIONS/IMAPS/HTTPS ports.
|
||||||
IPv6 is encouraged if available. Chatmail relay servers only require
|
IPv6 is encouraged if available. Chatmail relay servers only require
|
||||||
1GB RAM, one CPU, and perhaps 10GB storage for a few thousand active
|
1GB RAM, one CPU, and perhaps 10GB storage for a few thousand active
|
||||||
@@ -26,11 +28,6 @@ You will need the following:
|
|||||||
(An ed25519 private key is required due to an `upstream bug in
|
(An ed25519 private key is required due to an `upstream bug in
|
||||||
paramiko <https://github.com/paramiko/paramiko/issues/2191>`_)
|
paramiko <https://github.com/paramiko/paramiko/issues/2191>`_)
|
||||||
|
|
||||||
- Control over a domain through a DNS provider of your choice
|
|
||||||
(there is experimental support for :ref:`DNS-less relays <iponly>`).
|
|
||||||
|
|
||||||
|
|
||||||
.. _setup:
|
|
||||||
|
|
||||||
Setup with ``scripts/cmdeploy``
|
Setup with ``scripts/cmdeploy``
|
||||||
-------------------------------------
|
-------------------------------------
|
||||||
|
|||||||
@@ -19,4 +19,3 @@ Contributions and feedback welcome through the https://github.com/chatmail/relay
|
|||||||
reverse_dns
|
reverse_dns
|
||||||
related
|
related
|
||||||
faq
|
faq
|
||||||
iponly
|
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
.. _iponly:
|
|
||||||
|
|
||||||
Hosting without DNS records
|
|
||||||
===========================
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
This option is experimental and might change without notice.
|
|
||||||
|
|
||||||
In case you don't have a domain,
|
|
||||||
for example in a local network,
|
|
||||||
you can run a chatmail relay with only an IPv4 address as well.
|
|
||||||
|
|
||||||
To deploy a relay without a domain,
|
|
||||||
run ``cmdeploy init`` with only the IPv4 address
|
|
||||||
during the :ref:`installation steps <setup>`,
|
|
||||||
for example ``cmdeploy init 13.12.23.42``.
|
|
||||||
|
|
||||||
Drawbacks
|
|
||||||
---------
|
|
||||||
|
|
||||||
- your transport encryption will only use self-signed TLS certificates,
|
|
||||||
which are vulnerable against MITM attacks.
|
|
||||||
the chatmail core's end-to-end encryption should suffice in most scenarios though.
|
|
||||||
|
|
||||||
- your messages will not be DKIM-signed;
|
|
||||||
experimentally, most chatmail relays accept non-DKIM-signed messages from IPv4-only relays,
|
|
||||||
but some relays might not accept messages from yours.
|
|
||||||
|
|
||||||
@@ -153,7 +153,6 @@ Chatmail relay dependency diagram
|
|||||||
autoconfig.xml --- dovecot;
|
autoconfig.xml --- dovecot;
|
||||||
postfix --- |10080|filtermail-outgoing;
|
postfix --- |10080|filtermail-outgoing;
|
||||||
postfix --- |10081|filtermail-incoming;
|
postfix --- |10081|filtermail-incoming;
|
||||||
postfix --- |10083|filtermail-transport;
|
|
||||||
filtermail-outgoing --- |10025 reinject|postfix;
|
filtermail-outgoing --- |10025 reinject|postfix;
|
||||||
filtermail-incoming --- |10026 reinject|postfix;
|
filtermail-incoming --- |10026 reinject|postfix;
|
||||||
dovecot --- |doveauth.socket|doveauth;
|
dovecot --- |doveauth.socket|doveauth;
|
||||||
@@ -296,7 +295,9 @@ ensured by ``filtermail`` proxy.
|
|||||||
TLS requirements
|
TLS requirements
|
||||||
~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Filtermail (used for delivery) requires a valid TLS.
|
Postfix is configured to require valid TLS by setting
|
||||||
|
`smtp_tls_security_level <https://www.postfix.org/postconf.5.html#smtp_tls_security_level>`_
|
||||||
|
to ``verify``.
|
||||||
|
|
||||||
You can test it by resolving ``MX`` records of your relay domain and
|
You can test it by resolving ``MX`` records of your relay domain and
|
||||||
then connecting to MX relays (e.g ``mx.example.org``) with
|
then connecting to MX relays (e.g ``mx.example.org``) with
|
||||||
|
|||||||
Reference in New Issue
Block a user