Compare commits

..
Author SHA1 Message Date
KamyarAT 0043954c29 fix: read account address from list_transports()
Delta Chat 2.63 stopped writing the legacy addr and mail_pw config keys.
Online tests that called get_config for those keys got None and crashed.

Read the address and password from list_transports() instead.
2026-10-10 14:13:04 +04:00
holger krekel 672cea6dbe refactor: un-hardcode filesystem paths in configuration templates
Drive all file system paths from templating variables,
useful e.g. for FreeBSD which keeps debian's `/etc` hiearchy rather in `/usr/local/etc`.
Also support static nginx builds which have the stream module compiled in
and thus don't need dynamic linking to a stream module.
2026-10-01 18:28:37 +02:00
holger krekel becc825c8b fix: allow http MX delivery to carry max-message-sized messages
it otherwise uses the 1MB nginx default body size,
forcing a fallback to SMTP (which if not available, fails the delivery)
2026-09-27 19:53:17 +02:00
adbenitezandholger krekel b938882f1c update android version metadata to 2.62.0 2026-09-27 12:40:16 +02:00
maren-bunkandmissytake 4435864779 Update faq.rst to clarify how to deploy tagged releases 2026-09-24 12:18:00 +02:00
missytake 4ffb501c5e chore(release): prepare for 1.13.0 2026-09-22 15:18:38 +02:00
Jagoda Ślązakandmissytake b5f0d5f268 chore(deps): Upgrade filtermail to v0.7.7
## 0.7.7 - 2026-09-11

### Performance

- Don't convert Bytes to Vec

## 0.7.6 - 2026-09-11

### Miscellaneous Tasks

- Always use --locked flag in CI

### Performance

- Don't keep multiple copies of the same mail data in memory

## 0.7.5 - 2026-09-10

### Performance

- Pass HttpsClient by reference instead of cloning it
- Remove connection pool

### Refactor

- Do not evaluate smtp_write! argument twice
- Do not clone the Config unnecessarily

