mirror of
https://github.com/chatmail/relay.git
synced 2026-08-14 12:30:52 +00:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 33f9cddb1b | |||
| dc8e0a34a2 | |||
| efc24fcdf3 | |||
| 9a9bda80b1 | |||
| 74f4721f2b |
@@ -20,8 +20,6 @@ concurrency:
|
||||
jobs:
|
||||
no-dns:
|
||||
name: LXC deploy and test
|
||||
permissions:
|
||||
contents: read
|
||||
uses: chatmail/cmlxc/.github/workflows/lxc-test.yml@main
|
||||
with:
|
||||
cmlxc_version: main
|
||||
|
||||
@@ -57,8 +57,6 @@ jobs:
|
||||
|
||||
lxc-test:
|
||||
name: LXC deploy and test
|
||||
permissions:
|
||||
contents: read
|
||||
uses: chatmail/cmlxc/.github/workflows/lxc-test.yml@main
|
||||
with:
|
||||
cmlxc_version: main
|
||||
|
||||
@@ -47,5 +47,5 @@ jobs:
|
||||
mkdir -p "$HOME/.ssh"
|
||||
echo "${{ secrets.CHATMAIL_STAGING_SSHKEY }}" > "$HOME/.ssh/key"
|
||||
chmod 600 "$HOME/.ssh/key"
|
||||
rsync -rILvh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/doc/build/ "${{ secrets.USERNAME }}@chatmail.at:/var/www/html/chatmail.at/doc/relay/"
|
||||
rsync -rILvh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/doc/build/ "${{ secrets.USERNAME }}@chatmail.at:"
|
||||
|
||||
|
||||
@@ -1,5 +1,60 @@
|
||||
# Changelog for chatmail deployment
|
||||
|
||||
## [1.12.0] - 2026-07-31
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- [**breaking**] Introduce configurable system limits to reject new address creation and limit imap/smtp connections.
|
||||
Dovecot default connection limit lowered from 50k to 10k,
|
||||
Postfix default connection limit lowered from 5k to 1k,
|
||||
larger relays need to adjust their settings.
|
||||
|
||||
### Features
|
||||
|
||||
- Reduce maximal_queue_lifetime from 5d to 2d
|
||||
- Disable negative cache in unbound (#992)
|
||||
- *(mtail)* Add incoming_mailer_daemon_mail_count
|
||||
- *(postfix)* Disable processing of MIME headers
|
||||
- *(dovecot)* Advertise privacy_mail as admin contact, drop server comment
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Set relay restrictions per smtpd service with default reject
|
||||
- Reduce maxproc for filtermail-transport LMTP client to 500
|
||||
- Core 2.50.0 does not have delete_server_after config anymore.
|
||||
- Check if all required ports are available for filtermail (#983)
|
||||
- Always deploy unbound.conf.d/chatmail.conf (#993)
|
||||
- Expire empty directories (#994)
|
||||
- Crypt-r dependency was declared for wrong Python version
|
||||
- Always overwrite /etc/resolv.conf, even if it is a symbolic link
|
||||
- Pass kwargs to files.put()
|
||||
- List Iroh proxy endpoints used by 0.35 and 1.0, drop stale /relay/probe from earlier versions
|
||||
- Fix port discovery when ss -tulpn shows dovecot before stats
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add scripts/initenv.sh to upgrade instructions
|
||||
- Update overview diagrams (#995)
|
||||
- *(overview)* Remove mermaid styles from 'Accepting and delivering mail' (#1009)
|
||||
- *(README.md)* Clarify security enforcement (#1011)
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- *(ci)* Auto-trigger docker build on release tag push
|
||||
- *(acmetool)* Update let's encrypt ToS link to 1.8
|
||||
- *(ci)* Update doc staging upload path
|
||||
- *(ci)* Fix docs upload path
|
||||
|
||||
### Refactor
|
||||
|
||||
- *(postfix)* Remove unused "filter" lmtp service
|
||||
- Install dns-root-data instead of using unbound-anchor
|
||||
- *(deps)* Remove domain-validator dependency
|
||||
|
||||
### Testing
|
||||
|
||||
- Set socket security for IMAP and SMTP to "TLS" in "dclogin"
|
||||
|
||||
## [1.11.0] - 2026-05-15
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
@@ -5,3 +5,6 @@ We use [git-cliff] to generate the changelog from commit messages before the rel
|
||||
|
||||
[Conventional Commits]: https://www.conventionalcommits.org/
|
||||
[git-cliff]: https://git-cliff.org/
|
||||
|
||||
To update client app version information,
|
||||
edit [chatmaild/src/chatmaild/defaults/appversions.json](chatmaild/src/chatmaild/defaults/appversions.json).
|
||||
|
||||
+4
-6
@@ -1,15 +1,13 @@
|
||||
# Releasing a new version of chatmail relay
|
||||
|
||||
For example, to release version 1.9.0 of chatmail relay, do the following steps.
|
||||
For example, to release version 1.13.0 of chatmail relay, do the following steps.
|
||||
|
||||
1. Update the changelog: `git cliff --unreleased --tag 1.9.0 --prepend CHANGELOG.md` or `git cliff -u -t 1.9.0 -p CHANGELOG.md`.
|
||||
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`.
|
||||
|
||||
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. Tag the release: `git tag --annotate 1.9.0`.
|
||||
4. Open a PR with the new commit, merge it to main after review.
|
||||
|
||||
4. Push the release tag: `git push origin 1.9.0`.
|
||||
|
||||
5. Create a GitHub release: `gh release create 1.9.0`.
|
||||
5. In the web interface, create a GitHub release, tell it to create a new tag.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
include src/chatmaild/defaults/*.json
|
||||
include src/chatmaild/ini/*.ini.f
|
||||
include src/chatmaild/ini/*.ini
|
||||
include src/chatmaild/tests/mail-data/*
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"clients": [
|
||||
{
|
||||
"clientId": "deltachat",
|
||||
"sources": [
|
||||
{
|
||||
"sourceId": "gplay",
|
||||
"versionInteger": 754,
|
||||
"versionString": "2.57.0",
|
||||
"downloadUrl": "https://github.com/deltachat/deltachat-android/releases/download/v2.57.0/deltachat-gplay-release-2.57.0.apk"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
import json
|
||||
import logging
|
||||
import socket
|
||||
import sys
|
||||
import time
|
||||
from contextlib import contextmanager
|
||||
from importlib.resources import files
|
||||
|
||||
from .config import read_config
|
||||
from .dictproxy import DictProxy
|
||||
@@ -18,6 +20,18 @@ def turn_credentials(turn_socket_path):
|
||||
return file.readline().decode("utf-8").strip()
|
||||
|
||||
|
||||
def read_appversions(path):
|
||||
try:
|
||||
data = json.loads(path.read_bytes())
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
except (OSError, ValueError):
|
||||
logging.exception(f"failed to read {path}")
|
||||
return None
|
||||
# the dict protocol is line-based, keep the value single-line
|
||||
return json.dumps(data, separators=(",", ":"))
|
||||
|
||||
|
||||
def _is_valid_token_timestamp(timestamp, now):
|
||||
# Token if invalid after 90 days
|
||||
# or if the timestamp is in the future.
|
||||
@@ -101,6 +115,7 @@ class MetadataDictProxy(DictProxy):
|
||||
self.iroh_relay = iroh_relay
|
||||
self.turn_hostname = turn_hostname
|
||||
self.turn_socket_path = turn_socket_path
|
||||
self.appversions_path = files(__package__).joinpath("defaults/appversions.json")
|
||||
|
||||
def handle_lookup(self, parts):
|
||||
# Lpriv/43f5f508a7ea0366dff30200c15250e3/devicetoken\tlkj123poi@c2.testrun.org
|
||||
@@ -125,6 +140,9 @@ class MetadataDictProxy(DictProxy):
|
||||
case "maxsmtprecipients":
|
||||
# postfix default (see "postconf smtpd_recipient_limit")
|
||||
return "O1000\n"
|
||||
case "appversions":
|
||||
value = read_appversions(self.appversions_path)
|
||||
return f"O{value}\n" if value else "N\n"
|
||||
|
||||
logging.warning(f"lookup ignored: {parts!r}")
|
||||
return "N\n"
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from chatmaild.metadata import MetadataDictProxy
|
||||
|
||||
ALLOWED_URL_PREFIXES = (
|
||||
"https://github.com/deltachat/",
|
||||
"https://download.delta.chat/",
|
||||
)
|
||||
|
||||
|
||||
def check_string(value):
|
||||
assert isinstance(value, str), value
|
||||
assert value
|
||||
|
||||
|
||||
def check_version_integer(value):
|
||||
# core parses this as u32, see https://github.com/chatmail/core/pull/8557
|
||||
assert isinstance(value, int) and not isinstance(value, bool), value
|
||||
assert 0 <= value < 2**32, value
|
||||
|
||||
|
||||
def check_appversions(data):
|
||||
"""Verifies the file the way core parses it.
|
||||
|
||||
core deserializes into typed structs and drops the whole payload
|
||||
of a relay if a single value has an unexpected type,
|
||||
while missing or misspelled keys silently turn into defaults.
|
||||
"""
|
||||
assert set(data) == {"clients"}, data
|
||||
assert isinstance(data["clients"], list)
|
||||
assert data["clients"]
|
||||
client_ids = []
|
||||
for client in data["clients"]:
|
||||
assert set(client) == {"clientId", "sources"}, client
|
||||
check_string(client["clientId"])
|
||||
client_ids.append(client["clientId"])
|
||||
assert isinstance(client["sources"], list)
|
||||
assert client["sources"]
|
||||
source_ids = []
|
||||
for source in client["sources"]:
|
||||
assert set(source) == {
|
||||
"sourceId",
|
||||
"versionInteger",
|
||||
"versionString",
|
||||
"downloadUrl",
|
||||
}, source
|
||||
check_string(source["sourceId"])
|
||||
source_ids.append(source["sourceId"])
|
||||
check_version_integer(source["versionInteger"])
|
||||
check_string(source["versionString"])
|
||||
check_string(source["downloadUrl"])
|
||||
assert source["downloadUrl"].startswith(ALLOWED_URL_PREFIXES)
|
||||
# core takes the first matching source, later duplicates never surface
|
||||
assert len(set(source_ids)) == len(source_ids), source_ids
|
||||
assert len(set(client_ids)) == len(client_ids), client_ids
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def appversions():
|
||||
# check the file which chatmail-metadata actually serves
|
||||
path = MetadataDictProxy(notifier=None, metadata=None).appversions_path
|
||||
return json.loads(path.read_text())
|
||||
|
||||
|
||||
def test_appversions_schema(appversions):
|
||||
check_appversions(appversions)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", [True, -1, 2**32, "754", 754.0, None])
|
||||
def test_version_integer_rejected(appversions, value):
|
||||
appversions["clients"][0]["sources"][0]["versionInteger"] = value
|
||||
with pytest.raises(AssertionError):
|
||||
check_appversions(appversions)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("key", ["clientId", "sources"])
|
||||
def test_misspelled_client_key_rejected(appversions, key):
|
||||
client = appversions["clients"][0]
|
||||
client[key + "s"] = client.pop(key)
|
||||
with pytest.raises(AssertionError):
|
||||
check_appversions(appversions)
|
||||
|
||||
|
||||
def test_duplicate_source_id_rejected(appversions):
|
||||
sources = appversions["clients"][0]["sources"]
|
||||
sources.append(dict(sources[0]))
|
||||
with pytest.raises(AssertionError):
|
||||
check_appversions(appversions)
|
||||
|
||||
|
||||
def test_foreign_download_url_rejected(appversions):
|
||||
appversions["clients"][0]["sources"][0]["downloadUrl"] = "https://example.org/x.apk"
|
||||
with pytest.raises(AssertionError):
|
||||
check_appversions(appversions)
|
||||
@@ -1,4 +1,5 @@
|
||||
import io
|
||||
import json
|
||||
import time
|
||||
|
||||
import pytest
|
||||
@@ -7,6 +8,7 @@ import requests
|
||||
from chatmaild.metadata import (
|
||||
Metadata,
|
||||
MetadataDictProxy,
|
||||
read_appversions,
|
||||
)
|
||||
from chatmaild.notifier import (
|
||||
Notifier,
|
||||
@@ -369,6 +371,32 @@ def test_iroh_relay(dictproxy):
|
||||
assert wfile.getvalue() == b"Ohttps://example.org/\n"
|
||||
|
||||
|
||||
def test_read_appversions(tmp_path):
|
||||
path = tmp_path.joinpath("appversions.json")
|
||||
assert read_appversions(path) is None
|
||||
|
||||
path.write_text('{\n "clients": []\n}')
|
||||
assert read_appversions(path) == '{"clients":[]}'
|
||||
|
||||
# the value travels as a single dict protocol line
|
||||
path.write_text('{"clients": [{"clientId": "one\\ntwo"}]}')
|
||||
assert read_appversions(path) == '{"clients":[{"clientId":"one\\ntwo"}]}'
|
||||
|
||||
path.write_text("bad json")
|
||||
assert read_appversions(path) is None
|
||||
|
||||
|
||||
def test_appversions_lookup(dictproxy):
|
||||
# the version information shipped with chatmaild is served as a single line
|
||||
key = b"Lshared/0123/vendor/vendor.dovecot/pvt/server/vendor/deltachat/appversions"
|
||||
key += b"\tuser@example.org"
|
||||
rfile, wfile = io.BytesIO(b"H\n" + key), io.BytesIO()
|
||||
dictproxy.loop_forever(rfile, wfile)
|
||||
value = wfile.getvalue()
|
||||
assert value.startswith(b"O") and value.endswith(b"\n")
|
||||
assert json.loads(value[1:])["clients"]
|
||||
|
||||
|
||||
def test_legacy_token_migration(metadata, testaddr):
|
||||
with metadata.get_metadata_dict(testaddr).modify() as data:
|
||||
data[metadata.DEVICETOKEN_KEY] = ["oldtoken1", "oldtoken2"]
|
||||
|
||||
@@ -520,10 +520,10 @@ def deploy_chatmail(config_path: Path, disable_mail: bool, website_only: bool) -
|
||||
("nginx", 443),
|
||||
(["master", "smtpd"], 465),
|
||||
(["master", "smtpd"], 587),
|
||||
(["imap-login", "dovecot"], 993),
|
||||
(["dovecot", "imap-login"], 993),
|
||||
("iroh-relay", 3340),
|
||||
("mtail", 3903),
|
||||
("stats", 3904),
|
||||
(["dovecot", "stats"], 3904),
|
||||
("nginx", 8443),
|
||||
(["master", "smtpd"], config.postfix_reinject_port),
|
||||
(["master", "smtpd"], config.postfix_reinject_port_incoming),
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import ipaddress
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
|
||||
import imap_tools
|
||||
import pytest
|
||||
import requests
|
||||
from chatmaild.tests.test_appversions import check_appversions
|
||||
|
||||
from cmdeploy.cmdeploy import get_sshexec
|
||||
from cmdeploy.remote import rshell
|
||||
@@ -51,6 +53,17 @@ class TestMetadataTokens:
|
||||
assert res == b"1111 2222"
|
||||
assert b"Getmetadata completed" in client.readline()
|
||||
|
||||
def test_get_appversions(self, imap_mailbox):
|
||||
"get app version information shipped with the relay"
|
||||
client = imap_mailbox.client
|
||||
client.send(b'a01 GETMETADATA "" /shared/vendor/deltachat/appversions\n')
|
||||
res = client.readline()
|
||||
assert res[:1] == b"*"
|
||||
res = client.readline().strip().rstrip(b")")
|
||||
# the served value is a single line and passes the shipped file's schema
|
||||
check_appversions(json.loads(res))
|
||||
assert b"Getmetadata completed" in client.readline()
|
||||
|
||||
|
||||
class TestEndToEndDeltaChat:
|
||||
"Tests that use Delta Chat accounts on the chat mail instance."
|
||||
|
||||
@@ -22,6 +22,12 @@ extensions = [
|
||||
templates_path = ['_templates']
|
||||
exclude_patterns = []
|
||||
|
||||
linkcheck_ignore = [
|
||||
# only resolves once the file is merged to main
|
||||
r"https://github\.com/chatmail/relay/blob/main/chatmaild/src/chatmaild/defaults/appversions\.json",
|
||||
]
|
||||
|
||||
|
||||
|
||||
|
||||
# -- Options for HTML output -------------------------------------------------
|
||||
|
||||
@@ -249,6 +249,33 @@ Fresh chatmail addresses have a mailbox directory that contains:
|
||||
directories will typically be empty unless the user of that address
|
||||
hasn’t been online for a while.
|
||||
|
||||
App version information (experimental)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
A chatmail relay ships the
|
||||
`appversions.json <https://github.com/chatmail/relay/blob/main/chatmaild/src/chatmaild/defaults/appversions.json>`_
|
||||
file of the ``chatmaild`` package
|
||||
and serves its content under the IMAP METADATA key
|
||||
``/shared/vendor/deltachat/appversions``.
|
||||
Chat apps installed outside of app stores read this key
|
||||
to learn about updates and where to download them.
|
||||
The mechanism is experimental and may change.
|
||||
|
||||
The file travels with the normal deploy:
|
||||
update the repository checkout and run ``cmdeploy run``.
|
||||
Local modifications of ``appversions.json`` are deployed as-is,
|
||||
so you can serve your own app version information,
|
||||
including links to app downloads.
|
||||
There is no automatic refresh:
|
||||
version information changes only when you deploy again.
|
||||
|
||||
.. note::
|
||||
|
||||
Note that as of August 2026, only Delta Chat Android Google Play version
|
||||
is beginning to support discovering app versions from relays.
|
||||
Generally, consumers of relay-provided app version information
|
||||
need to verify themselves that downloaded app files are valid.
|
||||
|
||||
Active ports
|
||||
~~~~~~~~~~~~
|
||||
|
||||
|
||||
Reference in New Issue
Block a user