mirror of
https://github.com/chatmail/relay.git
synced 2026-05-19 12:28:06 +00:00
Compare commits
32 Commits
hpk/lxcdep
...
hpk/lxc-ci
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ddc36902c8 | ||
|
|
8a1c1d9288 | ||
|
|
c3fd52e5dd | ||
|
|
233d442bca | ||
|
|
8aabb9a955 | ||
|
|
9fc05646e0 | ||
|
|
155c1221b8 | ||
|
|
3393d071e5 | ||
|
|
52c73658f9 | ||
|
|
b022a61955 | ||
|
|
2e383a8e94 | ||
|
|
9908a4c88c | ||
|
|
c04f4a4b44 | ||
|
|
6d0ce061bc | ||
|
|
a48c525455 | ||
|
|
2786b60658 | ||
|
|
fe46b573a6 | ||
|
|
674e496a53 | ||
|
|
86e5708709 | ||
|
|
04ac2cf700 | ||
|
|
e2ec0cf2c5 | ||
|
|
ee9d54f7d6 | ||
|
|
1604321d5b | ||
|
|
4ab04fa6c4 | ||
|
|
8b6829b906 | ||
|
|
cf2cb57cca | ||
|
|
693c3f8555 | ||
|
|
371efdfafb | ||
|
|
23765a5ed8 | ||
|
|
0adeefbdd7 | ||
|
|
624838eedd | ||
|
|
1abdc407af |
90
.github/workflows/ci.yaml
vendored
90
.github/workflows/ci.yaml
vendored
@@ -15,28 +15,98 @@ 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
|
||||||
|
|
||||||
scripts:
|
scripts:
|
||||||
name: deploy-chatmail tests
|
name: deploy-chatmail tests
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: initenv
|
- name: initenv
|
||||||
run: scripts/initenv.sh
|
run: scripts/initenv.sh
|
||||||
|
|
||||||
- name: append venv/bin to PATH
|
- name: append venv/bin to PATH
|
||||||
run: echo venv/bin >>$GITHUB_PATH
|
run: echo venv/bin >>$GITHUB_PATH
|
||||||
|
|
||||||
- name: run formatting checks
|
- name: run formatting checks
|
||||||
run: cmdeploy fmt -v
|
run: cmdeploy fmt -v
|
||||||
|
|
||||||
- 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: 15
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
|
|
||||||
|
- name: install incus
|
||||||
|
run: 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 --quiet
|
||||||
|
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: restore cached images
|
||||||
|
id: cache-images
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
/tmp/localchat-base.tar.gz
|
||||||
|
/tmp/localchat-ns.tar.gz
|
||||||
|
/tmp/localchat-test0.tar.gz
|
||||||
|
/tmp/localchat-test1.tar.gz
|
||||||
|
lxconfigs/id_localchat*
|
||||||
|
key: incus-contain-v2-${{ runner.os }}-${{ github.ref_name }}
|
||||||
|
restore-keys: |
|
||||||
|
incus-contain-v2-${{ runner.os }}-${{ github.ref_name }}-
|
||||||
|
incus-contain-v2-${{ runner.os }}-main-
|
||||||
|
incus-contain-v2-${{ runner.os }}-
|
||||||
|
|
||||||
|
- name: import cached images
|
||||||
|
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 --quiet image import /tmp/$alias.tar.gz --alias $alias" || true
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: cmdeploy lxc-test
|
||||||
|
run: sg incus-admin -c 'cmdeploy lxc-test -vv'
|
||||||
|
|
||||||
|
- name: show container logs on failure
|
||||||
|
if: failure() || cancelled()
|
||||||
|
run: |
|
||||||
|
for c in test0-localchat test1-localchat; do
|
||||||
|
echo "::group::$c journal (warnings+errors)"
|
||||||
|
sg incus-admin -c "incus exec $c -- journalctl -p warning --no-pager -n200" 2>/dev/null || echo "no log"
|
||||||
|
echo "::endgroup::"
|
||||||
|
echo "::group::$c failed services"
|
||||||
|
sg incus-admin -c "incus exec $c -- systemctl --no-pager --failed" || true
|
||||||
|
echo "::endgroup::"
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: export images for cache
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
for alias in localchat-base localchat-ns localchat-test0 localchat-test1; do
|
||||||
|
sg incus-admin -c "incus --quiet image export $alias /tmp/$alias" || true
|
||||||
|
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
|
|
||||||
|
|
||||||
@@ -260,10 +260,10 @@ def test_cmd(args, out):
|
|||||||
pytest_args = [
|
pytest_args = [
|
||||||
pytest_path,
|
pytest_path,
|
||||||
"cmdeploy/src/",
|
"cmdeploy/src/",
|
||||||
"-n4",
|
|
||||||
"-rs",
|
"-rs",
|
||||||
"-x",
|
"-x",
|
||||||
"-v",
|
"-vv" if args.verbose > 1 else "-v",
|
||||||
|
"-s",
|
||||||
"--durations=5",
|
"--durations=5",
|
||||||
]
|
]
|
||||||
if args.slow:
|
if args.slow:
|
||||||
|
|||||||
@@ -145,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",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import os
|
|||||||
import time
|
import time
|
||||||
|
|
||||||
from ..util import get_git_hash, get_version_string, shell
|
from ..util import get_git_hash, get_version_string, shell
|
||||||
from .incus import RELAY_IMAGE_ALIAS, Incus, RelayContainer
|
from .incus import Incus, RelayContainer
|
||||||
|
|
||||||
RELAY_NAMES = ("test0", "test1")
|
RELAY_NAMES = ("test0", "test1")
|
||||||
|
|
||||||
@@ -47,6 +47,7 @@ def _lxc_start_cmd(args, out):
|
|||||||
out.green("Ensuring DNS container (ns-localchat) ...")
|
out.green("Ensuring DNS container (ns-localchat) ...")
|
||||||
dns_ct = ix.get_dns_container()
|
dns_ct = ix.get_dns_container()
|
||||||
dns_ct.ensure()
|
dns_ct.ensure()
|
||||||
|
dns_ct.ensure_cached_as_image()
|
||||||
sub.print(f"DNS container IP: {dns_ct.ipv4}")
|
sub.print(f"DNS container IP: {dns_ct.ipv4}")
|
||||||
|
|
||||||
names = args.names if args.names else RELAY_NAMES
|
names = args.names if args.names else RELAY_NAMES
|
||||||
@@ -116,17 +117,39 @@ def _lxc_start_cmd(args, out):
|
|||||||
|
|
||||||
# Optionally run cmdeploy run + dns on each relay
|
# Optionally run cmdeploy run + dns on each relay
|
||||||
if args.run:
|
if args.run:
|
||||||
|
local_hash = get_git_hash()
|
||||||
for ct in relays:
|
for ct in relays:
|
||||||
|
status = _deploy_status(ct, local_hash, ix)
|
||||||
with out.section(f"cmdeploy run: {ct.sname} ({ct.domain})"):
|
with out.section(f"cmdeploy run: {ct.sname} ({ct.domain})"):
|
||||||
ret = _run_cmdeploy("run", ct, ix, out, extra=["--skip-dns-check"])
|
if "IN-SYNC" in status:
|
||||||
if ret:
|
out.print(f"{ct.sname} is {status}, skipping")
|
||||||
out.red(f"Deploy to {ct.sname} failed (exit {ret})")
|
else:
|
||||||
return ret
|
ret = _run_cmdeploy("run", ct, ix, out, extra=["--skip-dns-check"])
|
||||||
|
if ret:
|
||||||
|
out.red(f"Deploy to {ct.sname} failed (exit {ret})")
|
||||||
|
return ret
|
||||||
|
# Cache a per-relay image after each successful deploy
|
||||||
|
# so the next run can launch directly from the deployed state.
|
||||||
|
with out.section(f"lxc-test: caching {ct.sname} image"):
|
||||||
|
ct.ensure_cached_as_image()
|
||||||
|
|
||||||
with out.section("loading DNS zones"):
|
# Restart mail services to flush stale DNS state.
|
||||||
for ct in relays:
|
# Cached container images boot with a resolv.conf
|
||||||
|
# pointing to the previous run's DNS IP;
|
||||||
|
# configure_dns() already restarted unbound,
|
||||||
|
# but postfix/dovecot may hold stale results
|
||||||
|
# from the window between boot and DNS fix.
|
||||||
|
for ct in relays:
|
||||||
|
out.print(f"Restarting mail services on {ct.name} ...")
|
||||||
|
ct.bash("systemctl restart postfix dovecot opendkim")
|
||||||
|
|
||||||
|
for ct in relays:
|
||||||
|
with out.section(f"cmdeploy dns: {ct.sname} ({ct.domain})"):
|
||||||
ret = _run_cmdeploy(
|
ret = _run_cmdeploy(
|
||||||
"dns", ct, ix, out,
|
"dns",
|
||||||
|
ct,
|
||||||
|
ix,
|
||||||
|
out,
|
||||||
extra=["--zonefile", str(ct.zone)],
|
extra=["--zonefile", str(ct.zone)],
|
||||||
)
|
)
|
||||||
if ret:
|
if ret:
|
||||||
@@ -134,7 +157,10 @@ def _lxc_start_cmd(args, out):
|
|||||||
return ret
|
return ret
|
||||||
if ct.zone.exists():
|
if ct.zone.exists():
|
||||||
dns_ct.set_dns_records(ct.zone.read_text())
|
dns_ct.set_dns_records(ct.zone.read_text())
|
||||||
out.print(f"Restarting filtermail-incoming on {ct.name}")
|
# Restart filtermail so its in-process DNS cache
|
||||||
|
# does not hold stale negative DKIM responses
|
||||||
|
# from before the zones were loaded.
|
||||||
|
out.print(f"Restarting filtermail-incoming on {ct.name} ...")
|
||||||
ct.bash("systemctl restart filtermail-incoming")
|
ct.bash("systemctl restart filtermail-incoming")
|
||||||
|
|
||||||
|
|
||||||
@@ -209,71 +235,26 @@ def lxc_test_cmd(args, out):
|
|||||||
"""
|
"""
|
||||||
ix = Incus(out)
|
ix = Incus(out)
|
||||||
t_total = time.time()
|
t_total = time.time()
|
||||||
relay_names = list(RELAY_NAMES)
|
v_flag = " -" + "v" * out.verbosity if out.verbosity > 0 else ""
|
||||||
if args.one:
|
|
||||||
relay_names = relay_names[:1]
|
|
||||||
|
|
||||||
local_hash = get_git_hash()
|
ret = out.shell(f"cmdeploy lxc-start{v_flag} --run test0", cwd=str(ix.project_root))
|
||||||
|
if ret:
|
||||||
|
return ret
|
||||||
|
|
||||||
# Per-relay: start, deploy, then snapshot the first relay as a
|
if not args.one:
|
||||||
# reusable image so the second relay launches pre-deployed.
|
ret = out.shell(
|
||||||
ipv4_only_flags = {RELAY_NAMES[0]: False, RELAY_NAMES[1]: True}
|
f"cmdeploy lxc-start{v_flag} --run test1 --ipv4-only",
|
||||||
|
cwd=str(ix.project_root),
|
||||||
for ct in map(ix.get_container, relay_names):
|
)
|
||||||
name = ct.sname
|
if ret:
|
||||||
ipv4_only = ipv4_only_flags.get(name, False)
|
return ret
|
||||||
v_flag = " -" + "v" * out.verbosity if out.verbosity > 0 else ""
|
|
||||||
start_cmd = f"cmdeploy lxc-start{v_flag} {name}"
|
|
||||||
if ipv4_only:
|
|
||||||
start_cmd += " --ipv4-only"
|
|
||||||
with out.section(f"cmdeploy lxc-start: {name}"):
|
|
||||||
ret = out.shell(start_cmd, cwd=str(ix.project_root))
|
|
||||||
if ret:
|
|
||||||
return ret
|
|
||||||
|
|
||||||
status = _deploy_status(ct, local_hash, ix)
|
|
||||||
with out.section(f"cmdeploy run: {name}"):
|
|
||||||
if "IN-SYNC" in status:
|
|
||||||
out.print(f"{name} is {status}, skipping")
|
|
||||||
else:
|
|
||||||
ret = _run_cmdeploy("run", ct, ix, out, extra=["--skip-dns-check"])
|
|
||||||
if ret:
|
|
||||||
out.red(f"Deploy to {name} failed (exit {ret})")
|
|
||||||
return ret
|
|
||||||
|
|
||||||
# Snapshot the first relay so subsequent ones launch pre-deployed
|
|
||||||
if not ix.find_image([RELAY_IMAGE_ALIAS]):
|
|
||||||
with out.section("lxc-test: caching relay image"):
|
|
||||||
ct.publish_as_relay_image()
|
|
||||||
|
|
||||||
for ct in map(ix.get_container, relay_names):
|
|
||||||
with out.section(f"cmdeploy dns: {ct.sname} ({ct.domain})"):
|
|
||||||
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
|
|
||||||
|
|
||||||
with out.section(f"lxc-test: loading DNS zones {' & '.join(relay_names)}"):
|
|
||||||
dns_ct = ix.get_dns_container()
|
|
||||||
for ct in map(ix.get_container, relay_names):
|
|
||||||
if ct.zone.exists():
|
|
||||||
zone_data = ct.zone.read_text()
|
|
||||||
out.print(f"Loading {ct.zone} into PowerDNS ...")
|
|
||||||
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("test0")
|
||||||
env = None
|
env = None
|
||||||
if len(relay_names) > 1:
|
if not args.one:
|
||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
env["CHATMAIL_DOMAIN2"] = ix.get_container(relay_names[1]).domain
|
env["CHATMAIL_DOMAIN2"] = ix.get_container("test1").domain
|
||||||
ret = _run_cmdeploy("test", first, ix, out, **({"env": env} if env else {}))
|
ret = _run_cmdeploy("test", first, ix, out, **({"env": env} if env else {}))
|
||||||
if ret:
|
if ret:
|
||||||
out.red(f"Tests failed (exit {ret})")
|
out.red(f"Tests failed (exit {ret})")
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ DOMAIN_SUFFIX = ".localchat"
|
|||||||
UPSTREAM_IMAGE = "images:debian/12"
|
UPSTREAM_IMAGE = "images:debian/12"
|
||||||
BASE_IMAGE_ALIAS = "localchat-base"
|
BASE_IMAGE_ALIAS = "localchat-base"
|
||||||
BASE_SETUP_NAME = "localchat-base-setup"
|
BASE_SETUP_NAME = "localchat-base-setup"
|
||||||
RELAY_IMAGE_ALIAS = "localchat-relay"
|
|
||||||
|
|
||||||
DNS_CONTAINER_NAME = "ns-localchat"
|
DNS_CONTAINER_NAME = "ns-localchat"
|
||||||
|
DNS_IMAGE_ALIAS = "localchat-ns"
|
||||||
DNS_DOMAIN = "ns.localchat"
|
DNS_DOMAIN = "ns.localchat"
|
||||||
|
|
||||||
|
|
||||||
@@ -184,7 +184,16 @@ class Incus:
|
|||||||
)
|
)
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
return None
|
return None
|
||||||
return json.loads(result.stdout)
|
try:
|
||||||
|
return json.loads(result.stdout)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
msg = f"Incus JSON processing failed for {args!r}: {e!s}"
|
||||||
|
self.out.red(msg)
|
||||||
|
self.out.red(f"Captured stdout: {result.stdout!r}")
|
||||||
|
self.out.red(f"Captured stderr: {result.stderr!r}")
|
||||||
|
if check:
|
||||||
|
raise
|
||||||
|
return None
|
||||||
|
|
||||||
def run_output(self, args, check=True):
|
def run_output(self, args, check=True):
|
||||||
"""Run an incus command and return its stripped stdout.
|
"""Run an incus command and return its stripped stdout.
|
||||||
@@ -207,8 +216,13 @@ class Incus:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def delete_images(self):
|
def delete_images(self):
|
||||||
"""Delete the cached base and relay images."""
|
"""Delete localchat-base and per-container images."""
|
||||||
for alias in (RELAY_IMAGE_ALIAS, BASE_IMAGE_ALIAS):
|
for alias in [
|
||||||
|
BASE_IMAGE_ALIAS,
|
||||||
|
DNS_IMAGE_ALIAS,
|
||||||
|
"localchat-test0",
|
||||||
|
"localchat-test1",
|
||||||
|
]:
|
||||||
self.run(["image", "delete", alias], check=False) # ok if absent
|
self.run(["image", "delete", alias], check=False) # ok if absent
|
||||||
|
|
||||||
def list_managed(self):
|
def list_managed(self):
|
||||||
@@ -238,7 +252,7 @@ class Incus:
|
|||||||
def ensure_base_image(self):
|
def ensure_base_image(self):
|
||||||
"""Build and cache a base image with openssh and the SSH key.
|
"""Build and cache a base image with openssh and the SSH key.
|
||||||
|
|
||||||
The image is published as a local incus image with alias
|
The image is cached as a local incus image with alias
|
||||||
'localchat-base'. Subsequent container launches use this
|
'localchat-base'. Subsequent container launches use this
|
||||||
image instead of the upstream Debian 12, skipping the
|
image instead of the upstream Debian 12, skipping the
|
||||||
slow apt-get install step.
|
slow apt-get install step.
|
||||||
@@ -338,7 +352,7 @@ class Container:
|
|||||||
|
|
||||||
def launch(self):
|
def launch(self):
|
||||||
"""Launch from the best available image, return the alias used."""
|
"""Launch from the best available image, return the alias used."""
|
||||||
image = self.incus.find_image([RELAY_IMAGE_ALIAS, BASE_IMAGE_ALIAS])
|
image = self.incus.find_image([BASE_IMAGE_ALIAS])
|
||||||
if not image:
|
if not image:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"No base image '{BASE_IMAGE_ALIAS}' found. "
|
f"No base image '{BASE_IMAGE_ALIAS}' found. "
|
||||||
@@ -419,6 +433,18 @@ class Container:
|
|||||||
parts = line.split()
|
parts = line.split()
|
||||||
return int(parts[2]), int(parts[1])
|
return int(parts[2]), int(parts[1])
|
||||||
|
|
||||||
|
def ensure_cached_as_image(self):
|
||||||
|
"""Cache this container as a respective image."""
|
||||||
|
alias = self.image_alias
|
||||||
|
if self.incus.find_image([alias]):
|
||||||
|
return
|
||||||
|
self.out.print(" Cleaning apt cache before caching image ...")
|
||||||
|
self.bash("apt-get clean")
|
||||||
|
self.out.print(f" Caching {self.name!r} as '{alias}' ...")
|
||||||
|
self.incus.run(["publish", self.name, f"--alias={alias}", "--force"])
|
||||||
|
self.out.print(f" Image '{alias}' cached.")
|
||||||
|
self.wait_ready()
|
||||||
|
|
||||||
|
|
||||||
class RelayContainer(Container):
|
class RelayContainer(Container):
|
||||||
"""Container handle for a chatmail relay.
|
"""Container handle for a chatmail relay.
|
||||||
@@ -434,12 +460,22 @@ class RelayContainer(Container):
|
|||||||
domain=f"_{name}{DOMAIN_SUFFIX}",
|
domain=f"_{name}{DOMAIN_SUFFIX}",
|
||||||
)
|
)
|
||||||
self.sname = name
|
self.sname = name
|
||||||
|
self.image_alias = f"localchat-{name}"
|
||||||
self.ini = incus.lxconfigs_dir / f"chatmail-{name}.ini"
|
self.ini = incus.lxconfigs_dir / f"chatmail-{name}.ini"
|
||||||
self.zone = incus.lxconfigs_dir / f"{name}.zone"
|
self.zone = incus.lxconfigs_dir / f"{name}.zone"
|
||||||
|
|
||||||
def launch(self):
|
def launch(self):
|
||||||
"""Launch (from a potentially cached image) and clear inherited chatmail-version."""
|
"""Launch from localchat-{sname} if cached, else localchat-base."""
|
||||||
image = super().launch()
|
|
||||||
|
candidates = [self.image_alias]
|
||||||
|
candidates.append(BASE_IMAGE_ALIAS)
|
||||||
|
image = self.incus.find_image(candidates)
|
||||||
|
assert image, f"No deployment base, candidates: {','.join(candidates)}"
|
||||||
|
self.out.print(f" Launching from '{image}' image ...")
|
||||||
|
cfg = []
|
||||||
|
cfg += ("-c", f"{LABEL_KEY}=true")
|
||||||
|
cfg += ("-c", f"user.localchat-domain={self.domain}")
|
||||||
|
self.incus.run(["launch", image, self.name, *cfg])
|
||||||
self.bash("rm -f /etc/chatmail-version")
|
self.bash("rm -f /etc/chatmail-version")
|
||||||
return image
|
return image
|
||||||
|
|
||||||
@@ -474,22 +510,6 @@ class RelayContainer(Container):
|
|||||||
echo '{ip} {self.name} {self.domain}' >> /etc/hosts
|
echo '{ip} {self.name} {self.domain}' >> /etc/hosts
|
||||||
""")
|
""")
|
||||||
|
|
||||||
def publish_as_relay_image(self):
|
|
||||||
"""Publish this container as a reusable relay image.
|
|
||||||
|
|
||||||
Stops the container, 'publishes' it as 'localchat-relay', then restarts it.
|
|
||||||
"""
|
|
||||||
if self.incus.find_image([RELAY_IMAGE_ALIAS]):
|
|
||||||
return
|
|
||||||
self.out.print(
|
|
||||||
f" Locally caching {self.name!r} as '{RELAY_IMAGE_ALIAS}' image ..."
|
|
||||||
)
|
|
||||||
self.incus.run(
|
|
||||||
["publish", self.name, f"--alias={RELAY_IMAGE_ALIAS}", "--force"]
|
|
||||||
)
|
|
||||||
self.wait_ready()
|
|
||||||
self.out.print(f" Relay image '{RELAY_IMAGE_ALIAS}' ready.")
|
|
||||||
|
|
||||||
def deployed_version(self):
|
def deployed_version(self):
|
||||||
"""Read /etc/chatmail-version, or None if absent."""
|
"""Read /etc/chatmail-version, or None if absent."""
|
||||||
return self.bash("cat /etc/chatmail-version", check=False)
|
return self.bash("cat /etc/chatmail-version", check=False)
|
||||||
@@ -572,6 +592,21 @@ class DNSContainer(Container):
|
|||||||
|
|
||||||
def __init__(self, incus):
|
def __init__(self, incus):
|
||||||
super().__init__(incus, DNS_CONTAINER_NAME, domain=DNS_DOMAIN)
|
super().__init__(incus, DNS_CONTAINER_NAME, domain=DNS_DOMAIN)
|
||||||
|
self.image_alias = DNS_IMAGE_ALIAS
|
||||||
|
|
||||||
|
def launch(self):
|
||||||
|
"""Launch from localchat-ns if cached, else localchat-base."""
|
||||||
|
image = self.incus.find_image([DNS_IMAGE_ALIAS, BASE_IMAGE_ALIAS])
|
||||||
|
if not image:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"No base image '{BASE_IMAGE_ALIAS}' found. "
|
||||||
|
"Call ensure_base_image() before launching containers."
|
||||||
|
)
|
||||||
|
self.out.print(f" Launching from '{image}' image ...")
|
||||||
|
cfg = []
|
||||||
|
cfg += ("-c", f"{LABEL_KEY}=true")
|
||||||
|
cfg += ("-c", f"user.localchat-domain={self.domain}")
|
||||||
|
self.incus.run(["launch", image, self.name, *cfg])
|
||||||
|
|
||||||
def pdnsutil(self, *args, check=True):
|
def pdnsutil(self, *args, check=True):
|
||||||
"""Run ``pdnsutil <args>`` inside the DNS container."""
|
"""Run ``pdnsutil <args>`` inside the DNS container."""
|
||||||
|
|||||||
@@ -60,6 +60,10 @@ PidFile /run/opendkim/opendkim.pid
|
|||||||
# by the package dns-root-data.
|
# by the package dns-root-data.
|
||||||
TrustAnchorFile /usr/share/dns/root.key
|
TrustAnchorFile /usr/share/dns/root.key
|
||||||
|
|
||||||
|
# Use the local unbound resolver rather than querying root servers directly.
|
||||||
|
# on IPv4-only hosts opendkim may otherwise try ipv6 requests and time out.
|
||||||
|
Nameservers 127.0.0.1
|
||||||
|
|
||||||
# Sign messages when `-o milter_macro_daemon_name=ORIGINATING` is set.
|
# Sign messages when `-o milter_macro_daemon_name=ORIGINATING` is set.
|
||||||
MTA ORIGINATING
|
MTA ORIGINATING
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user