Signed-off-by: Jagoda Ślązak <jslazak@jslazak.com>
2026-09-22 14:04:26 +02:00
22 changed files with 216 additions and 242 deletions
+1 -1
View File
@@ -29,7 +29,7 @@ jobs:
ref: ${{ github.event.pull_request.head.sha }} ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false persist-credentials: false
- name: download filtermail - name: download filtermail
run: curl -L https://github.com/chatmail/filtermail/releases/download/v0.7.4/filtermail-x86_64 -o /usr/local/bin/filtermail && chmod +x /usr/local/bin/filtermail run: curl -L https://github.com/chatmail/filtermail/releases/download/v0.7.7/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
+1 -1
View File
@@ -8,7 +8,7 @@ name: Trigger Docker build
on: on:
push: push:
branches: [main, j4n/dovecot-multidist] branches: [main]
tags: ['[0-9]+.[0-9]+.[0-9]+'] tags: ['[0-9]+.[0-9]+.[0-9]+']
workflow_dispatch: workflow_dispatch:
+33
View File
@@ -1,5 +1,38 @@
# Changelog for chatmail deployment # Changelog for chatmail deployment
## [1.13.0] - 2026-09-22
### Bug Fixes
- Actually use UTC time instead of just seemingly using it
### Documentation
- Describe Madmail v2 as a Rust chatmail relay
### Features
- Serve an APPVERSIONS.json index file to clients via IMAP metadata
- *(mtail)* Deploy filtermail.mtail and gate mtail rule copy on mtail_address
- *(mtail)* Validate programs during deploy
- Move doveauth from dictproxy to lua/http
- Distinguish AUTHENTICATION_FAILED/UNAVAILABLE login failures
### Miscellaneous Tasks
- Try to fix lack of RFC822 item support in madmail and make CI pass
- Un-hardcode executable paths in some systemd service files
- Follow the new mtail release source, upgrade 3.0.8 to 3.4.9
- *(cmdeploy)* Refactor all pins into pins.py
- *(doc)* Use sphinx roles for referencing repository files and dirs
- update deltachat-android link to 2.59.1
### Testing
- Integrate lua testing into regular pytest run for push notifications
- Cleanup and allow a repo-root level "pytest -n6" to succeed.
- [**breaking**] Remove global registration of pytest plugins
## [1.12.0] - 2026-07-31 ## [1.12.0] - 2026-07-31
### Breaking Changes ### Breaking Changes
+3 -3
View File
@@ -1,12 +1,12 @@
# Releasing a new version of chatmail relay # Releasing a new version of chatmail relay
For example, to release version 1.13.0 of chatmail relay, do the following steps. For example, to release version 1.14.0 of chatmail relay, do the following steps.
1. Update the changelog: `git cliff --unreleased --tag 1.13.0 --prepend CHANGELOG.md` or `git cliff -u -t 1.13.0 -p CHANGELOG.md`. 1. Update the changelog: `git cliff --unreleased --tag 1.14.0 --prepend CHANGELOG.md` or `git cliff -u -t 1.14.0 -p CHANGELOG.md`.
2. Open the changelog in the editor, edit it if required. 2. Open the changelog in the editor, edit it if required.
3. Commit the changes to the changelog with a commit message `chore(release): prepare for 1.9.0`. 3. Commit the changes to the changelog with a commit message `chore(release): prepare for 1.14.0`.
4. Open a PR with the new commit, merge it to main after review. 4. Open a PR with the new commit, merge it to main after review.
@@ -5,9 +5,9 @@
"sources": [ "sources": [
{ {
"sourceId": "gplay", "sourceId": "gplay",
"versionInteger": 757, "versionInteger": 758,
"versionString": "2.59.1", "versionString": "2.62.0",
"downloadUrl": "https://github.com/deltachat/deltachat-android/releases/download/v2.59.1/deltachat-gplay-release-2.59.1.apk" "downloadUrl": "https://github.com/deltachat/deltachat-android/releases/download/v2.62.0/deltachat-gplay-release-2.62.0.apk"
} }
] ]
} }
-23
View File
@@ -99,23 +99,6 @@ def _install_remote_venv_with_chatmaild(deployer) -> None:
dest=remote_dist_file, dest=remote_dist_file,
) )
# Remove venv if its Python major.minor doesn't match the system Python
server.shell(
name="remove stale chatmaild venv if python version changed",
commands=[
"\n".join(
[
r"re='[0-9]+\.[0-9]+'", # major.minor out of 'Python X.Y.Z'
'sys_version=$(python3 --version 2>/dev/null | grep -oE "$re")',
f'venv_version=$({remote_venv_dir}/bin/python --version 2>/dev/null | grep -oE "$re")',
# an empty sys_version means we could not tell: keep the venv
f'[ -z "$sys_version" ] || [ "$sys_version" = "$venv_version" ] '
f"|| rm -rf {remote_venv_dir}",
]
)
],
)
pip.virtualenv( pip.virtualenv(
name=f"chatmaild virtualenv {remote_venv_dir}", name=f"chatmaild virtualenv {remote_venv_dir}",
path=remote_venv_dir, path=remote_venv_dir,
@@ -417,12 +400,6 @@ class ChatmailDeployer(Deployer):
src=BytesIO(b'APT::Install-Recommends "false";\n'), src=BytesIO(b'APT::Install-Recommends "false";\n'),
dest="/etc/apt/apt.conf.d/00InstallRecommends", dest="/etc/apt/apt.conf.d/00InstallRecommends",
) )
# Pin dovecot-* to priority -1 before any apt operation, apt should
# never manage dovecot as our version might be lower than the distro's.
self.put_file(
src=StringIO("Package: dovecot-*\nPin: version *\nPin-Priority: -1\n"),
dest="/etc/apt/preferences.d/pin-dovecot",
)
apt.update(name="apt update", cache_time=24 * 3600) apt.update(name="apt update", cache_time=24 * 3600)
apt.upgrade(name="upgrade apt packages", auto_remove=True) apt.upgrade(name="upgrade apt packages", auto_remove=True)
+32 -49
View File
@@ -1,10 +1,11 @@
import io
import urllib.request 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.deb import DebPackages
from pyinfra.facts.server import Arch, Command, Sysctl from pyinfra.facts.server import Arch, Command, Sysctl
from pyinfra.operations import files, server from pyinfra.operations import apt, files, server
from cmdeploy.basedeploy import ( from cmdeploy.basedeploy import (
Deployer, Deployer,
@@ -15,13 +16,7 @@ from cmdeploy.basedeploy import (
) )
from cmdeploy.pins import DOVECOT_SHA256, DOVECOT_VERSION from cmdeploy.pins import DOVECOT_SHA256, DOVECOT_VERSION
VERSION_ID_CMD = "grep '^VERSION_ID=' /etc/os-release" DOVECOT_PACKAGE_VERSION = f"1:{DOVECOT_VERSION}"
def _stamped_version(deb_release: int) -> str:
"""Version as built, including the per-distro suffix stamped by
chatmail/dovecot CI into package version and filename."""
return f"{DOVECOT_VERSION}+deb{deb_release}u1"
class DovecotDeployer(Deployer): class DovecotDeployer(Deployer):
@@ -34,30 +29,34 @@ class DovecotDeployer(Deployer):
def install(self): def install(self):
arch = host.get_fact(Arch) arch = host.get_fact(Arch)
deb_release = _parse_version_id(host.get_fact(Command, VERSION_ID_CMD))
with blocked_service_startup(): with blocked_service_startup():
debs = [] debs = []
for pkg in ("core", "imapd", "lmtpd", "auth-lua"): for pkg in ("core", "imapd", "lmtpd", "auth-lua"):
deb, changed = _download_dovecot_package(pkg, arch, deb_release) deb, changed = _download_dovecot_package(pkg, arch)
self.need_restart |= changed self.need_restart |= changed
if deb: if deb:
debs.append(deb) debs.append(deb)
if debs: if debs:
deb_list = " ".join(debs) deb_list = " ".join(debs)
# apt-get install with local .deb paths resolves depends # First dpkg may fail on missing dependencies (stderr suppressed);
# against the configured repos (e.g. pulls libwrap0), # apt-get --fix-broken pulls them in, then dpkg retries cleanly.
# The pin file written earlier by ChatmailDeployer prevents apt
# from installing a 'wrong' version
server.shell( server.shell(
name="Install dovecot packages", name="Install dovecot packages",
commands=[ commands=[
"DEBIAN_FRONTEND=noninteractive apt-get install -y " f"dpkg --force-confdef --force-confold -i {deb_list} 2> /dev/null || true",
'-o Dpkg::Options::="--force-confdef" ' "DEBIAN_FRONTEND=noninteractive apt-get -y --fix-broken install",
'-o Dpkg::Options::="--force-confold" ' f"dpkg --force-confdef --force-confold -i {deb_list}",
f"--allow-downgrades {deb_list}",
], ],
) )
self.need_restart = True self.need_restart = True
self.put_file(
src=io.StringIO(
"Package: dovecot-*\n"
"Pin: version *\n"
"Pin-Priority: -1\n"
),
dest="/etc/apt/preferences.d/pin-dovecot",
)
def configure(self): def configure(self):
configure_remote_units(self, self.config.mail_domain_bare, self.units) configure_remote_units(self, self.config.mail_domain_bare, self.units)
@@ -70,7 +69,7 @@ class DovecotDeployer(Deployer):
if not self.disable_mail and not self.need_restart: if not self.disable_mail and not self.need_restart:
stale = host.get_fact( stale = host.get_fact(
Command, Command,
"pid=$(systemctl show -p MainPID --value dovecot.service 2>/dev/null);" 'pid=$(systemctl show -p MainPID --value dovecot.service 2>/dev/null);'
' [ "${pid:-0}" != "0" ] && readlink "/proc/$pid/exe" 2>/dev/null | grep -q "(deleted)"' ' [ "${pid:-0}" != "0" ] && readlink "/proc/$pid/exe" 2>/dev/null | grep -q "(deleted)"'
" && echo STALE || true", " && echo STALE || true",
) )
@@ -85,15 +84,6 @@ class DovecotDeployer(Deployer):
) )
def _parse_version_id(version_line: str) -> int:
"""Debian major release from an /etc/os-release VERSION_ID line."""
_, _, raw = (version_line or "").strip().partition("=")
try:
return int(raw.strip('"'))
except ValueError:
raise ValueError(f"cannot determine Debian release from {version_line!r}")
def _pick_url(primary, fallback): def _pick_url(primary, fallback):
try: try:
req = urllib.request.Request(primary, method="HEAD") req = urllib.request.Request(primary, method="HEAD")
@@ -103,36 +93,27 @@ def _pick_url(primary, fallback):
return fallback return fallback
def _download_dovecot_package(package: str, arch: str, deb_release: int) -> tuple[str | None, bool]: def _download_dovecot_package(package: str, arch: str) -> tuple[str | None, bool]:
"""Download a dovecot .deb if needed, return (path, changed).""" """Download a dovecot .deb if needed, return (path, changed)."""
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
pkg_name = f"dovecot-{package}" pkg_name = f"dovecot-{package}"
try: sha256 = DOVECOT_SHA256.get((package, arch))
# never fall back to the distro package: it is pinned to -1 and would if sha256 is None:
# in any case be a version we did not build and do not support op = apt.packages(packages=[pkg_name])
sha256 = DOVECOT_SHA256[(arch, deb_release, package)] return None, bool(getattr(op, "changed", False))
except KeyError:
raise ValueError(f"no dovecot build for {pkg_name} on deb{deb_release}/{arch}")
stamped_version = _stamped_version(deb_release)
installed_versions = host.get_fact(DebPackages).get(pkg_name, []) installed_versions = host.get_fact(DebPackages).get(pkg_name, [])
if f"1:{stamped_version}" in installed_versions: if DOVECOT_PACKAGE_VERSION in installed_versions:
return None, False return None, False
# Primary URL: flat structure with distro suffix in filename url_version = DOVECOT_VERSION.replace("+", "%2B")
primary_deb = f"{pkg_name}_{stamped_version}_{arch}.deb" deb_base = f"{pkg_name}_{url_version}_{arch}.deb"
primary_url = f"https://download.delta.chat/dovecot/{primary_deb}" primary_url = f"https://download.delta.chat/dovecot/{deb_base}"
# GitHub release files: escaped + in filename; the release tag stays fallback_url = f"https://github.com/chatmail/dovecot/releases/download/upstream%2F{url_version}/{deb_base}"
# distro-neutral, both distros ship in one combined release
tag_version = DOVECOT_VERSION.replace("+", "%2B")
fallback_deb = f"{pkg_name}_{stamped_version.replace('+', '%2B')}_{arch}.deb"
fallback_url = (
f"https://github.com/chatmail/dovecot/releases/download/upstream%2F{tag_version}/{fallback_deb}"
)
url = _pick_url(primary_url, fallback_url) url = _pick_url(primary_url, fallback_url)
deb_filename = f"/root/{primary_deb}" deb_filename = f"/root/{deb_base}"
files.download( files.download(
name=f"Download {pkg_name}", name=f"Download {pkg_name}",
@@ -144,7 +125,6 @@ def _download_dovecot_package(package: str, arch: str, deb_release: int) -> tupl
return deb_filename, True return deb_filename, True
def _configure_dovecot(deployer, config: Config, debug: bool = False): def _configure_dovecot(deployer, config: Config, debug: bool = False):
"""Configures Dovecot IMAP server.""" """Configures Dovecot IMAP server."""
deployer.put_template( deployer.put_template(
@@ -153,6 +133,9 @@ def _configure_dovecot(deployer, config: Config, debug: bool = False):
config=config, config=config,
debug=debug, debug=debug,
disable_ipv6=config.disable_ipv6, disable_ipv6=config.disable_ipv6,
config_dir="/etc/dovecot",
dh_path="/usr/share/dovecot/dh.pem",
quota_expire_bin="/usr/local/lib/chatmaild/venv/bin/chatmail-quota-expire",
) )
deployer.put_template("dovecot/auth.lua.j2", "/etc/dovecot/auth.lua", config=config) deployer.put_template("dovecot/auth.lua.j2", "/etc/dovecot/auth.lua", config=config)
deployer.remove_file("/etc/dovecot/auth.conf") deployer.remove_file("/etc/dovecot/auth.conf")
@@ -62,11 +62,11 @@ imap_capability = +XDELTAPUSH XCHATMAIL
# Authentication for system users. # Authentication for system users.
passdb { passdb {
driver = lua driver = lua
args = file=/etc/dovecot/auth.lua blocking=yes args = file={{ config_dir }}/auth.lua blocking=yes
} }
userdb { userdb {
driver = lua driver = lua
args = file=/etc/dovecot/auth.lua blocking=yes args = file={{ config_dir }}/auth.lua blocking=yes
} }
## ##
## Mailbox locations and namespaces ## Mailbox locations and namespaces
@@ -168,7 +168,7 @@ plugin {
} }
service quota-warning { service quota-warning {
executable = script /usr/local/lib/chatmaild/venv/bin/chatmail-quota-expire executable = script {{ quota_expire_bin }}
user = vmail user = vmail
unix_listener quota-warning { unix_listener quota-warning {
user = vmail user = vmail
@@ -179,7 +179,7 @@ service quota-warning {
# push_notification configuration # push_notification configuration
plugin { plugin {
# <https://doc.dovecot.org/2.3/configuration_manual/push_notification/#lua-lua> # <https://doc.dovecot.org/2.3/configuration_manual/push_notification/#lua-lua>
push_notification_driver = lua:file=/etc/dovecot/push_notification.lua push_notification_driver = lua:file={{ config_dir }}/push_notification.lua
} }
service lmtp { service lmtp {
@@ -254,7 +254,7 @@ service anvil {
ssl = required ssl = required
ssl_cert = <{{ config.tls_cert_path }} ssl_cert = <{{ config.tls_cert_path }}
ssl_key = <{{ config.tls_key_path }} ssl_key = <{{ config.tls_key_path }}
ssl_dh = </usr/share/dovecot/dh.pem ssl_dh = <{{ dh_path }}
ssl_min_protocol = TLSv1.3 ssl_min_protocol = TLSv1.3
ssl_prefer_server_ciphers = yes ssl_prefer_server_ciphers = yes
+4
View File
@@ -55,6 +55,10 @@ def _configure_nginx(deployer, config: Config, debug: bool = False):
"/etc/nginx/nginx.conf", "/etc/nginx/nginx.conf",
config=config, config=config,
disable_ipv6=config.disable_ipv6, disable_ipv6=config.disable_ipv6,
config_dir="/etc/nginx",
stream_module="modules/ngx_stream_module.so",
www_root="/var/www/html",
cgi_dir="/usr/lib/cgi-bin",
) )
deployer.put_template( deployer.put_template(
+8 -7
View File
@@ -1,4 +1,4 @@
load_module modules/ngx_stream_module.so; {% if stream_module %}load_module {{ stream_module }};{% endif %}
user www-data; user www-data;
worker_processes auto; worker_processes auto;
@@ -54,7 +54,7 @@ http {
# Do not emit nginx version on error pages. # Do not emit nginx version on error pages.
server_tokens off; server_tokens off;
include /etc/nginx/mime.types; include {{ config_dir }}/mime.types;
default_type application/octet-stream; default_type application/octet-stream;
ssl_protocols TLSv1.2 TLSv1.3; ssl_protocols TLSv1.2 TLSv1.3;
@@ -68,13 +68,14 @@ http {
listen 127.0.0.1:8443 ssl default_server; listen 127.0.0.1:8443 ssl default_server;
root /var/www/html; root {{ www_root }};
index index.html index.htm; index index.html index.htm;
server_name {{ config.mail_domain }} mta-sts.{{ config.mail_domain }}; server_name {{ config.mail_domain }} mta-sts.{{ config.mail_domain }};
location /mxdeliv { location /mxdeliv {
client_max_body_size {{ config.max_message_size }};
proxy_pass http://127.0.0.1:{{ config.filtermail_http_port_incoming }}; proxy_pass http://127.0.0.1:{{ config.filtermail_http_port_incoming }};
} }
@@ -96,8 +97,8 @@ http {
{% endif %} {% endif %}
fastcgi_pass unix:/run/fcgiwrap.socket; fastcgi_pass unix:/run/fcgiwrap.socket;
include /etc/nginx/fastcgi_params; include {{ config_dir }}/fastcgi_params;
fastcgi_param SCRIPT_FILENAME /usr/lib/cgi-bin/newemail.py; fastcgi_param SCRIPT_FILENAME {{ cgi_dir }}/newemail.py;
} }
# Old URL for compatibility with e.g. printed QR codes. # Old URL for compatibility with e.g. printed QR codes.
@@ -114,8 +115,8 @@ http {
{% endif %} {% endif %}
fastcgi_pass unix:/run/fcgiwrap.socket; fastcgi_pass unix:/run/fcgiwrap.socket;
include /etc/nginx/fastcgi_params; include {{ config_dir }}/fastcgi_params;
fastcgi_param SCRIPT_FILENAME /usr/lib/cgi-bin/newemail.py; fastcgi_param SCRIPT_FILENAME {{ cgi_dir }}/newemail.py;
} }
# Proxy to iroh-relay service. # Proxy to iroh-relay service.
+1 -1
View File
@@ -1 +1 @@
{{ config.opendkim_selector }}._domainkey.{{ config.domain_name }} {{ config.domain_name }}:{{ config.opendkim_selector }}:/etc/dkimkeys/{{ config.opendkim_selector }}.private {{ config.opendkim_selector }}._domainkey.{{ config.domain_name }} {{ config.domain_name }}:{{ config.opendkim_selector }}:{{ keys_dir }}/{{ config.opendkim_selector }}.private
@@ -30,6 +30,8 @@ class OpendkimDeployer(Deployer):
"opendkim/opendkim.conf", "opendkim/opendkim.conf",
"/etc/opendkim.conf", "/etc/opendkim.conf",
config={"domain_name": domain, "opendkim_selector": dkim_selector}, config={"domain_name": domain, "opendkim_selector": dkim_selector},
keys_dir="/etc/dkimkeys",
trust_anchor="/usr/share/dns/root.key",
) )
self.remove_file("/etc/opendkim/screen.lua") self.remove_file("/etc/opendkim/screen.lua")
@@ -46,6 +48,7 @@ class OpendkimDeployer(Deployer):
"/etc/dkimkeys/KeyTable", "/etc/dkimkeys/KeyTable",
owner="opendkim", owner="opendkim",
config={"domain_name": domain, "opendkim_selector": dkim_selector}, config={"domain_name": domain, "opendkim_selector": dkim_selector},
keys_dir="/etc/dkimkeys",
) )
self.put_template( self.put_template(
+4 -4
View File
@@ -21,9 +21,9 @@ DNSTimeout 60
# setup options can be found in /usr/share/doc/opendkim/README.opendkim. # setup options can be found in /usr/share/doc/opendkim/README.opendkim.
Domain {{ config.domain_name }} Domain {{ config.domain_name }}
Selector {{ config.opendkim_selector }} Selector {{ config.opendkim_selector }}
KeyFile /etc/dkimkeys/{{ config.opendkim_selector }}.private KeyFile {{ keys_dir }}/{{ config.opendkim_selector }}.private
KeyTable /etc/dkimkeys/KeyTable KeyTable {{ keys_dir }}/KeyTable
SigningTable refile:/etc/dkimkeys/SigningTable SigningTable refile:{{ keys_dir }}/SigningTable
# Sign Autocrypt header in addition to the default specified in RFC 6376. # Sign Autocrypt header in addition to the default specified in RFC 6376.
# #
@@ -58,7 +58,7 @@ PidFile /run/opendkim/opendkim.pid
# The trust anchor enables DNSSEC. In Debian, the trust anchor file is provided # The trust anchor enables DNSSEC. In Debian, the trust anchor file is provided
# by the package dns-root-data. # by the package dns-root-data.
TrustAnchorFile /usr/share/dns/root.key TrustAnchorFile {{ trust_anchor }}
# 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
+12 -21
View File
@@ -1,14 +1,14 @@
"""Versions, hashes, and download URLs for pre-built artifacts fetched during deploy.""" """Versions, hashes, and download URLs for pre-built artifacts fetched during deploy."""
FILTERMAIL_VERSION = "v0.7.4" FILTERMAIL_VERSION = "v0.7.7"
FILTERMAIL_ARTIFACTS = { FILTERMAIL_ARTIFACTS = {
"x86_64": ( "x86_64": (
f"https://github.com/chatmail/filtermail/releases/download/{FILTERMAIL_VERSION}/filtermail-x86_64", f"https://github.com/chatmail/filtermail/releases/download/{FILTERMAIL_VERSION}/filtermail-x86_64",
"484cb8dff083134aefba9fce4a6b7ef4784a0f0e28e5108ecf8bb9e58a44fd2c", "0691debf501f854f4e6a9dd6516f3a0ef03d95721a9b540309014bfe1a1f52b1",
), ),
"aarch64": ( "aarch64": (
f"https://github.com/chatmail/filtermail/releases/download/{FILTERMAIL_VERSION}/filtermail-aarch64", f"https://github.com/chatmail/filtermail/releases/download/{FILTERMAIL_VERSION}/filtermail-aarch64",
"66aa0ca2ca9add7a12d92883d76f8786384092adfde24a3d3a1d0b1f30d23a9e", "964f85df8b65b812666113f968cbfdd8da88ce16d218284deb359c1e2400d2da",
), ),
"mtail": ( "mtail": (
f"https://raw.githubusercontent.com/chatmail/filtermail/{FILTERMAIL_VERSION}/contrib/filtermail.mtail", f"https://raw.githubusercontent.com/chatmail/filtermail/{FILTERMAIL_VERSION}/contrib/filtermail.mtail",
@@ -28,25 +28,16 @@ MTAIL_ARTIFACTS = {
), ),
} }
# distro-neutral base version, as committed in chatmail/dovecot debian/changelog DOVECOT_VERSION = "2.3.21+dfsg1-3"
DOVECOT_VERSION = "2.3.21+dfsg1-3+chatmail2"
DOVECOT_SHA256 = { DOVECOT_SHA256 = {
("amd64", 12, "auth-lua"): "ef1b8e1db45147a74b48d63125bd61b2cc2f250e1006656ba1c58b9c12f5cde6", ("core", "amd64"): "dd060706f52a306fa863d874717210b9fe10536c824afe1790eec247ded5b27d",
("arm64", 12, "auth-lua"): "c1a06ee9374439893e397ba3b0cacf532a733290c184f4f30ea39df8699be329", ("core", "arm64"): "e7548e8a82929722e973629ecc40fcfa886894cef3db88f23535149e7f730dc9",
("amd64", 13, "auth-lua"): "6c0946d2516efcbcaa09a27df9b8ea701861cce270d71941056b3c69831a5ea2", ("imapd", "amd64"): "8d8dc6fc00bbb6cdb25d345844f41ce2f1c53f764b79a838eb2a03103eebfa86",
("arm64", 13, "auth-lua"): "5e6c9cfe47f7f3b8aa0d68a3e607161b6abaee1ad696cb1419e997b3099b2985", ("imapd", "arm64"): "178fa877ddd5df9930e8308b518f4b07df10e759050725f8217a0c1fb3fd707f",
("amd64", 12, "core"): "ac3977264d9b9a6fcec53fd3f5cdd2a79ca8aa0324de530c07e535008540826e", ("lmtpd", "amd64"): "2f69ba5e35363de50962d42cccbfe4ed8495265044e244007d7ccddad77513ab",
("arm64", 12, "core"): "21626c9c9b52cbdcf1a17b5c09e3c4043e69aa371bf83cc2fcb3b7ddaecdc109", ("lmtpd", "arm64"): "89f52fb36524f5877a177dff4a713ba771fd3f91f22ed0af7238d495e143b38f",
("amd64", 13, "core"): "47c242ef23c17e700ac19d52d82c9fdb2ebd757d8beb3a7f6781d2de59f87bd0", ("auth-lua", "amd64"): "d724f37712faba52e177153114af1831e54da555c57c6474c05f96f176176ce4",
("arm64", 13, "core"): "c14c53f112c875f698c4cb6e5870c605cd0a9dd98d35a66e94ceb1827f8020a3", ("auth-lua", "arm64"): "7272768e20de148c35891d99cd60205acbe7e732b056caf0a83e8194d49136e6",
("amd64", 12, "imapd"): "92a7ab5fc7dc32886a0c34404f919f1335d397b48c467e0c1ef77e56978f60ea",
("arm64", 12, "imapd"): "9369fd566fec4df109ef23debf34ea0417ae85beb29cbe7de619d4d1f31b120c",
("amd64", 13, "imapd"): "e38cc1266455f937ed62f971ea859c47e1a99247841ed0ad946963b524cfdbc5",
("arm64", 13, "imapd"): "11d97dabf23171b37f8b1335dfdb81d408f8b95391aea6d4066aecc9fde01dfe",
("amd64", 12, "lmtpd"): "dc3de473789969f7dd3504ac8783da5e42a446d2d7a305a4e9d7081a6dfe71ab",
("arm64", 12, "lmtpd"): "ae2cbd6c5c43f6d8e2172997b055448f4c79238e2f99cd9ab9200a7d9f548908",
("amd64", 13, "lmtpd"): "833b243e28c7baff141ecf37456e310f5d836e7944a3b9f2fe5074adf0d6a418",
("arm64", 13, "lmtpd"): "55af47a121ba7e23966b20ddaab2dff7feba4b34677864e045e31a702afa180d",
} }
TURN_VERSION = "v0.4" TURN_VERSION = "v0.4"
TURN_ARTIFACTS = { TURN_ARTIFACTS = {
@@ -24,6 +24,8 @@ class PostfixDeployer(Deployer):
"/etc/postfix/main.cf", "/etc/postfix/main.cf",
config=config, config=config,
disable_ipv6=config.disable_ipv6, disable_ipv6=config.disable_ipv6,
config_dir="/etc/postfix",
ca_path="/etc/ssl/certs",
) )
self.put_template( self.put_template(
@@ -31,6 +33,7 @@ class PostfixDeployer(Deployer):
"/etc/postfix/master.cf", "/etc/postfix/master.cf",
debug=False, debug=False,
config=config, config=config,
config_dir="/etc/postfix",
) )
self.put_file( self.put_file(
+4 -4
View File
@@ -19,13 +19,13 @@ smtpd_tls_cert_file={{ config.tls_cert_path }}
smtpd_tls_key_file={{ config.tls_key_path }} 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={{ ca_path }}
smtp_tls_security_level=verify smtp_tls_security_level=verify
# 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
smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache
smtp_tls_policy_maps = regexp:/etc/postfix/smtp_tls_policy_map smtp_tls_policy_maps = regexp:{{ config_dir }}/smtp_tls_policy_map
smtp_tls_protocols = >=TLSv1.2 smtp_tls_protocols = >=TLSv1.2
smtp_tls_mandatory_protocols = >=TLSv1.2 smtp_tls_mandatory_protocols = >=TLSv1.2
@@ -83,7 +83,7 @@ inet_protocols = ipv4
inet_protocols = all inet_protocols = all
{% endif %} {% endif %}
lmtp_header_checks = regexp:/etc/postfix/lmtp_header_cleanup lmtp_header_checks = regexp:{{ config_dir }}/lmtp_header_cleanup
# Do not apply header checks to MIME headers # Do not apply header checks to MIME headers
# and other headers that are actually part of the message body. # and other headers that are actually part of the message body.
@@ -98,7 +98,7 @@ mua_sender_restrictions = reject_sender_login_mismatch, permit_sasl_authenticate
mua_helo_restrictions = permit_mynetworks, reject_invalid_helo_hostname, reject_non_fqdn_helo_hostname, permit mua_helo_restrictions = permit_mynetworks, reject_invalid_helo_hostname, reject_non_fqdn_helo_hostname, permit
# 1:1 map MAIL FROM to SASL login name. # 1:1 map MAIL FROM to SASL login name.
smtpd_sender_login_maps = regexp:/etc/postfix/login_map smtpd_sender_login_maps = regexp:{{ config_dir }}/login_map
# Do not lookup SMTP client hostnames to reduce delays # Do not lookup SMTP client hostnames to reduce delays
# and avoid unnecessary DNS requests. # and avoid unnecessary DNS requests.
+1 -1
View File
@@ -102,7 +102,7 @@ postlog unix-dgram n - n - 1 postlogd
# to make sure the users # to make sure the users
# cannot send unprotected Subject. # cannot send unprotected Subject.
authclean unix n - - - 0 cleanup authclean unix n - - - 0 cleanup
-o header_checks=regexp:/etc/postfix/submission_header_cleanup -o header_checks=regexp:{{ config_dir }}/submission_header_cleanup
# Reducing `maxproc` here may result in a head of line blocking # Reducing `maxproc` here may result in a head of line blocking
# when there are many messages sent to unreachable destinations # when there are many messages sent to unreachable destinations
@@ -91,7 +91,7 @@ def test_concurrent_logins_same_account(
def test_no_vrfy(cmfactory, chatmail_config, maildomain): def test_no_vrfy(cmfactory, chatmail_config, maildomain):
ac = cmfactory.get_online_account() ac = cmfactory.get_online_account()
addr = ac.get_config("addr") addr = ac.list_transports()[0]["addr"]
s = smtplib.SMTP(maildomain) s = smtplib.SMTP(maildomain)
s.starttls() s.starttls()
@@ -5,6 +5,7 @@ import subprocess
import time import time
import pytest import pytest
import requests
from chatmaild.config import is_valid_ipv4 from chatmaild.config import is_valid_ipv4
from cmdeploy import remote from cmdeploy import remote
@@ -132,8 +133,8 @@ def test_use_two_chatmailservers(cmfactory, maildomain2):
ac1 = cmfactory.get_online_account() ac1 = cmfactory.get_online_account()
ac2 = cmfactory.get_online_account(domain=maildomain2) ac2 = cmfactory.get_online_account(domain=maildomain2)
cmfactory.get_accepted_chat(ac1, ac2) cmfactory.get_accepted_chat(ac1, ac2)
domain1 = ac1.get_config("addr").split("@")[1] domain1 = ac1.list_transports()[0]["addr"].split("@")[1]
domain2 = ac2.get_config("addr").split("@")[1] domain2 = ac2.list_transports()[0]["addr"].split("@")[1]
assert domain1 != domain2 assert domain1 != domain2
@@ -326,3 +327,14 @@ def test_nginx_access_log_only_defined_once(sshdomain):
assert len(access_logs) == 1, ( assert len(access_logs) == 1, (
f"expected 1 access_log, found {len(access_logs)}: {access_logs}" f"expected 1 access_log, found {len(access_logs)}: {access_logs}"
) )
@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning")
def test_mxdeliv_respects_max_message_size(maildomain, chatmail_config):
url = f"https://{maildomain}/mxdeliv"
verify = chatmail_config.tls_cert_mode == "acme"
size = chatmail_config.max_message_size
res = requests.post(url, data=b"x" * size, verify=verify)
assert res.text == "500 Invalid DATA"
res = requests.post(url, data=b"x" * (size + 1), verify=verify)
assert res.status_code == 413
@@ -15,8 +15,10 @@ from cmdeploy.remote import rshell
@pytest.fixture @pytest.fixture
def imap_mailbox(cmfactory, ssl_context): def imap_mailbox(cmfactory, ssl_context):
(ac1,) = cmfactory.get_online_accounts(1) (ac1,) = cmfactory.get_online_accounts(1)
user = ac1.get_config("addr") # Delta Chat 2.63 stopped writing the legacy addr and mail_pw config keys.
password = ac1.get_config("mail_pw") transport = ac1.list_transports()[0]
user = transport["addr"]
password = transport["password"]
host = user.split("@")[1].strip("[").strip("]") host = user.split("@")[1].strip("[").strip("]")
mailbox = imap_tools.MailBox(host, ssl_context=ssl_context) mailbox = imap_tools.MailBox(host, ssl_context=ssl_context)
mailbox.login(user, password) mailbox.login(user, password)
@@ -88,7 +90,7 @@ class TestEndToEndDeltaChat:
ac1, ac2 = cmfactory.get_online_accounts(2) ac1, ac2 = cmfactory.get_online_accounts(2)
chat = cmfactory.get_accepted_chat(ac1, ac2) chat = cmfactory.get_accepted_chat(ac1, ac2)
user = ac2.get_config("configured_addr") user = ac2.list_transports()[0]["addr"]
def parse_size_limit(limit: str) -> int: def parse_size_limit(limit: str) -> int:
"""Parse a size limit and return the number of bytes as integer. """Parse a size limit and return the number of bytes as integer.
@@ -190,9 +192,10 @@ def test_hide_senders_ip_address(cmfactory, ssl_context):
chat.send_text("testing submission header cleanup") chat.send_text("testing submission header cleanup")
user2.wait_for_incoming_msg() user2.wait_for_incoming_msg()
addr = user2.get_config("addr") transport = user2.list_transports()[0]
addr = transport["addr"]
host = addr.split("@")[1].strip("[").strip("]") host = addr.split("@")[1].strip("[").strip("]")
pw = user2.get_config("mail_pw") pw = transport["password"]
mailbox = imap_tools.MailBox(host, ssl_context=ssl_context) mailbox = imap_tools.MailBox(host, ssl_context=ssl_context)
mailbox.login(addr, pw) mailbox.login(addr, pw)
msgs = list(mailbox.fetch(mark_seen=False)) msgs = list(mailbox.fetch(mark_seen=False))
@@ -3,40 +3,29 @@ from types import SimpleNamespace
import pytest import pytest
from pyinfra.facts.deb import DebPackages from pyinfra.facts.deb import DebPackages
from pyinfra.facts.server import Command
from cmdeploy.dovecot import deployer as dovecot_deployer from cmdeploy.dovecot import deployer as dovecot_deployer
def _fact_name(key):
if isinstance(key, tuple):
return f"{key[0].__name__}{key[1:]!r}"
return key.__name__
def make_host(*fact_pairs): def make_host(*fact_pairs):
"""Build a mock host; get_fact() dispatches to the provided facts mapping. """Build a mock host; get_fact(cls) dispatches to the provided facts mapping.
Args: Args:
*fact_pairs: (fact_class, value) to match any call of that fact, or *fact_pairs: tuples of (fact_class, fact_value) to register
((fact_class, *args), value) to match one specific call. Needed
for Command, which install() and check_restart() invoke with
different scripts; a bare Command entry would serve both.
Returns: Returns:
SimpleNamespace with get_fact that raises a clear error if an SimpleNamespace with get_fact that raises a clear error if an
unregistered fact is requested. unexpected fact type is requested.
""" """
facts = dict(fact_pairs) facts = dict(fact_pairs)
def get_fact(cls, *args): def get_fact(cls):
for key in ((cls, *args), cls): if cls not in facts:
if key in facts: registered = ", ".join(c.__name__ for c in facts)
return facts[key] raise LookupError(
registered = ", ".join(_fact_name(k) for k in facts) f"unexpected get_fact({cls.__name__}); only registered: {registered}"
raise LookupError( )
f"unexpected get_fact({_fact_name((cls, *args))}); only registered: {registered}" return facts[cls]
)
return SimpleNamespace(get_fact=get_fact) return SimpleNamespace(get_fact=get_fact)
@@ -75,9 +64,7 @@ def track_shell(monkeypatch):
def test_download_dovecot_package_skips_epoch_matched_install(monkeypatch): def test_download_dovecot_package_skips_epoch_matched_install(monkeypatch):
# what dpkg reports after installing our deb: epoch + the +debNu1 suffix epoch_version = dovecot_deployer.DOVECOT_PACKAGE_VERSION
# that chatmail/dovecot CI stamps via dch before building
epoch_version = f"1:{dovecot_deployer._stamped_version(12)}"
downloads = [] downloads = []
monkeypatch.setattr( monkeypatch.setattr(
dovecot_deployer, dovecot_deployer,
@@ -95,17 +82,15 @@ def test_download_dovecot_package_skips_epoch_matched_install(monkeypatch):
lambda **kwargs: downloads.append(kwargs), lambda **kwargs: downloads.append(kwargs),
) )
deb, changed = dovecot_deployer._download_dovecot_package("core", "amd64", deb_release=12) deb, changed = dovecot_deployer._download_dovecot_package("core", "amd64")
assert deb is None, f"expected no deb path when version matches, got {deb!r}" assert deb is None, f"expected no deb path when version matches, got {deb!r}"
assert changed is False, "should not flag changed when version already installed" assert changed is False, "should not flag changed when version already installed"
assert downloads == [], "should not download when version already installed" assert downloads == [], "should not download when version already installed"
@pytest.mark.parametrize("deb_release", [12, 13])
@pytest.mark.parametrize("arch", ["amd64", "arm64"])
def test_download_dovecot_package_uses_archive_version_for_url_and_filename( def test_download_dovecot_package_uses_archive_version_for_url_and_filename(
monkeypatch, deb_release, arch monkeypatch,
): ):
downloads = [] downloads = []
monkeypatch.setattr( monkeypatch.setattr(
@@ -124,26 +109,18 @@ def test_download_dovecot_package_uses_archive_version_for_url_and_filename(
lambda **kwargs: downloads.append(kwargs), lambda **kwargs: downloads.append(kwargs),
) )
deb, changed = dovecot_deployer._download_dovecot_package( deb, changed = dovecot_deployer._download_dovecot_package("core", "amd64")
"core", arch, deb_release=deb_release
)
stamped = dovecot_deployer._stamped_version(deb_release) archive_version = dovecot_deployer.DOVECOT_VERSION.replace("+", "%2B")
expected_deb = f"/root/dovecot-core_{stamped}_{arch}.deb" expected_deb = f"/root/dovecot-core_{archive_version}_amd64.deb"
# path uses the stamped version, and deb filenames never carry the epoch # Verify the returned path uses archive version, not package version (with epoch)
assert changed is True, "should flag changed when package not yet installed" assert changed is True, "should flag changed when package not yet installed"
assert deb == expected_deb, f"deb path mismatch: {deb!r} != {expected_deb!r}" assert deb == expected_deb, f"deb path mismatch: {deb!r} != {expected_deb!r}"
assert "1:" not in deb, f"deb filename must not contain the epoch, got {deb!r}" assert dovecot_deployer.DOVECOT_PACKAGE_VERSION not in deb, (
assert len(downloads) == 1, "files.download should be called exactly once" f"deb path should use archive version (no epoch), got {deb!r}"
# the checksum is the security boundary: verify the right table row is used
assert (
downloads[0]["sha256sum"]
== dovecot_deployer.DOVECOT_SHA256[(arch, deb_release, "core")]
), "must pass the sha256 matching (arch, release, package)"
assert f"deb{deb_release}u1" in downloads[0]["src"], (
f"download URL should carry the deb{deb_release} suffix, got {downloads[0]['src']!r}"
) )
assert len(downloads) == 1, "files.download should be called exactly once"
def test_install_skips_dpkg_path_when_epoch_matched_packages_present( def test_install_skips_dpkg_path_when_epoch_matched_packages_present(
@@ -156,14 +133,13 @@ def test_install_skips_dpkg_path_when_epoch_matched_packages_present(
( (
dovecot_deployer.DebPackages, dovecot_deployer.DebPackages,
{ {
"dovecot-core": [f"1:{dovecot_deployer._stamped_version(12)}"], "dovecot-core": [dovecot_deployer.DOVECOT_PACKAGE_VERSION],
"dovecot-imapd": [f"1:{dovecot_deployer._stamped_version(12)}"], "dovecot-imapd": [dovecot_deployer.DOVECOT_PACKAGE_VERSION],
"dovecot-lmtpd": [f"1:{dovecot_deployer._stamped_version(12)}"], "dovecot-lmtpd": [dovecot_deployer.DOVECOT_PACKAGE_VERSION],
"dovecot-auth-lua": [f"1:{dovecot_deployer._stamped_version(12)}"], "dovecot-auth-lua": [dovecot_deployer.DOVECOT_PACKAGE_VERSION],
}, },
), ),
(dovecot_deployer.Arch, "x86_64"), (dovecot_deployer.Arch, "x86_64"),
((Command, dovecot_deployer.VERSION_ID_CMD), 'VERSION_ID="12"'),
), ),
) )
downloads = [] downloads = []
@@ -177,26 +153,44 @@ def test_install_skips_dpkg_path_when_epoch_matched_packages_present(
assert downloads == [], "should not download when all packages epoch-matched" assert downloads == [], "should not download when all packages epoch-matched"
assert track_shell == [], "should not run dpkg when all packages epoch-matched" assert track_shell == [], "should not run dpkg when all packages epoch-matched"
assert deployer.need_restart is False, "need_restart should be False when nothing changed" assert deployer.need_restart is False, (
"need_restart should be False when nothing changed"
)
def test_install_unsupported_arch_raises( def test_install_unsupported_arch_falls_back_to_apt(
deployer, patch_blocked, mock_files_put, track_shell, monkeypatch deployer, patch_blocked, mock_files_put, track_shell, monkeypatch
): ):
# For unsupported architectures, all fact lookups return the arch string.
monkeypatch.setattr( monkeypatch.setattr(
dovecot_deployer, dovecot_deployer,
"host", "host",
make_host( SimpleNamespace(get_fact=lambda cls: "riscv64"),
(dovecot_deployer.Arch, "riscv64"),
((Command, dovecot_deployer.VERSION_ID_CMD), 'VERSION_ID="12"'),
),
) )
apt_calls = []
# we never fall back to the pinned distro package # Mirrors apt.packages() return value: OperationMeta with .changed property.
with pytest.raises(ValueError, match="no dovecot build for dovecot-core"): # Only lmtpd triggers a change to verify |= accumulation of changed flags.
deployer.install() def fake_apt(**kwargs):
apt_calls.append(kwargs)
changed = "lmtpd" in kwargs["packages"][0]
return SimpleNamespace(changed=changed)
assert track_shell == [], "should not run apt-get for unsupported arch" monkeypatch.setattr(dovecot_deployer.apt, "packages", fake_apt)
deployer.install()
actual_pkgs = [c["packages"] for c in apt_calls]
assert actual_pkgs == [
["dovecot-core"],
["dovecot-imapd"],
["dovecot-lmtpd"],
["dovecot-auth-lua"],
], f"expected apt install of core/imapd/lmtpd/auth-lua, got {actual_pkgs}"
assert track_shell == [], "should not run dpkg for unsupported arch"
assert deployer.need_restart is True, (
"need_restart should be True when apt installed a package"
)
def test_install_runs_dpkg_when_packages_need_download( def test_install_runs_dpkg_when_packages_need_download(
@@ -208,7 +202,6 @@ def test_install_runs_dpkg_when_packages_need_download(
make_host( make_host(
(dovecot_deployer.DebPackages, {}), (dovecot_deployer.DebPackages, {}),
(dovecot_deployer.Arch, "x86_64"), (dovecot_deployer.Arch, "x86_64"),
((Command, dovecot_deployer.VERSION_ID_CMD), 'VERSION_ID="12"'),
), ),
) )
monkeypatch.setattr( monkeypatch.setattr(
@@ -224,15 +217,17 @@ def test_install_runs_dpkg_when_packages_need_download(
deployer.install() deployer.install()
assert len(track_shell) == 1, f"expected one server.shell() call for dpkg install, got {len(track_shell)}" assert len(track_shell) == 1, (
f"expected one server.shell() call for dpkg install, got {len(track_shell)}"
)
cmds = track_shell[0]["commands"] cmds = track_shell[0]["commands"]
assert len(cmds) == 1, f"expected single apt-get install command, got: {cmds}" assert len(cmds) == 3, f"expected 3 dpkg/apt commands, got: {cmds}"
assert "apt-get install -y" in cmds[0] assert cmds[0].startswith("dpkg --force-confdef --force-confold -i ")
assert '-o Dpkg::Options::="--force-confdef"' in cmds[0] assert "apt-get -y --fix-broken install" in cmds[1]
assert '-o Dpkg::Options::="--force-confold"' in cmds[0] assert cmds[2].startswith("dpkg --force-confdef --force-confold -i ")
assert "--allow-downgrades" in cmds[0] assert deployer.need_restart is True, (
assert ".deb" in cmds[0] "need_restart should be True after dpkg install"
assert deployer.need_restart is True, "need_restart should be True after dpkg install" )
def test_pick_url_falls_back_on_primary_error(monkeypatch): def test_pick_url_falls_back_on_primary_error(monkeypatch):
@@ -241,45 +236,6 @@ def test_pick_url_falls_back_on_primary_error(monkeypatch):
monkeypatch.setattr(dovecot_deployer.urllib.request, "urlopen", raise_error) monkeypatch.setattr(dovecot_deployer.urllib.request, "urlopen", raise_error)
result = dovecot_deployer._pick_url("http://primary", "http://fallback") result = dovecot_deployer._pick_url("http://primary", "http://fallback")
assert result == "http://fallback", f"should fall back when primary fails, got {result!r}" assert result == "http://fallback", (
f"should fall back when primary fails, got {result!r}"
def test_install_fails_on_unsupported_debian_version(deployer, patch_blocked, monkeypatch):
monkeypatch.setattr(
dovecot_deployer,
"host",
make_host(
(dovecot_deployer.Arch, "x86_64"),
((Command, dovecot_deployer.VERSION_ID_CMD), 'VERSION_ID="99"'),
),
) )
with pytest.raises(ValueError, match="no dovecot build for dovecot-core on deb99"):
deployer.install()
@pytest.mark.parametrize(
"version_line", ["", None, "ID=debian"], ids=["empty", "none", "no-version-id"]
)
def test_parse_version_id_raises_without_version_id(version_line):
with pytest.raises(ValueError, match="cannot determine Debian release"):
dovecot_deployer._parse_version_id(version_line)
@pytest.mark.parametrize("deb_release", [12, 13])
def test_parse_version_id(deb_release):
parsed = dovecot_deployer._parse_version_id(f'VERSION_ID="{deb_release}"\n')
assert parsed == deb_release
def test_dovecot_sha256_covers_all_packages_per_release():
"""Every release in the table needs all four packages on both arches."""
table = dovecot_deployer.DOVECOT_SHA256
expected = {
(arch, pkg)
for arch in ("amd64", "arm64")
for pkg in ("core", "imapd", "lmtpd", "auth-lua")
}
for release in {r for _, r, _ in table}:
got = {(arch, pkg) for arch, r, pkg in table if r == release}
assert got == expected, f"deb{release} incomplete: {sorted(expected - got)}"
+11 -3
View File
@@ -63,9 +63,17 @@ and run the following commands:
scripts/initenv.sh scripts/initenv.sh
scripts/cmdeploy run scripts/cmdeploy run
If you don't want the latest development version, To upgrade to the latest tag,
but a specific tagged release like `1.10.0 <https://github.com/chatmail/relay/releases/tag/1.10.0>`_, ``cd`` into your local checkout of https://github.com/chatmail/relay/
run ``git pull origin 1.10.0`` instead. and run the following commands:
::
git fetch --tags
latestTag=$(git describe --tags "$(git rev-list --tags --max-count=1)")
git checkout $latestTag
scripts/initenv.sh
scripts/cmdeploy run
If you made local changes for your setup, If you made local changes for your setup,
they will be reapplied as long as they don't conflict with the upgrade. they will be reapplied as long as they don't conflict with the upgrade.