mirror of
https://github.com/chatmail/relay.git
synced 2026-05-23 06:18:03 +00:00
Compare commits
19 Commits
j4n/hpk-lx
...
hpk/lxc-ci
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
195c680455 | ||
|
|
a48c525455 | ||
|
|
2786b60658 | ||
|
|
fe46b573a6 | ||
|
|
674e496a53 | ||
|
|
86e5708709 | ||
|
|
04ac2cf700 | ||
|
|
e2ec0cf2c5 | ||
|
|
ee9d54f7d6 | ||
|
|
1604321d5b | ||
|
|
4ab04fa6c4 | ||
|
|
8b6829b906 | ||
|
|
cf2cb57cca | ||
|
|
693c3f8555 | ||
|
|
371efdfafb | ||
|
|
23765a5ed8 | ||
|
|
0adeefbdd7 | ||
|
|
624838eedd | ||
|
|
1abdc407af |
56
.github/workflows/ci.yaml
vendored
56
.github/workflows/ci.yaml
vendored
@@ -15,7 +15,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
- name: download filtermail
|
- name: download filtermail
|
||||||
run: curl -L https://github.com/chatmail/filtermail/releases/download/v0.6.0/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.5.2/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
|
||||||
@@ -38,5 +38,55 @@ jobs:
|
|||||||
- name: run deploy-chatmail offline tests
|
- name: run deploy-chatmail offline tests
|
||||||
run: pytest --pyargs cmdeploy
|
run: pytest --pyargs cmdeploy
|
||||||
|
|
||||||
# all other cmdeploy commands require a staging server
|
lxc-test:
|
||||||
# see https://github.com/deltachat/chatmail/issues/100
|
name: LXC deploy and test
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
timeout-minutes: 30
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
|
|
||||||
|
- name: install incus
|
||||||
|
run: |
|
||||||
|
# zabbly is the official incus community packages source
|
||||||
|
curl -fsSL https://pkgs.zabbly.com/key.asc \
|
||||||
|
| sudo gpg --dearmor -o /etc/apt/keyrings/zabbly.gpg
|
||||||
|
sudo sh -c 'cat <<EOF > /etc/apt/sources.list.d/zabbly-incus-stable.sources
|
||||||
|
Enabled: yes
|
||||||
|
Types: deb
|
||||||
|
URIs: https://pkgs.zabbly.com/incus/stable
|
||||||
|
Suites: $(. /etc/os-release && echo ${VERSION_CODENAME})
|
||||||
|
Components: main
|
||||||
|
Architectures: $(dpkg --print-architecture)
|
||||||
|
Signed-By: /etc/apt/keyrings/zabbly.gpg
|
||||||
|
EOF'
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y incus
|
||||||
|
|
||||||
|
- name: initialise incus
|
||||||
|
run: |
|
||||||
|
sudo systemctl stop docker.socket docker || true
|
||||||
|
sudo iptables -P FORWARD ACCEPT
|
||||||
|
sudo sysctl -w fs.inotify.max_user_instances=65535
|
||||||
|
sudo sysctl -w fs.inotify.max_user_watches=65535
|
||||||
|
sudo incus admin init --minimal
|
||||||
|
sudo usermod -aG incus-admin "$USER"
|
||||||
|
|
||||||
|
- name: initenv
|
||||||
|
run: scripts/initenv.sh
|
||||||
|
|
||||||
|
- name: append venv/bin to PATH
|
||||||
|
run: echo venv/bin >>$GITHUB_PATH
|
||||||
|
|
||||||
|
- name: lxc-test
|
||||||
|
run: sg incus-admin -c 'cmdeploy lxc-test'
|
||||||
|
|
||||||
|
- name: export images for cache
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
for alias in localchat-base localchat-ns localchat-test0 localchat-test1; do
|
||||||
|
if ! [ -f /tmp/$alias.tar.gz ]; then
|
||||||
|
sg incus-admin -c "incus image export $alias /tmp/$alias" || true
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
;; Zone file for staging-ipv4.testrun.org
|
|
||||||
|
|
||||||
$ORIGIN staging-ipv4.testrun.org.
|
|
||||||
$TTL 300
|
|
||||||
|
|
||||||
@ IN SOA ns.testrun.org. root.nine.testrun.org (
|
|
||||||
2023010101 ; Serial
|
|
||||||
7200 ; Refresh
|
|
||||||
3600 ; Retry
|
|
||||||
1209600 ; Expire
|
|
||||||
3600 ; Negative response caching TTL
|
|
||||||
)
|
|
||||||
|
|
||||||
;; Nameservers.
|
|
||||||
@ IN NS ns.testrun.org.
|
|
||||||
|
|
||||||
;; DNS records.
|
|
||||||
@ IN A 37.27.95.249
|
|
||||||
mta-sts.staging-ipv4.testrun.org. CNAME staging-ipv4.testrun.org.
|
|
||||||
www.staging-ipv4.testrun.org. CNAME staging-ipv4.testrun.org.
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
;; Zone file for staging2.testrun.org
|
|
||||||
|
|
||||||
$ORIGIN staging2.testrun.org.
|
|
||||||
$TTL 300
|
|
||||||
|
|
||||||
@ IN SOA ns.testrun.org. root.nine.testrun.org (
|
|
||||||
2023010101 ; Serial
|
|
||||||
7200 ; Refresh
|
|
||||||
3600 ; Retry
|
|
||||||
1209600 ; Expire
|
|
||||||
3600 ; Negative response caching TTL
|
|
||||||
)
|
|
||||||
|
|
||||||
;; Nameservers.
|
|
||||||
@ IN NS ns.testrun.org.
|
|
||||||
|
|
||||||
;; DNS records.
|
|
||||||
@ IN A 37.27.24.139
|
|
||||||
mta-sts.staging2.testrun.org. CNAME staging2.testrun.org.
|
|
||||||
www.staging2.testrun.org. CNAME staging2.testrun.org.
|
|
||||||
|
|
||||||
104
.github/workflows/test-and-deploy-ipv4only.yaml
vendored
104
.github/workflows/test-and-deploy-ipv4only.yaml
vendored
@@ -1,104 +0,0 @@
|
|||||||
name: deploy on staging-ipv4.testrun.org, and run tests
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
pull_request:
|
|
||||||
paths-ignore:
|
|
||||||
- 'scripts/**'
|
|
||||||
- '**/README.md'
|
|
||||||
- 'CHANGELOG.md'
|
|
||||||
- 'LICENSE'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deploy:
|
|
||||||
name: deploy on staging-ipv4.testrun.org, and run tests
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 30
|
|
||||||
environment:
|
|
||||||
name: staging-ipv4.testrun.org
|
|
||||||
url: https://staging-ipv4.testrun.org/
|
|
||||||
concurrency: staging-ipv4.testrun.org
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: prepare SSH
|
|
||||||
run: |
|
|
||||||
mkdir ~/.ssh
|
|
||||||
echo "${{ secrets.STAGING_SSH_KEY }}" >> ~/.ssh/id_ed25519
|
|
||||||
chmod 600 ~/.ssh/id_ed25519
|
|
||||||
ssh-keyscan staging-ipv4.testrun.org > ~/.ssh/known_hosts
|
|
||||||
# save previous acme & dkim state
|
|
||||||
rsync -avz root@staging-ipv4.testrun.org:/var/lib/acme acme-ipv4 || true
|
|
||||||
rsync -avz root@staging-ipv4.testrun.org:/etc/dkimkeys dkimkeys-ipv4 || true
|
|
||||||
# store previous acme & dkim state on ns.testrun.org, if it contains useful certs
|
|
||||||
if [ -f dkimkeys-ipv4/dkimkeys/opendkim.private ]; then rsync -avz -e "ssh -o StrictHostKeyChecking=accept-new" dkimkeys-ipv4 root@ns.testrun.org:/tmp/ || true; fi
|
|
||||||
if [ "$(ls -A acme-ipv4/acme/certs)" ]; then rsync -avz -e "ssh -o StrictHostKeyChecking=accept-new" acme-ipv4 root@ns.testrun.org:/tmp/ || true; fi
|
|
||||||
# make sure CAA record isn't set
|
|
||||||
scp -o StrictHostKeyChecking=accept-new .github/workflows/staging-ipv4.testrun.org-default.zone root@ns.testrun.org:/etc/nsd/staging-ipv4.testrun.org.zone
|
|
||||||
ssh root@ns.testrun.org sed -i '/CAA/d' /etc/nsd/staging-ipv4.testrun.org.zone
|
|
||||||
ssh root@ns.testrun.org nsd-checkzone staging-ipv4.testrun.org /etc/nsd/staging-ipv4.testrun.org.zone
|
|
||||||
ssh root@ns.testrun.org systemctl reload nsd
|
|
||||||
|
|
||||||
- name: rebuild staging-ipv4.testrun.org to have a clean VPS
|
|
||||||
run: |
|
|
||||||
curl -X POST \
|
|
||||||
-H "Authorization: Bearer ${{ secrets.HETZNER_API_TOKEN }}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"image":"debian-12"}' \
|
|
||||||
"https://api.hetzner.cloud/v1/servers/${{ secrets.STAGING_IPV4_SERVER_ID }}/actions/rebuild"
|
|
||||||
|
|
||||||
- run: scripts/initenv.sh
|
|
||||||
|
|
||||||
- name: append venv/bin to PATH
|
|
||||||
run: echo venv/bin >>$GITHUB_PATH
|
|
||||||
|
|
||||||
- name: upload TLS cert after rebuilding
|
|
||||||
run: |
|
|
||||||
echo " --- wait until staging-ipv4.testrun.org VPS is rebuilt --- "
|
|
||||||
rm ~/.ssh/known_hosts
|
|
||||||
while ! ssh -o ConnectTimeout=180 -o StrictHostKeyChecking=accept-new -v root@staging-ipv4.testrun.org id -u ; do sleep 1 ; done
|
|
||||||
ssh -o StrictHostKeyChecking=accept-new -v root@staging-ipv4.testrun.org id -u
|
|
||||||
# download acme & dkim state from ns.testrun.org
|
|
||||||
rsync -e "ssh -o StrictHostKeyChecking=accept-new" -avz root@ns.testrun.org:/tmp/acme-ipv4/acme acme-restore || true
|
|
||||||
rsync -avz root@ns.testrun.org:/tmp/dkimkeys-ipv4/dkimkeys dkimkeys-restore || true
|
|
||||||
# restore acme & dkim state to staging2.testrun.org
|
|
||||||
rsync -avz acme-restore/acme root@staging-ipv4.testrun.org:/var/lib/ || true
|
|
||||||
rsync -avz dkimkeys-restore/dkimkeys root@staging-ipv4.testrun.org:/etc/ || true
|
|
||||||
ssh -o StrictHostKeyChecking=accept-new -v root@staging-ipv4.testrun.org chown root:root -R /var/lib/acme || true
|
|
||||||
|
|
||||||
- name: run deploy-chatmail offline tests
|
|
||||||
run: pytest --pyargs cmdeploy
|
|
||||||
|
|
||||||
- name: setup dependencies
|
|
||||||
run: |
|
|
||||||
ssh root@staging-ipv4.testrun.org apt update
|
|
||||||
ssh root@staging-ipv4.testrun.org apt install -y git python3.11-venv python3-dev gcc
|
|
||||||
ssh root@staging-ipv4.testrun.org git clone https://github.com/chatmail/relay
|
|
||||||
ssh root@staging-ipv4.testrun.org "cd relay && git checkout " ${{ github.head_ref }}
|
|
||||||
ssh root@staging-ipv4.testrun.org "cd relay && scripts/initenv.sh"
|
|
||||||
|
|
||||||
- name: initialize config
|
|
||||||
run: |
|
|
||||||
ssh root@staging-ipv4.testrun.org "cd relay && scripts/cmdeploy init staging-ipv4.testrun.org"
|
|
||||||
ssh root@staging-ipv4.testrun.org "sed -i 's#disable_ipv6 = False#disable_ipv6 = True#' relay/chatmail.ini"
|
|
||||||
ssh root@staging-ipv4.testrun.org "sed -i 's/#\s*mtail_address/mtail_address/' relay/chatmail.ini"
|
|
||||||
|
|
||||||
- run: ssh root@staging-ipv4.testrun.org "cd relay && scripts/cmdeploy run --verbose --skip-dns-check --ssh-host localhost"
|
|
||||||
|
|
||||||
- name: set DNS entries
|
|
||||||
run: |
|
|
||||||
ssh root@staging-ipv4.testrun.org "cd relay && scripts/cmdeploy dns --zonefile staging-generated.zone --ssh-host localhost"
|
|
||||||
ssh root@staging-ipv4.testrun.org cat relay/staging-generated.zone >> .github/workflows/staging-ipv4.testrun.org-default.zone
|
|
||||||
cat .github/workflows/staging-ipv4.testrun.org-default.zone
|
|
||||||
scp .github/workflows/staging-ipv4.testrun.org-default.zone root@ns.testrun.org:/etc/nsd/staging-ipv4.testrun.org.zone
|
|
||||||
ssh root@ns.testrun.org nsd-checkzone staging-ipv4.testrun.org /etc/nsd/staging-ipv4.testrun.org.zone
|
|
||||||
ssh root@ns.testrun.org systemctl reload nsd
|
|
||||||
|
|
||||||
- name: cmdeploy test
|
|
||||||
run: ssh root@staging-ipv4.testrun.org "cd relay && CHATMAIL_DOMAIN2=ci-chatmail.testrun.org scripts/cmdeploy test --slow --ssh-host localhost"
|
|
||||||
|
|
||||||
- name: cmdeploy dns
|
|
||||||
run: ssh root@staging-ipv4.testrun.org "cd relay && scripts/cmdeploy dns -v --ssh-host localhost"
|
|
||||||
|
|
||||||
97
.github/workflows/test-and-deploy.yaml
vendored
97
.github/workflows/test-and-deploy.yaml
vendored
@@ -1,97 +0,0 @@
|
|||||||
name: deploy on staging2.testrun.org, and run tests
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
pull_request:
|
|
||||||
paths-ignore:
|
|
||||||
- 'scripts/**'
|
|
||||||
- '**/README.md'
|
|
||||||
- 'CHANGELOG.md'
|
|
||||||
- 'LICENSE'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deploy:
|
|
||||||
name: deploy on staging2.testrun.org, and run tests
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 30
|
|
||||||
environment:
|
|
||||||
name: staging2.testrun.org
|
|
||||||
url: https://staging2.testrun.org/
|
|
||||||
concurrency: staging2.testrun.org
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: prepare SSH
|
|
||||||
run: |
|
|
||||||
mkdir ~/.ssh
|
|
||||||
echo "${{ secrets.STAGING_SSH_KEY }}" >> ~/.ssh/id_ed25519
|
|
||||||
chmod 600 ~/.ssh/id_ed25519
|
|
||||||
ssh-keyscan staging2.testrun.org > ~/.ssh/known_hosts
|
|
||||||
# save previous acme & dkim state
|
|
||||||
rsync -avz root@staging2.testrun.org:/var/lib/acme . || true
|
|
||||||
rsync -avz root@staging2.testrun.org:/etc/dkimkeys . || true
|
|
||||||
# store previous acme & dkim state on ns.testrun.org, if it contains useful certs
|
|
||||||
if [ -f dkimkeys/opendkim.private ]; then rsync -avz -e "ssh -o StrictHostKeyChecking=accept-new" dkimkeys root@ns.testrun.org:/tmp/ || true; fi
|
|
||||||
if [ "$(ls -A acme/certs)" ]; then rsync -avz -e "ssh -o StrictHostKeyChecking=accept-new" acme root@ns.testrun.org:/tmp/ || true; fi
|
|
||||||
# make sure CAA record isn't set
|
|
||||||
scp -o StrictHostKeyChecking=accept-new .github/workflows/staging.testrun.org-default.zone root@ns.testrun.org:/etc/nsd/staging2.testrun.org.zone
|
|
||||||
ssh root@ns.testrun.org sed -i '/CAA/d' /etc/nsd/staging2.testrun.org.zone
|
|
||||||
ssh root@ns.testrun.org nsd-checkzone staging2.testrun.org /etc/nsd/staging2.testrun.org.zone
|
|
||||||
ssh root@ns.testrun.org systemctl reload nsd
|
|
||||||
|
|
||||||
- name: rebuild staging2.testrun.org to have a clean VPS
|
|
||||||
run: |
|
|
||||||
curl -X POST \
|
|
||||||
-H "Authorization: Bearer ${{ secrets.HETZNER_API_TOKEN }}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"image":"debian-12"}' \
|
|
||||||
"https://api.hetzner.cloud/v1/servers/${{ secrets.STAGING_SERVER_ID }}/actions/rebuild"
|
|
||||||
|
|
||||||
- run: scripts/initenv.sh
|
|
||||||
|
|
||||||
- name: append venv/bin to PATH
|
|
||||||
run: echo venv/bin >>$GITHUB_PATH
|
|
||||||
|
|
||||||
- name: upload TLS cert after rebuilding
|
|
||||||
run: |
|
|
||||||
echo " --- wait until staging2.testrun.org VPS is rebuilt --- "
|
|
||||||
rm ~/.ssh/known_hosts
|
|
||||||
while ! ssh -o ConnectTimeout=180 -o StrictHostKeyChecking=accept-new -v root@staging2.testrun.org id -u ; do sleep 1 ; done
|
|
||||||
ssh -o StrictHostKeyChecking=accept-new -v root@staging2.testrun.org id -u
|
|
||||||
# download acme & dkim state from ns.testrun.org
|
|
||||||
rsync -e "ssh -o StrictHostKeyChecking=accept-new" -avz root@ns.testrun.org:/tmp/acme acme-restore || true
|
|
||||||
rsync -avz root@ns.testrun.org:/tmp/dkimkeys dkimkeys-restore || true
|
|
||||||
# restore acme & dkim state to staging2.testrun.org
|
|
||||||
rsync -avz acme-restore/acme root@staging2.testrun.org:/var/lib/ || true
|
|
||||||
rsync -avz dkimkeys-restore/dkimkeys root@staging2.testrun.org:/etc/ || true
|
|
||||||
ssh -o StrictHostKeyChecking=accept-new -v root@staging2.testrun.org chown root:root -R /var/lib/acme || true
|
|
||||||
|
|
||||||
- name: add hpk42 key to staging server
|
|
||||||
run: ssh root@staging2.testrun.org 'curl -s https://github.com/hpk42.keys >> .ssh/authorized_keys'
|
|
||||||
|
|
||||||
- name: run deploy-chatmail offline tests
|
|
||||||
run: pytest --pyargs cmdeploy
|
|
||||||
|
|
||||||
- run: |
|
|
||||||
cmdeploy init staging2.testrun.org
|
|
||||||
sed -i 's/#\s*mtail_address/mtail_address/' chatmail.ini
|
|
||||||
|
|
||||||
- run: cmdeploy run --verbose --skip-dns-check
|
|
||||||
|
|
||||||
- name: set DNS entries
|
|
||||||
run: |
|
|
||||||
cmdeploy dns --zonefile staging-generated.zone --verbose
|
|
||||||
cat staging-generated.zone >> .github/workflows/staging.testrun.org-default.zone
|
|
||||||
cat .github/workflows/staging.testrun.org-default.zone
|
|
||||||
scp .github/workflows/staging.testrun.org-default.zone root@ns.testrun.org:/etc/nsd/staging2.testrun.org.zone
|
|
||||||
ssh root@ns.testrun.org nsd-checkzone staging2.testrun.org /etc/nsd/staging2.testrun.org.zone
|
|
||||||
ssh root@ns.testrun.org systemctl reload nsd
|
|
||||||
|
|
||||||
- name: cmdeploy test
|
|
||||||
run: CHATMAIL_DOMAIN2=ci-chatmail.testrun.org cmdeploy test --slow
|
|
||||||
|
|
||||||
- name: cmdeploy dns
|
|
||||||
run: cmdeploy dns -v
|
|
||||||
|
|
||||||
@@ -6,7 +6,10 @@ build-backend = "setuptools.build_meta"
|
|||||||
name = "chatmaild"
|
name = "chatmaild"
|
||||||
version = "0.3"
|
version = "0.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"aiosmtpd",
|
||||||
"iniconfig",
|
"iniconfig",
|
||||||
|
"deltachat-rpc-server",
|
||||||
|
"deltachat-rpc-client",
|
||||||
"filelock",
|
"filelock",
|
||||||
"requests",
|
"requests",
|
||||||
"crypt-r >= 3.13.1 ; python_version >= '3.11'",
|
"crypt-r >= 3.13.1 ; python_version >= '3.11'",
|
||||||
@@ -67,7 +70,6 @@ commands =
|
|||||||
deps = pytest
|
deps = pytest
|
||||||
pdbpp
|
pdbpp
|
||||||
pytest-localserver
|
pytest-localserver
|
||||||
aiosmtpd
|
|
||||||
execnet
|
execnet
|
||||||
commands = pytest -v -rsXx {posargs}
|
commands = pytest -v -rsXx {posargs}
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ dependencies = [
|
|||||||
"pillow",
|
"pillow",
|
||||||
"qrcode",
|
"qrcode",
|
||||||
"markdown",
|
"markdown",
|
||||||
|
"pytest",
|
||||||
"setuptools>=68",
|
"setuptools>=68",
|
||||||
"termcolor",
|
"termcolor",
|
||||||
"build",
|
"build",
|
||||||
@@ -20,7 +21,6 @@ dependencies = [
|
|||||||
"execnet",
|
"execnet",
|
||||||
"imap_tools",
|
"imap_tools",
|
||||||
"deltachat-rpc-client",
|
"deltachat-rpc-client",
|
||||||
"deltachat-rpc-server",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
|
|||||||
@@ -437,7 +437,7 @@ def main(args=None):
|
|||||||
if args.func is None:
|
if args.func is None:
|
||||||
return parser.parse_args(["-h"])
|
return parser.parse_args(["-h"])
|
||||||
|
|
||||||
out = Out(verbosity=args.verbose)
|
out = Out(sepchar="\u2501", verbosity=args.verbose)
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
|
|
||||||
if args.inipath is not None and args.func.__name__ not in ("init_cmd", "fmt_cmd"):
|
if args.inipath is not None and args.func.__name__ not in ("init_cmd", "fmt_cmd"):
|
||||||
|
|||||||
@@ -463,9 +463,8 @@ class ChatmailDeployer(Deployer):
|
|||||||
("iroh", None, None),
|
("iroh", None, None),
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, config):
|
def __init__(self, mail_domain):
|
||||||
self.config = config
|
self.mail_domain = mail_domain
|
||||||
self.mail_domain = config.mail_domain
|
|
||||||
|
|
||||||
def install(self):
|
def install(self):
|
||||||
files.put(
|
files.put(
|
||||||
@@ -488,19 +487,12 @@ class ChatmailDeployer(Deployer):
|
|||||||
name="Install rsync",
|
name="Install rsync",
|
||||||
packages=["rsync"],
|
packages=["rsync"],
|
||||||
)
|
)
|
||||||
|
apt.packages(
|
||||||
def configure(self):
|
name="Ensure cron is installed",
|
||||||
# Ensure the per-domain mailbox directory exists before
|
packages=["cron"],
|
||||||
# chatmail-metadata starts (it crashes without it).
|
|
||||||
files.directory(
|
|
||||||
name="Ensure vmail mailbox directory exists",
|
|
||||||
path=f"/home/vmail/mail/{self.mail_domain}",
|
|
||||||
user="vmail",
|
|
||||||
group="vmail",
|
|
||||||
mode="700",
|
|
||||||
present=True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def configure(self):
|
||||||
# This file is used by auth proxy.
|
# This file is used by auth proxy.
|
||||||
# https://wiki.debian.org/EtcMailName
|
# https://wiki.debian.org/EtcMailName
|
||||||
server.shell(
|
server.shell(
|
||||||
@@ -510,15 +502,6 @@ class ChatmailDeployer(Deployer):
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
files.directory(
|
|
||||||
name=f"Ensure mailboxes directory {self.config.mailboxes_dir} exists",
|
|
||||||
path=str(self.config.mailboxes_dir),
|
|
||||||
user="vmail",
|
|
||||||
group="vmail",
|
|
||||||
mode="700",
|
|
||||||
present=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class FcgiwrapDeployer(Deployer):
|
class FcgiwrapDeployer(Deployer):
|
||||||
def install(self):
|
def install(self):
|
||||||
@@ -637,7 +620,7 @@ def deploy_chatmail(config_path: Path, disable_mail: bool, website_only: bool) -
|
|||||||
tls_deployer = get_tls_deployer(config, mail_domain)
|
tls_deployer = get_tls_deployer(config, mail_domain)
|
||||||
|
|
||||||
all_deployers = [
|
all_deployers = [
|
||||||
ChatmailDeployer(config),
|
ChatmailDeployer(mail_domain),
|
||||||
LegacyRemoveDeployer(),
|
LegacyRemoveDeployer(),
|
||||||
FiltermailDeployer(),
|
FiltermailDeployer(),
|
||||||
JournaldDeployer(),
|
JournaldDeployer(),
|
||||||
|
|||||||
@@ -47,40 +47,33 @@ def get_filled_zone_file(remote_data):
|
|||||||
remote_data["sts_id"] = datetime.datetime.now().strftime("%Y%m%d%H%M")
|
remote_data["sts_id"] = datetime.datetime.now().strftime("%Y%m%d%H%M")
|
||||||
|
|
||||||
d = remote_data["mail_domain"]
|
d = remote_data["mail_domain"]
|
||||||
|
|
||||||
def rec(name, rtype, rdata, ttl=3600):
|
|
||||||
return f"{name:<40} {ttl:<6} IN {rtype:<5} {rdata}"
|
|
||||||
|
|
||||||
lines = ["; Required DNS entries"]
|
lines = ["; Required DNS entries"]
|
||||||
if remote_data.get("A"):
|
if remote_data.get("A"):
|
||||||
lines.append(rec(f"{d}.", "A", remote_data["A"]))
|
lines.append(f"{d}. 3600 IN A {remote_data['A']}")
|
||||||
if remote_data.get("AAAA"):
|
if remote_data.get("AAAA"):
|
||||||
lines.append(rec(f"{d}.", "AAAA", remote_data["AAAA"]))
|
lines.append(f"{d}. 3600 IN AAAA {remote_data['AAAA']}")
|
||||||
lines.append(rec(f"{d}.", "MX", f"10 {d}."))
|
lines.append(f"{d}. 3600 IN MX 10 {d}.")
|
||||||
if remote_data.get("strict_tls"):
|
if remote_data.get("strict_tls"):
|
||||||
lines.append(
|
lines.append(
|
||||||
rec(f"_mta-sts.{d}.", "TXT", f'"v=STSv1; id={remote_data["sts_id"]}"')
|
f'_mta-sts.{d}. 3600 IN TXT "v=STSv1; id={remote_data["sts_id"]}"'
|
||||||
)
|
)
|
||||||
lines.append(rec(f"mta-sts.{d}.", "CNAME", f"{d}."))
|
lines.append(f"mta-sts.{d}. 3600 IN CNAME {d}.")
|
||||||
lines.append(rec(f"www.{d}.", "CNAME", f"{d}."))
|
lines.append(f"www.{d}. 3600 IN CNAME {d}.")
|
||||||
lines.append(remote_data["dkim_entry"])
|
lines.append(remote_data["dkim_entry"])
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append("; Recommended DNS entries")
|
lines.append("; Recommended DNS entries")
|
||||||
lines.append(rec(f"{d}.", "TXT", '"v=spf1 a ~all"'))
|
lines.append(f'{d}. 3600 IN TXT "v=spf1 a ~all"')
|
||||||
lines.append(rec(f"_dmarc.{d}.", "TXT", '"v=DMARC1;p=reject;adkim=s;aspf=s"'))
|
lines.append(f'_dmarc.{d}. 3600 IN TXT "v=DMARC1;p=reject;adkim=s;aspf=s"')
|
||||||
if remote_data.get("acme_account_url"):
|
if remote_data.get("acme_account_url"):
|
||||||
lines.append(
|
lines.append(
|
||||||
rec(
|
f"{d}. 3600 IN CAA 0 issue"
|
||||||
f"{d}.",
|
f' "letsencrypt.org;accounturi={remote_data["acme_account_url"]}"'
|
||||||
"CAA",
|
|
||||||
f'0 issue "letsencrypt.org;accounturi={remote_data["acme_account_url"]}"',
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
lines.append(rec(f"_adsp._domainkey.{d}.", "TXT", '"dkim=discardable"'))
|
lines.append(f'_adsp._domainkey.{d}. 3600 IN TXT "dkim=discardable"')
|
||||||
lines.append(rec(f"_submission._tcp.{d}.", "SRV", f"0 1 587 {d}."))
|
lines.append(f"_submission._tcp.{d}. 3600 IN SRV 0 1 587 {d}.")
|
||||||
lines.append(rec(f"_submissions._tcp.{d}.", "SRV", f"0 1 465 {d}."))
|
lines.append(f"_submissions._tcp.{d}. 3600 IN SRV 0 1 465 {d}.")
|
||||||
lines.append(rec(f"_imap._tcp.{d}.", "SRV", f"0 1 143 {d}."))
|
lines.append(f"_imap._tcp.{d}. 3600 IN SRV 0 1 143 {d}.")
|
||||||
lines.append(rec(f"_imaps._tcp.{d}.", "SRV", f"0 1 993 {d}."))
|
lines.append(f"_imaps._tcp.{d}. 3600 IN SRV 0 1 993 {d}.")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import urllib.request
|
|||||||
|
|
||||||
from chatmaild.config import Config
|
from chatmaild.config import Config
|
||||||
from pyinfra import host
|
from pyinfra import host
|
||||||
from pyinfra.facts.deb import DebPackages
|
|
||||||
from pyinfra.facts.server import Arch, Command, Sysctl
|
from pyinfra.facts.server import Arch, Command, Sysctl
|
||||||
|
from pyinfra.facts.systemd import SystemdEnabled
|
||||||
from pyinfra.operations import apt, files, server, systemd
|
from pyinfra.operations import apt, files, server, systemd
|
||||||
|
|
||||||
from cmdeploy.basedeploy import (
|
from cmdeploy.basedeploy import (
|
||||||
@@ -12,19 +12,9 @@ from cmdeploy.basedeploy import (
|
|||||||
blocked_service_startup,
|
blocked_service_startup,
|
||||||
configure_remote_units,
|
configure_remote_units,
|
||||||
get_resource,
|
get_resource,
|
||||||
|
has_systemd,
|
||||||
)
|
)
|
||||||
|
|
||||||
DOVECOT_VERSION = "2.3.21+dfsg1-3"
|
|
||||||
|
|
||||||
DOVECOT_SHA256 = {
|
|
||||||
("core", "amd64"): "dd060706f52a306fa863d874717210b9fe10536c824afe1790eec247ded5b27d",
|
|
||||||
("core", "arm64"): "e7548e8a82929722e973629ecc40fcfa886894cef3db88f23535149e7f730dc9",
|
|
||||||
("imapd", "amd64"): "8d8dc6fc00bbb6cdb25d345844f41ce2f1c53f764b79a838eb2a03103eebfa86",
|
|
||||||
("imapd", "arm64"): "178fa877ddd5df9930e8308b518f4b07df10e759050725f8217a0c1fb3fd707f",
|
|
||||||
("lmtpd", "amd64"): "2f69ba5e35363de50962d42cccbfe4ed8495265044e244007d7ccddad77513ab",
|
|
||||||
("lmtpd", "arm64"): "89f52fb36524f5877a177dff4a713ba771fd3f91f22ed0af7238d495e143b38f",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class DovecotDeployer(Deployer):
|
class DovecotDeployer(Deployer):
|
||||||
daemon_reload = False
|
daemon_reload = False
|
||||||
@@ -36,22 +26,13 @@ class DovecotDeployer(Deployer):
|
|||||||
|
|
||||||
def install(self):
|
def install(self):
|
||||||
arch = host.get_fact(Arch)
|
arch = host.get_fact(Arch)
|
||||||
|
if has_systemd() and "dovecot.service" in host.get_fact(SystemdEnabled):
|
||||||
|
return # already installed and running
|
||||||
|
|
||||||
with blocked_service_startup():
|
with blocked_service_startup():
|
||||||
debs = []
|
_install_dovecot_package("core", arch)
|
||||||
for pkg in ("core", "imapd", "lmtpd"):
|
_install_dovecot_package("imapd", arch)
|
||||||
deb = _download_dovecot_package(pkg, arch)
|
_install_dovecot_package("lmtpd", arch)
|
||||||
if deb:
|
|
||||||
debs.append(deb)
|
|
||||||
if debs:
|
|
||||||
deb_list = " ".join(debs)
|
|
||||||
server.shell(
|
|
||||||
name="Install dovecot packages",
|
|
||||||
commands=[
|
|
||||||
f"dpkg --force-confdef --force-confold -i {deb_list} 2> /dev/null || true",
|
|
||||||
"DEBIAN_FRONTEND=noninteractive apt-get -y --fix-broken install",
|
|
||||||
f"dpkg --force-confdef --force-confold -i {deb_list}",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
def configure(self):
|
def configure(self):
|
||||||
configure_remote_units(self.config.mail_domain, self.units)
|
configure_remote_units(self.config.mail_domain, self.units)
|
||||||
@@ -84,37 +65,40 @@ def _pick_url(primary, fallback):
|
|||||||
return fallback
|
return fallback
|
||||||
|
|
||||||
|
|
||||||
def _download_dovecot_package(package: str, arch: str):
|
def _install_dovecot_package(package: str, arch: str):
|
||||||
"""Download a dovecot .deb if needed, return its path (or None)."""
|
|
||||||
arch = "amd64" if arch == "x86_64" else arch
|
arch = "amd64" if arch == "x86_64" else arch
|
||||||
arch = "arm64" if arch == "aarch64" else arch
|
arch = "arm64" if arch == "aarch64" else arch
|
||||||
|
primary_url = f"https://download.delta.chat/dovecot/dovecot-{package}_2.3.21%2Bdfsg1-3_{arch}.deb"
|
||||||
pkg_name = f"dovecot-{package}"
|
fallback_url = f"https://github.com/chatmail/dovecot/releases/download/upstream%2F2.3.21%2Bdfsg1/dovecot-{package}_2.3.21%2Bdfsg1-3_{arch}.deb"
|
||||||
sha256 = DOVECOT_SHA256.get((package, arch))
|
|
||||||
if sha256 is None:
|
|
||||||
apt.packages(packages=[pkg_name])
|
|
||||||
return None
|
|
||||||
|
|
||||||
installed_versions = host.get_fact(DebPackages).get(pkg_name, [])
|
|
||||||
if DOVECOT_VERSION in installed_versions:
|
|
||||||
return None
|
|
||||||
|
|
||||||
url_version = DOVECOT_VERSION.replace("+", "%2B")
|
|
||||||
deb_base = f"{pkg_name}_{url_version}_{arch}.deb"
|
|
||||||
primary_url = f"https://download.delta.chat/dovecot/{deb_base}"
|
|
||||||
fallback_url = f"https://github.com/chatmail/dovecot/releases/download/upstream%2F{url_version}/{deb_base}"
|
|
||||||
url = _pick_url(primary_url, fallback_url)
|
url = _pick_url(primary_url, fallback_url)
|
||||||
deb_filename = f"/root/{deb_base}"
|
deb_filename = "/root/" + url.split("/")[-1]
|
||||||
|
|
||||||
|
match (package, arch):
|
||||||
|
case ("core", "amd64"):
|
||||||
|
sha256 = "dd060706f52a306fa863d874717210b9fe10536c824afe1790eec247ded5b27d"
|
||||||
|
case ("core", "arm64"):
|
||||||
|
sha256 = "e7548e8a82929722e973629ecc40fcfa886894cef3db88f23535149e7f730dc9"
|
||||||
|
case ("imapd", "amd64"):
|
||||||
|
sha256 = "8d8dc6fc00bbb6cdb25d345844f41ce2f1c53f764b79a838eb2a03103eebfa86"
|
||||||
|
case ("imapd", "arm64"):
|
||||||
|
sha256 = "178fa877ddd5df9930e8308b518f4b07df10e759050725f8217a0c1fb3fd707f"
|
||||||
|
case ("lmtpd", "amd64"):
|
||||||
|
sha256 = "2f69ba5e35363de50962d42cccbfe4ed8495265044e244007d7ccddad77513ab"
|
||||||
|
case ("lmtpd", "arm64"):
|
||||||
|
sha256 = "89f52fb36524f5877a177dff4a713ba771fd3f91f22ed0af7238d495e143b38f"
|
||||||
|
case _:
|
||||||
|
apt.packages(packages=[f"dovecot-{package}"])
|
||||||
|
return
|
||||||
|
|
||||||
files.download(
|
files.download(
|
||||||
name=f"Download {pkg_name}",
|
name=f"Download dovecot-{package}",
|
||||||
src=url,
|
src=url,
|
||||||
dest=deb_filename,
|
dest=deb_filename,
|
||||||
sha256sum=sha256,
|
sha256sum=sha256,
|
||||||
cache_time=60 * 60 * 24 * 365 * 10, # never redownload the package
|
cache_time=60 * 60 * 24 * 365 * 10, # never redownload the package
|
||||||
)
|
)
|
||||||
|
|
||||||
return deb_filename
|
apt.deb(name=f"Install dovecot-{package}", src=deb_filename)
|
||||||
|
|
||||||
|
|
||||||
def _configure_dovecot(config: Config, debug: bool = False) -> (bool, bool):
|
def _configure_dovecot(config: Config, debug: bool = False) -> (bool, bool):
|
||||||
@@ -161,7 +145,7 @@ def _configure_dovecot(config: Config, debug: bool = False) -> (bool, bool):
|
|||||||
if not can_modify:
|
if not can_modify:
|
||||||
print(
|
print(
|
||||||
"\n!!!! refusing to attempt sysctl setting in shared-kernel containers\n"
|
"\n!!!! refusing to attempt sysctl setting in shared-kernel containers\n"
|
||||||
f"!!!! dovecot: sysctl {key!r}={value}, should be >65534 for production setups\n"
|
f"!!!! dovecot: sysctl {key!r}={value}, should be >65535 for production setups\n"
|
||||||
"!!!!"
|
"!!!!"
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -14,10 +14,10 @@ class FiltermailDeployer(Deployer):
|
|||||||
|
|
||||||
def install(self):
|
def install(self):
|
||||||
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.0/filtermail-{arch}"
|
url = f"https://github.com/chatmail/filtermail/releases/download/v0.5.2/filtermail-{arch}"
|
||||||
sha256sum = {
|
sha256sum = {
|
||||||
"x86_64": "3fd8b18282252c75a5bbfa603d8c1b65f6563e5e920bddf3e64e451b7cdb43ce",
|
"x86_64": "ce24ca0075aa445510291d775fb3aea8f4411818c7b885ae51a0fe18c5f789ce",
|
||||||
"aarch64": "2bd191de205f7fd60158dd8e3516ab7e3efb14627696f3d7dc186bdcd9e10a43",
|
"aarch64": "c5d783eefa5332db3d97a0e6a23917d72849e3eb45da3d16ce908a9b4e5a797d",
|
||||||
}[arch]
|
}[arch]
|
||||||
self.need_restart |= files.download(
|
self.need_restart |= files.download(
|
||||||
name="Download filtermail",
|
name="Download filtermail",
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ def _lxc_start_cmd(args, out):
|
|||||||
)
|
)
|
||||||
sub.green(f" Include {ssh_cfg}")
|
sub.green(f" Include {ssh_cfg}")
|
||||||
|
|
||||||
# Optionally run cmdeploy run + dns on each relay
|
# Optionally run cmdeploy run on each relay
|
||||||
if args.run:
|
if args.run:
|
||||||
for ct in relays:
|
for ct in relays:
|
||||||
with out.section(f"cmdeploy run: {ct.sname} ({ct.domain})"):
|
with out.section(f"cmdeploy run: {ct.sname} ({ct.domain})"):
|
||||||
@@ -123,20 +123,6 @@ def _lxc_start_cmd(args, out):
|
|||||||
out.red(f"Deploy to {ct.sname} failed (exit {ret})")
|
out.red(f"Deploy to {ct.sname} failed (exit {ret})")
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
with out.section("loading DNS zones"):
|
|
||||||
for ct in relays:
|
|
||||||
ret = _run_cmdeploy(
|
|
||||||
"dns", ct, ix, out,
|
|
||||||
extra=["--zonefile", str(ct.zone)],
|
|
||||||
)
|
|
||||||
if ret:
|
|
||||||
out.red(f"DNS for {ct.sname} failed (exit {ret})")
|
|
||||||
return ret
|
|
||||||
if ct.zone.exists():
|
|
||||||
dns_ct.set_dns_records(ct.zone.read_text())
|
|
||||||
out.print(f"Restarting filtermail-incoming on {ct.name}")
|
|
||||||
ct.bash("systemctl restart filtermail-incoming")
|
|
||||||
|
|
||||||
|
|
||||||
# -------------------------------------------------------------------
|
# -------------------------------------------------------------------
|
||||||
# lxc-stop
|
# lxc-stop
|
||||||
@@ -261,13 +247,6 @@ def lxc_test_cmd(args, out):
|
|||||||
out.print(f"Loading {ct.zone} into PowerDNS ...")
|
out.print(f"Loading {ct.zone} into PowerDNS ...")
|
||||||
dns_ct.set_dns_records(zone_data)
|
dns_ct.set_dns_records(zone_data)
|
||||||
|
|
||||||
# Restart filtermail so its in-process DNS cache
|
|
||||||
# does not hold stale negative DKIM responses
|
|
||||||
# from before the zones were loaded.
|
|
||||||
for ct in map(ix.get_container, relay_names):
|
|
||||||
out.print(f"Restarting filtermail-incoming on {ct.name} ...")
|
|
||||||
ct.bash("systemctl restart filtermail-incoming")
|
|
||||||
|
|
||||||
with out.section("cmdeploy test"):
|
with out.section("cmdeploy test"):
|
||||||
first = ix.get_container(relay_names[0])
|
first = ix.get_container(relay_names[0])
|
||||||
env = None
|
env = None
|
||||||
|
|||||||
@@ -99,22 +99,6 @@ class Incus:
|
|||||||
target = f"include {self.ssh_config_path}".lower()
|
target = f"include {self.ssh_config_path}".lower()
|
||||||
return any(line.strip().lower() == target for line in lines)
|
return any(line.strip().lower() == target for line in lines)
|
||||||
|
|
||||||
def get_host_nameservers(self):
|
|
||||||
"""Return upstream nameservers found on the host."""
|
|
||||||
ns = []
|
|
||||||
for path in ["/run/systemd/resolve/resolv.conf", "/etc/resolv.conf"]:
|
|
||||||
p = Path(path)
|
|
||||||
if p.exists():
|
|
||||||
for line in p.read_text().splitlines():
|
|
||||||
if line.strip().startswith("nameserver "):
|
|
||||||
addr = line.split()[1]
|
|
||||||
if addr not in ("127.0.0.1", "127.0.0.53", "::1"):
|
|
||||||
if addr not in ns:
|
|
||||||
ns.append(addr)
|
|
||||||
if ns:
|
|
||||||
break
|
|
||||||
return ns
|
|
||||||
|
|
||||||
def run(self, args, check=True, capture=True, input=None):
|
def run(self, args, check=True, capture=True, input=None):
|
||||||
"""Run an incus command.
|
"""Run an incus command.
|
||||||
|
|
||||||
@@ -122,7 +106,7 @@ class Incus:
|
|||||||
to the terminal line-by-line while also being captured for
|
to the terminal line-by-line while also being captured for
|
||||||
later return via result.stdout.
|
later return via result.stdout.
|
||||||
"""
|
"""
|
||||||
cmd = ["incus", "--quiet"] + list(args)
|
cmd = ["incus"] + list(args)
|
||||||
sub = self.out.new_prefixed_out(" ")
|
sub = self.out.new_prefixed_out(" ")
|
||||||
|
|
||||||
if not capture:
|
if not capture:
|
||||||
@@ -144,9 +128,9 @@ class Incus:
|
|||||||
proc = subprocess.Popen(
|
proc = subprocess.Popen(
|
||||||
cmd,
|
cmd,
|
||||||
text=True,
|
text=True,
|
||||||
stdin=subprocess.PIPE if input else subprocess.DEVNULL,
|
stdin=subprocess.PIPE if input else None,
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
stderr=subprocess.PIPE,
|
stderr=subprocess.STDOUT,
|
||||||
)
|
)
|
||||||
|
|
||||||
stdout_lines = []
|
stdout_lines = []
|
||||||
@@ -159,17 +143,15 @@ class Incus:
|
|||||||
if sub.verbosity >= 2:
|
if sub.verbosity >= 2:
|
||||||
sub.print(f" > {line.rstrip()}")
|
sub.print(f" > {line.rstrip()}")
|
||||||
|
|
||||||
stderr = proc.stderr.read()
|
|
||||||
ret = proc.wait()
|
ret = proc.wait()
|
||||||
stdout = "".join(stdout_lines)
|
stdout = "".join(stdout_lines)
|
||||||
if check and ret != 0:
|
if check and ret != 0:
|
||||||
full_output = stdout + stderr
|
for line in stdout.splitlines():
|
||||||
for line in full_output.splitlines():
|
|
||||||
if sub.verbosity < 1: # and we haven't printed it yet
|
if sub.verbosity < 1: # and we haven't printed it yet
|
||||||
sub.red(line)
|
sub.red(line)
|
||||||
raise subprocess.CalledProcessError(ret, cmd, output=stdout, stderr=stderr)
|
raise subprocess.CalledProcessError(ret, cmd, output=stdout)
|
||||||
|
|
||||||
return subprocess.CompletedProcess(cmd, ret, stdout=stdout, stderr=stderr)
|
return subprocess.CompletedProcess(cmd, ret, stdout=stdout)
|
||||||
|
|
||||||
def run_json(self, args, check=True):
|
def run_json(self, args, check=True):
|
||||||
"""Run an incus command with ``--format=json``.
|
"""Run an incus command with ``--format=json``.
|
||||||
@@ -259,10 +241,8 @@ class Incus:
|
|||||||
|
|
||||||
key_path = self.ssh_key_path
|
key_path = self.ssh_key_path
|
||||||
pub_key = key_path.with_suffix(".pub").read_text().strip()
|
pub_key = key_path.with_suffix(".pub").read_text().strip()
|
||||||
host_ns = self.get_host_nameservers()
|
|
||||||
ns_lines = "\n".join(f"nameserver {n}" for n in host_ns)
|
|
||||||
ct.bash(f"""
|
ct.bash(f"""
|
||||||
printf '{ns_lines}\n' > /etc/resolv.conf
|
echo 'nameserver 9.9.9.9' > /etc/resolv.conf
|
||||||
apt-get -o DPkg::Lock::Timeout=60 update
|
apt-get -o DPkg::Lock::Timeout=60 update
|
||||||
DEBIAN_FRONTEND=noninteractive apt-get install -y openssh-server python3
|
DEBIAN_FRONTEND=noninteractive apt-get install -y openssh-server python3
|
||||||
systemctl enable ssh
|
systemctl enable ssh
|
||||||
@@ -299,11 +279,12 @@ class Incus:
|
|||||||
class Container:
|
class Container:
|
||||||
"""The base container handle wraps all interactions with incus."""
|
"""The base container handle wraps all interactions with incus."""
|
||||||
|
|
||||||
def __init__(self, incus, name, domain=None):
|
def __init__(self, incus, name, domain=None, memory="200MiB"):
|
||||||
self.incus = incus
|
self.incus = incus
|
||||||
self.out = incus.out
|
self.out = incus.out
|
||||||
self.name = name
|
self.name = name
|
||||||
self.domain = domain or f"{name}{DOMAIN_SUFFIX}"
|
self.domain = domain or f"{name}{DOMAIN_SUFFIX}"
|
||||||
|
self.memory = memory
|
||||||
self.ipv4 = None
|
self.ipv4 = None
|
||||||
self.ipv6 = None
|
self.ipv6 = None
|
||||||
|
|
||||||
@@ -348,6 +329,7 @@ class Container:
|
|||||||
cfg = []
|
cfg = []
|
||||||
cfg += ("-c", f"{LABEL_KEY}=true")
|
cfg += ("-c", f"{LABEL_KEY}=true")
|
||||||
cfg += ("-c", f"user.localchat-domain={self.domain}")
|
cfg += ("-c", f"user.localchat-domain={self.domain}")
|
||||||
|
cfg += ("-c", f"limits.memory={self.memory}")
|
||||||
self.incus.run(["launch", image, self.name, *cfg])
|
self.incus.run(["launch", image, self.name, *cfg])
|
||||||
return image
|
return image
|
||||||
|
|
||||||
@@ -432,6 +414,7 @@ class RelayContainer(Container):
|
|||||||
incus,
|
incus,
|
||||||
f"{name}-localchat",
|
f"{name}-localchat",
|
||||||
domain=f"_{name}{DOMAIN_SUFFIX}",
|
domain=f"_{name}{DOMAIN_SUFFIX}",
|
||||||
|
memory="600MiB",
|
||||||
)
|
)
|
||||||
self.sname = name
|
self.sname = name
|
||||||
self.ini = incus.lxconfigs_dir / f"chatmail-{name}.ini"
|
self.ini = incus.lxconfigs_dir / f"chatmail-{name}.ini"
|
||||||
@@ -456,14 +439,11 @@ class RelayContainer(Container):
|
|||||||
self.bash("""
|
self.bash("""
|
||||||
sysctl -w net.ipv6.conf.all.disable_ipv6=1
|
sysctl -w net.ipv6.conf.all.disable_ipv6=1
|
||||||
sysctl -w net.ipv6.conf.default.disable_ipv6=1
|
sysctl -w net.ipv6.conf.default.disable_ipv6=1
|
||||||
|
mkdir -p /etc/sysctl.d
|
||||||
|
printf 'net.ipv6.conf.all.disable_ipv6=1\\n
|
||||||
|
net.ipv6.conf.default.disable_ipv6=1\\n'
|
||||||
|
> /etc/sysctl.d/99-disable-ipv6.conf
|
||||||
""")
|
""")
|
||||||
self.push_file_content(
|
|
||||||
"/etc/sysctl.d/99-disable-ipv6.conf",
|
|
||||||
"""
|
|
||||||
net.ipv6.conf.all.disable_ipv6=1
|
|
||||||
net.ipv6.conf.default.disable_ipv6=1
|
|
||||||
""",
|
|
||||||
)
|
|
||||||
|
|
||||||
def configure_hosts(self, ip):
|
def configure_hosts(self, ip):
|
||||||
"""Set hostname and /etc/hosts inside the container."""
|
"""Set hostname and /etc/hosts inside the container."""
|
||||||
@@ -503,29 +483,22 @@ class RelayContainer(Container):
|
|||||||
|
|
||||||
def verify_ssh(self, ssh_config):
|
def verify_ssh(self, ssh_config):
|
||||||
"""Verify SSH connectivity to this container."""
|
"""Verify SSH connectivity to this container."""
|
||||||
cmd = f"ssh -F {ssh_config} -o ConnectTimeout=60 root@{self.domain} hostname"
|
cmd = f"ssh -F {ssh_config} -o ConnectTimeout=10 root@{self.domain} hostname"
|
||||||
return shell(cmd, timeout=60).returncode == 0
|
return shell(cmd, timeout=15).returncode == 0
|
||||||
|
|
||||||
def configure_dns(self, dns_ip):
|
def configure_dns(self, dns_ip):
|
||||||
"""Point this container's resolver at *dns_ip* and verify DNS is reachable."""
|
"""Point this container's resolver at *dns_ip* and verify DNS is reachable."""
|
||||||
self.bash(f"""
|
self.bash(f"""
|
||||||
systemctl disable --now systemd-resolved 2>/dev/null || true
|
systemctl disable --now systemd-resolved 2>/dev/null || true
|
||||||
rm -f /etc/resolv.conf
|
rm -f /etc/resolv.conf
|
||||||
printf 'nameserver {dns_ip}\\n' >/etc/resolv.conf
|
echo 'nameserver {dns_ip}' > /etc/resolv.conf
|
||||||
mkdir -p /etc/unbound/unbound.conf.d
|
mkdir -p /etc/unbound/unbound.conf.d
|
||||||
|
printf 'server:\\n domain-insecure: "localchat"\\n\\n
|
||||||
|
forward-zone:\\n name: "localchat"\\n
|
||||||
|
forward-addr: {dns_ip}\\n'
|
||||||
|
> /etc/unbound/unbound.conf.d/localchat-forward.conf
|
||||||
|
systemctl restart unbound 2>/dev/null || true
|
||||||
""")
|
""")
|
||||||
self.push_file_content(
|
|
||||||
"/etc/unbound/unbound.conf.d/localchat-forward.conf",
|
|
||||||
f"""
|
|
||||||
server:
|
|
||||||
domain-insecure: "localchat"
|
|
||||||
|
|
||||||
forward-zone:
|
|
||||||
name: "localchat"
|
|
||||||
forward-addr: {dns_ip}
|
|
||||||
""",
|
|
||||||
)
|
|
||||||
self.bash("systemctl restart unbound 2>/dev/null || true")
|
|
||||||
self._wait_dns_reachable(dns_ip)
|
self._wait_dns_reachable(dns_ip)
|
||||||
|
|
||||||
def _wait_dns_reachable(self, dns_ip, timeout=10):
|
def _wait_dns_reachable(self, dns_ip, timeout=10):
|
||||||
@@ -589,7 +562,7 @@ class DNSContainer(Container):
|
|||||||
""")
|
""")
|
||||||
self._wait_dns_ready()
|
self._wait_dns_ready()
|
||||||
|
|
||||||
def _wait_dns_ready(self, timeout=60):
|
def _wait_dns_ready(self, timeout=10):
|
||||||
"""Poll until the recursor answers a query on port 53."""
|
"""Poll until the recursor answers a query on port 53."""
|
||||||
deadline = time.time() + timeout
|
deadline = time.time() + timeout
|
||||||
while time.time() < deadline:
|
while time.time() < deadline:
|
||||||
@@ -631,13 +604,10 @@ class DNSContainer(Container):
|
|||||||
if self.run_cmd("which", "pdns_server", check=False) is not None:
|
if self.run_cmd("which", "pdns_server", check=False) is not None:
|
||||||
return
|
return
|
||||||
|
|
||||||
host_ns = self.incus.get_host_nameservers()
|
self.bash("""
|
||||||
ns_lines = "\n".join(f"nameserver {n}" for n in host_ns)
|
|
||||||
|
|
||||||
self.bash(f"""
|
|
||||||
systemctl disable --now systemd-resolved 2>/dev/null || true
|
systemctl disable --now systemd-resolved 2>/dev/null || true
|
||||||
rm -f /etc/resolv.conf
|
rm -f /etc/resolv.conf
|
||||||
printf '{ns_lines}\n' > /etc/resolv.conf
|
echo 'nameserver 9.9.9.9' > /etc/resolv.conf
|
||||||
|
|
||||||
# Block automatic service startup during package installation
|
# Block automatic service startup during package installation
|
||||||
printf '#!/bin/sh\\nexit 101\\n' > /usr/sbin/policy-rc.d
|
printf '#!/bin/sh\\nexit 101\\n' > /usr/sbin/policy-rc.d
|
||||||
@@ -673,6 +643,7 @@ class DNSContainer(Container):
|
|||||||
local-address=0.0.0.0
|
local-address=0.0.0.0
|
||||||
local-port=53
|
local-port=53
|
||||||
forward-zones=localchat=127.0.0.1:5353
|
forward-zones=localchat=127.0.0.1:5353
|
||||||
|
forward-zones-recurse=.=9.9.9.9;149.112.112.112
|
||||||
allow-from=0.0.0.0/0
|
allow-from=0.0.0.0/0
|
||||||
dont-query=
|
dont-query=
|
||||||
dnssec=off
|
dnssec=off
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ smtpd_tls_key_file={{ config.tls_key_path }}
|
|||||||
smtpd_tls_security_level=may
|
smtpd_tls_security_level=may
|
||||||
|
|
||||||
smtp_tls_CApath=/etc/ssl/certs
|
smtp_tls_CApath=/etc/ssl/certs
|
||||||
smtp_tls_security_level=verify
|
smtp_tls_security_level={{ "verify" if config.tls_cert_mode == "acme" else "encrypt" }}
|
||||||
# Send SNI extension when connecting to other servers.
|
# Send SNI extension when connecting to other servers.
|
||||||
# <https://www.postfix.org/postconf.5.html#smtp_tls_servername>
|
# <https://www.postfix.org/postconf.5.html#smtp_tls_servername>
|
||||||
smtp_tls_servername = hostname
|
smtp_tls_servername = hostname
|
||||||
@@ -88,22 +88,6 @@ 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_transport = lmtp:unix:private/dovecot-lmtp
|
||||||
virtual_mailbox_domains = {{ config.mail_domain }}
|
virtual_mailbox_domains = {{ config.mail_domain }}
|
||||||
lmtp_header_checks = regexp:/etc/postfix/lmtp_header_cleanup
|
lmtp_header_checks = regexp:/etc/postfix/lmtp_header_cleanup
|
||||||
|
|||||||
@@ -57,10 +57,9 @@ def get_dkim_entry(mail_domain, pre_command, dkim_selector):
|
|||||||
dkim_value_raw = f"v=DKIM1;k=rsa;p={dkim_pubkey};s=email;t=s"
|
dkim_value_raw = f"v=DKIM1;k=rsa;p={dkim_pubkey};s=email;t=s"
|
||||||
dkim_value = '" "'.join(re.findall(".{1,255}", dkim_value_raw))
|
dkim_value = '" "'.join(re.findall(".{1,255}", dkim_value_raw))
|
||||||
web_dkim_value = "".join(re.findall(".{1,255}", dkim_value_raw))
|
web_dkim_value = "".join(re.findall(".{1,255}", dkim_value_raw))
|
||||||
name = f"{dkim_selector}._domainkey.{mail_domain}."
|
|
||||||
return (
|
return (
|
||||||
f'{name:<40} 3600 IN TXT "{dkim_value}"',
|
f'{dkim_selector}._domainkey.{mail_domain}. 3600 IN TXT "{dkim_value}"',
|
||||||
f'{name:<40} 3600 IN TXT "{web_dkim_value}"',
|
f'{dkim_selector}._domainkey.{mail_domain}. 3600 IN TXT "{web_dkim_value}"',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
; Required DNS entries
|
; Required DNS entries
|
||||||
zftest.testrun.org. 3600 IN A 135.181.204.127
|
zftest.testrun.org. 3600 IN A 135.181.204.127
|
||||||
zftest.testrun.org. 3600 IN AAAA 2a01:4f9:c012:52f4::1
|
zftest.testrun.org. 3600 IN AAAA 2a01:4f9:c012:52f4::1
|
||||||
zftest.testrun.org. 3600 IN MX 10 zftest.testrun.org.
|
zftest.testrun.org. 3600 IN MX 10 zftest.testrun.org.
|
||||||
_mta-sts.zftest.testrun.org. 3600 IN TXT "v=STSv1; id=202403211706"
|
_mta-sts.zftest.testrun.org. 3600 IN TXT "v=STSv1; id=202403211706"
|
||||||
mta-sts.zftest.testrun.org. 3600 IN CNAME zftest.testrun.org.
|
mta-sts.zftest.testrun.org. 3600 IN CNAME zftest.testrun.org.
|
||||||
www.zftest.testrun.org. 3600 IN CNAME zftest.testrun.org.
|
www.zftest.testrun.org. 3600 IN CNAME zftest.testrun.org.
|
||||||
opendkim._domainkey.zftest.testrun.org. 3600 IN TXT "v=DKIM1;k=rsa;p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoYt82CVUyz2ouaqjX2kB+5J80knAyoOU3MGU5aWppmwUwwTvj/oSTSpkc5JMtVTRmKKr8NUDWAL1Yw7dfGqqPHdHfwwjS3BIvDzYx+hzgtz62RnfNgV+/2MAoNpfX7cAFIHdRzEHNtwugc3RDLquqPoupAE3Y2YRw2T5zG5fILh4vwIcJZL5Uq6B92j8wwJqOex" "33n+vm1NKQ9rxo/UsHAmZlJzpooXcG/4igTBxJyJlamVSRR6N7Nul1v//YJb7J6v2o0iPHW6uE0StzKaPPNC2IVosSRFbD9H2oqppltptFSNPlI0E+t0JBWHem6YK7xcugiO3ImMCaaU8g6Jt/wIDAQAB;s=email;t=s"
|
opendkim._domainkey.zftest.testrun.org. 3600 IN TXT "v=DKIM1;k=rsa;p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoYt82CVUyz2ouaqjX2kB+5J80knAyoOU3MGU5aWppmwUwwTvj/oSTSpkc5JMtVTRmKKr8NUDWAL1Yw7dfGqqPHdHfwwjS3BIvDzYx+hzgtz62RnfNgV+/2MAoNpfX7cAFIHdRzEHNtwugc3RDLquqPoupAE3Y2YRw2T5zG5fILh4vwIcJZL5Uq6B92j8wwJqOex" "33n+vm1NKQ9rxo/UsHAmZlJzpooXcG/4igTBxJyJlamVSRR6N7Nul1v//YJb7J6v2o0iPHW6uE0StzKaPPNC2IVosSRFbD9H2oqppltptFSNPlI0E+t0JBWHem6YK7xcugiO3ImMCaaU8g6Jt/wIDAQAB;s=email;t=s"
|
||||||
|
|
||||||
; Recommended DNS entries
|
; Recommended DNS entries
|
||||||
zftest.testrun.org. 3600 IN TXT "v=spf1 a ~all"
|
zftest.testrun.org. 3600 IN TXT "v=spf1 a ~all"
|
||||||
_dmarc.zftest.testrun.org. 3600 IN TXT "v=DMARC1;p=reject;adkim=s;aspf=s"
|
_dmarc.zftest.testrun.org. 3600 IN TXT "v=DMARC1;p=reject;adkim=s;aspf=s"
|
||||||
zftest.testrun.org. 3600 IN CAA 0 issue "letsencrypt.org;accounturi=https://acme-v02.api.letsencrypt.org/acme/acct/1371472956"
|
zftest.testrun.org. 3600 IN CAA 0 issue "letsencrypt.org;accounturi=https://acme-v02.api.letsencrypt.org/acme/acct/1371472956"
|
||||||
_adsp._domainkey.zftest.testrun.org. 3600 IN TXT "dkim=discardable"
|
_adsp._domainkey.zftest.testrun.org. 3600 IN TXT "dkim=discardable"
|
||||||
_submission._tcp.zftest.testrun.org. 3600 IN SRV 0 1 587 zftest.testrun.org.
|
_submission._tcp.zftest.testrun.org. 3600 IN SRV 0 1 587 zftest.testrun.org.
|
||||||
_submissions._tcp.zftest.testrun.org. 3600 IN SRV 0 1 465 zftest.testrun.org.
|
_submissions._tcp.zftest.testrun.org. 3600 IN SRV 0 1 465 zftest.testrun.org.
|
||||||
_imap._tcp.zftest.testrun.org. 3600 IN SRV 0 1 143 zftest.testrun.org.
|
_imap._tcp.zftest.testrun.org. 3600 IN SRV 0 1 143 zftest.testrun.org.
|
||||||
_imaps._tcp.zftest.testrun.org. 3600 IN SRV 0 1 993 zftest.testrun.org.
|
_imaps._tcp.zftest.testrun.org. 3600 IN SRV 0 1 993 zftest.testrun.org.
|
||||||
|
|||||||
@@ -89,9 +89,7 @@ def test_concurrent_logins_same_account(
|
|||||||
assert login_results.get()
|
assert login_results.get()
|
||||||
|
|
||||||
|
|
||||||
def test_no_vrfy(cmfactory, chatmail_config):
|
def test_no_vrfy(chatmail_config):
|
||||||
ac = cmfactory.get_online_account()
|
|
||||||
addr = ac.get_config("addr")
|
|
||||||
domain = chatmail_config.mail_domain
|
domain = chatmail_config.mail_domain
|
||||||
|
|
||||||
s = smtplib.SMTP(domain)
|
s = smtplib.SMTP(domain)
|
||||||
@@ -100,7 +98,7 @@ def test_no_vrfy(cmfactory, chatmail_config):
|
|||||||
s.putcmd("vrfy", f"wrongaddress@{chatmail_config.mail_domain}")
|
s.putcmd("vrfy", f"wrongaddress@{chatmail_config.mail_domain}")
|
||||||
result = s.getreply()
|
result = s.getreply()
|
||||||
print(result)
|
print(result)
|
||||||
s.putcmd("vrfy", addr)
|
s.putcmd("vrfy", f"echo@{chatmail_config.mail_domain}")
|
||||||
result2 = s.getreply()
|
result2 = s.getreply()
|
||||||
print(result2)
|
print(result2)
|
||||||
assert result[0] == result2[0] == 252
|
assert result[0] == result2[0] == 252
|
||||||
|
|||||||
@@ -487,16 +487,13 @@ def cmfactory(
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def remote(sshdomain, pytestconfig):
|
def remote(sshdomain, pytestconfig):
|
||||||
r = Remote(sshdomain, ssh_config=pytestconfig.getoption("ssh_config"))
|
return Remote(sshdomain, ssh_config=pytestconfig.getoption("ssh_config"))
|
||||||
yield r
|
|
||||||
r.close()
|
|
||||||
|
|
||||||
|
|
||||||
class Remote:
|
class Remote:
|
||||||
def __init__(self, sshdomain, ssh_config=None):
|
def __init__(self, sshdomain, ssh_config=None):
|
||||||
self.sshdomain = sshdomain
|
self.sshdomain = sshdomain
|
||||||
self.ssh_config = ssh_config
|
self.ssh_config = ssh_config
|
||||||
self._procs = []
|
|
||||||
|
|
||||||
def iter_output(self, logcmd="", ready=None):
|
def iter_output(self, logcmd="", ready=None):
|
||||||
getjournal = "journalctl -f" if not logcmd else logcmd
|
getjournal = "journalctl -f" if not logcmd else logcmd
|
||||||
@@ -512,15 +509,12 @@ class Remote:
|
|||||||
command.extend(["-F", self.ssh_config])
|
command.extend(["-F", self.ssh_config])
|
||||||
command.append(f"root@{self.sshdomain}")
|
command.append(f"root@{self.sshdomain}")
|
||||||
[command.append(arg) for arg in getjournal.split()]
|
[command.append(arg) for arg in getjournal.split()]
|
||||||
popen = subprocess.Popen(
|
self.popen = subprocess.Popen(
|
||||||
command,
|
command,
|
||||||
stdin=subprocess.DEVNULL,
|
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
stderr=subprocess.DEVNULL,
|
|
||||||
)
|
)
|
||||||
self._procs.append(popen)
|
|
||||||
while 1:
|
while 1:
|
||||||
line = popen.stdout.readline()
|
line = self.popen.stdout.readline()
|
||||||
res = line.decode().strip().lower()
|
res = line.decode().strip().lower()
|
||||||
if not res:
|
if not res:
|
||||||
break
|
break
|
||||||
@@ -529,12 +523,6 @@ class Remote:
|
|||||||
ready = None
|
ready = None
|
||||||
yield res
|
yield res
|
||||||
|
|
||||||
def close(self):
|
|
||||||
while self._procs:
|
|
||||||
proc = self._procs.pop()
|
|
||||||
proc.kill()
|
|
||||||
proc.wait()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def lp(request):
|
def lp(request):
|
||||||
|
|||||||
@@ -132,28 +132,11 @@ def test_parse_zone_records():
|
|||||||
|
|
||||||
; Another comment
|
; Another comment
|
||||||
www.some.domain. 3600 IN CNAME some.domain.
|
www.some.domain. 3600 IN CNAME some.domain.
|
||||||
|
|
||||||
; Multi-word rdata
|
|
||||||
some.domain. 3600 IN MX 10 mail.some.domain.
|
|
||||||
|
|
||||||
; DKIM record (single line, multi-word TXT rdata)
|
|
||||||
dkim._domainkey.some.domain. 3600 IN TXT "v=DKIM1;k=rsa;p=MIIBIjANBgkqhkiG" "9w0BAQEFAAOCAQ8AMIIBCgKCAQEA"
|
|
||||||
|
|
||||||
; Another TXT record
|
|
||||||
_dmarc.some.domain. 3600 IN TXT "v=DMARC1;p=reject"
|
|
||||||
"""
|
"""
|
||||||
records = list(parse_zone_records(text))
|
records = list(parse_zone_records(text))
|
||||||
assert records == [
|
assert records == [
|
||||||
("some.domain", "3600", "A", "1.1.1.1"),
|
("some.domain", "3600", "A", "1.1.1.1"),
|
||||||
("www.some.domain", "3600", "CNAME", "some.domain."),
|
("www.some.domain", "3600", "CNAME", "some.domain."),
|
||||||
("some.domain", "3600", "MX", "10 mail.some.domain."),
|
|
||||||
(
|
|
||||||
"dkim._domainkey.some.domain",
|
|
||||||
"3600",
|
|
||||||
"TXT",
|
|
||||||
'"v=DKIM1;k=rsa;p=MIIBIjANBgkqhkiG" "9w0BAQEFAAOCAQ8AMIIBCgKCAQEA"',
|
|
||||||
),
|
|
||||||
("_dmarc.some.domain", "3600", "TXT", '"v=DMARC1;p=reject"'),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -15,10 +15,10 @@ from termcolor import colored
|
|||||||
class Out:
|
class Out:
|
||||||
"""Convenience output printer providing coloring and section formatting."""
|
"""Convenience output printer providing coloring and section formatting."""
|
||||||
|
|
||||||
def __init__(self, prefix="", verbosity=0):
|
def __init__(self, sepchar="\u2501", prefix="", verbosity=0):
|
||||||
self.section_timings = []
|
self.section_timings = []
|
||||||
self.prefix = prefix
|
self.prefix = prefix
|
||||||
self.sepchar = "\u2501"
|
self.sepchar = sepchar
|
||||||
self.verbosity = verbosity
|
self.verbosity = verbosity
|
||||||
env_width = os.environ.get("_CMDEPLOY_WIDTH")
|
env_width = os.environ.get("_CMDEPLOY_WIDTH")
|
||||||
if env_width:
|
if env_width:
|
||||||
@@ -31,6 +31,7 @@ class Out:
|
|||||||
sharing section_timings with the parent.
|
sharing section_timings with the parent.
|
||||||
"""
|
"""
|
||||||
out = Out(
|
out = Out(
|
||||||
|
sepchar=self.sepchar,
|
||||||
prefix=self.prefix + newprefix,
|
prefix=self.prefix + newprefix,
|
||||||
verbosity=self.verbosity,
|
verbosity=self.verbosity,
|
||||||
)
|
)
|
||||||
@@ -89,7 +90,6 @@ class Out:
|
|||||||
cmd,
|
cmd,
|
||||||
shell=True,
|
shell=True,
|
||||||
text=True,
|
text=True,
|
||||||
stdin=subprocess.DEVNULL,
|
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
stderr=subprocess.STDOUT,
|
stderr=subprocess.STDOUT,
|
||||||
env=env,
|
env=env,
|
||||||
@@ -133,7 +133,6 @@ def shell(cmd, check=False, **kwargs):
|
|||||||
"""
|
"""
|
||||||
if "capture_output" not in kwargs and "stdout" not in kwargs:
|
if "capture_output" not in kwargs and "stdout" not in kwargs:
|
||||||
kwargs["capture_output"] = True
|
kwargs["capture_output"] = True
|
||||||
kwargs.setdefault("stdin", subprocess.DEVNULL)
|
|
||||||
return subprocess.run(collapse(cmd), shell=True, text=True, check=check, **kwargs)
|
return subprocess.run(collapse(cmd), shell=True, text=True, check=check, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -15,10 +15,6 @@ as they would on a real Debian server or cloud VPS.
|
|||||||
Prerequisites
|
Prerequisites
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
- Around 4-5 GiB free disk space
|
|
||||||
- `systemd-networkd` for the automagic hostname resolution
|
|
||||||
- No other service occupying Port 53
|
|
||||||
|
|
||||||
Install `Incus <https://linuxcontainers.org/incus/>`_
|
Install `Incus <https://linuxcontainers.org/incus/>`_
|
||||||
(LXC container manager).
|
(LXC container manager).
|
||||||
See the `official installation guide
|
See the `official installation guide
|
||||||
@@ -180,7 +176,7 @@ running two `PowerDNS <https://www.powerdns.com/>`_ services:
|
|||||||
* **pdns-recursor** (recursive) listens on the Incus
|
* **pdns-recursor** (recursive) listens on the Incus
|
||||||
bridge so all containers can use it.
|
bridge so all containers can use it.
|
||||||
Forwards ``.localchat`` queries to the local
|
Forwards ``.localchat`` queries to the local
|
||||||
authoritative server and resolves everything else recursively.
|
authoritative server and everything else to Quad9 (``9.9.9.9``).
|
||||||
|
|
||||||
After the DNS container is up, ``lxc-start`` configures the Incus bridge
|
After the DNS container is up, ``lxc-start`` configures the Incus bridge
|
||||||
to advertise its IP via DHCP and disables Incus's own DNS.
|
to advertise its IP via DHCP and disables Incus's own DNS.
|
||||||
|
|||||||
Reference in New Issue
Block a user