mirror of
https://github.com/chatmail/relay.git
synced 2026-09-03 22:13:14 +00:00
Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6cd943afb5 | ||
|
|
d25e8a8ee8 | ||
|
|
cae03e2714 | ||
|
|
7db16cc716 | ||
|
|
4cdccee63b | ||
|
|
12664d9188 | ||
|
|
1f0ddb7e5b | ||
|
|
2af8d0e7b5 | ||
|
|
e489a1ea29 | ||
|
|
455da45d36 | ||
|
|
48ad92bf24 | ||
|
|
33f9cddb1b |
@@ -13,6 +13,11 @@ jobs:
|
||||
scripts:
|
||||
name: build
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
# Pin the repository links in the docs to this pull request's head commit
|
||||
# so that linkcheck resolves files which only exist on the branch so far.
|
||||
# see doc/conf.py
|
||||
DOC_GITHUB_REF: ${{ github.event.pull_request.head.sha }}
|
||||
environment:
|
||||
name: 'staging.chatmail.at/doc/relay/'
|
||||
url: https://staging.chatmail.at/doc/relay/${{ steps.prepare.outputs.prid }}
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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": 757,
|
||||
"versionString": "2.59.1",
|
||||
"downloadUrl": "https://github.com/deltachat/deltachat-android/releases/download/v2.59.1/deltachat-gplay-release-2.59.1.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"
|
||||
|
||||
@@ -41,22 +41,22 @@ def ipv4_config(make_config):
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def maildomain(example_config):
|
||||
def example_maildomain(example_config):
|
||||
return example_config.mail_domain
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def testaddr(maildomain):
|
||||
return f"user.name@{maildomain}"
|
||||
def testaddr(example_maildomain):
|
||||
return f"user.name@{example_maildomain}"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def gencreds(maildomain):
|
||||
def example_gencreds(example_maildomain):
|
||||
count = itertools.count()
|
||||
next(count)
|
||||
|
||||
def gen(domain=None):
|
||||
domain = domain if domain else maildomain
|
||||
domain = domain if domain else example_maildomain
|
||||
while 1:
|
||||
num = next(count)
|
||||
alphanumeric = "abcdefghijklmnopqrstuvwxyz1234567890"
|
||||
|
||||
@@ -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)
|
||||
@@ -30,9 +30,9 @@ def test_read_config_ipv4(ipv4_config):
|
||||
assert ipv4_config.mail_domain == "[1.3.3.7]"
|
||||
|
||||
|
||||
def test_read_config_basic_using_defaults(tmp_path, maildomain):
|
||||
def test_read_config_basic_using_defaults(tmp_path, example_maildomain):
|
||||
inipath = tmp_path.joinpath("chatmail.ini")
|
||||
inipath.write_text(f"[params]\nmail_domain = {maildomain}")
|
||||
inipath.write_text(f"[params]\nmail_domain = {example_maildomain}")
|
||||
example_config = read_config(inipath)
|
||||
assert example_config.max_user_send_per_minute == 60
|
||||
assert example_config.filtermail_smtp_port_incoming == 10081
|
||||
|
||||
@@ -19,8 +19,8 @@ def dictproxy(example_config):
|
||||
return AuthDictProxy(config=example_config)
|
||||
|
||||
|
||||
def test_basic(dictproxy, gencreds):
|
||||
addr, password = gencreds()
|
||||
def test_basic(dictproxy, example_gencreds):
|
||||
addr, password = example_gencreds()
|
||||
dictproxy.lookup_passdb(addr, password)
|
||||
data = dictproxy.lookup_userdb(addr)
|
||||
assert data
|
||||
@@ -107,7 +107,7 @@ def test_handle_dovecot_protocol_user_not_exists(example_config):
|
||||
assert wfile.getvalue() == b"N\n"
|
||||
|
||||
|
||||
def test_handle_dovecot_protocol_iterate(gencreds, example_config):
|
||||
def test_handle_dovecot_protocol_iterate(example_config):
|
||||
dictproxy = AuthDictProxy(config=example_config)
|
||||
dictproxy.lookup_passdb("asdf00000@chat.example.org", "q9mr3faue")
|
||||
dictproxy.lookup_passdb("asdf11111@chat.example.org", "q9mr3faue")
|
||||
@@ -174,14 +174,14 @@ def test_concurrent_creation_same_account(dictproxy):
|
||||
assert len(passwords_seen) == 1
|
||||
|
||||
|
||||
def test_50_concurrent_lookups_different_accounts(gencreds, dictproxy):
|
||||
def test_50_concurrent_lookups_different_accounts(example_gencreds, dictproxy):
|
||||
num_threads = 50
|
||||
req_per_thread = 5
|
||||
results = queue.Queue()
|
||||
|
||||
def lookup():
|
||||
for i in range(req_per_thread):
|
||||
addr, password = gencreds()
|
||||
addr, password = example_gencreds()
|
||||
try:
|
||||
dictproxy.lookup_passdb(addr, password)
|
||||
except Exception:
|
||||
@@ -205,14 +205,14 @@ def test_50_concurrent_lookups_different_accounts(gencreds, dictproxy):
|
||||
|
||||
|
||||
def test_insufficient_resources_block_creation_not_existing_logins(
|
||||
dictproxy, gencreds, monkeypatch
|
||||
dictproxy, example_gencreds, monkeypatch
|
||||
):
|
||||
addr, password = gencreds()
|
||||
addr, password = example_gencreds()
|
||||
assert dictproxy.lookup_passdb(addr, password)
|
||||
|
||||
monkeypatch.setattr(
|
||||
chatmaild.doveauth, "has_sufficient_resources", lambda config: False
|
||||
)
|
||||
newaddr, newpassword = gencreds()
|
||||
newaddr, newpassword = example_gencreds()
|
||||
assert not dictproxy.lookup_passdb(newaddr, newpassword)
|
||||
assert dictproxy.lookup_passdb(addr, password)
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -48,7 +48,7 @@ def test_create_dclogin_url_ipv4(ipv4_config):
|
||||
assert addr in url
|
||||
|
||||
|
||||
def test_print_new_account(capsys, monkeypatch, maildomain, tmpdir, example_config):
|
||||
def test_print_new_account(capsys, monkeypatch, tmpdir, example_config):
|
||||
monkeypatch.setattr(chatmaild.newemail, "CONFIG_PATH", str(example_config._inipath))
|
||||
print_new_account()
|
||||
out, err = capsys.readouterr()
|
||||
|
||||
@@ -19,6 +19,7 @@ dependencies = [
|
||||
"pytest-xdist",
|
||||
"execnet",
|
||||
"imap_tools",
|
||||
"lupa",
|
||||
"deltachat-rpc-client",
|
||||
"deltachat-rpc-server",
|
||||
]
|
||||
|
||||
@@ -4,6 +4,8 @@ from ..basedeploy import Deployer
|
||||
|
||||
|
||||
class AcmetoolDeployer(Deployer):
|
||||
bin_path = "/usr/bin/acmetool"
|
||||
|
||||
def __init__(self, email, domains):
|
||||
self.domains = domains
|
||||
self.email = email
|
||||
@@ -41,8 +43,12 @@ class AcmetoolDeployer(Deployer):
|
||||
domains=self.domains,
|
||||
)
|
||||
|
||||
self.ensure_systemd_unit("acmetool/acmetool-redirector.service")
|
||||
self.ensure_systemd_unit("acmetool/acmetool-reconcile.service")
|
||||
self.ensure_systemd_unit(
|
||||
"acmetool/acmetool-redirector.service.j2", bin_path=self.bin_path
|
||||
)
|
||||
self.ensure_systemd_unit(
|
||||
"acmetool/acmetool-reconcile.service.j2", bin_path=self.bin_path
|
||||
)
|
||||
self.ensure_systemd_unit("acmetool/acmetool-reconcile.timer")
|
||||
|
||||
def activate(self):
|
||||
@@ -52,5 +58,5 @@ class AcmetoolDeployer(Deployer):
|
||||
|
||||
server.shell(
|
||||
name=f"Reconcile certificates for: {', '.join(self.domains)}",
|
||||
commands=["acmetool --batch --xlog.severity=debug reconcile"],
|
||||
commands=[f"{self.bin_path} --batch --xlog.severity=debug reconcile"],
|
||||
)
|
||||
|
||||
+1
-1
@@ -4,5 +4,5 @@ After=network.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/bin/acmetool --batch reconcile
|
||||
ExecStart={{ bin_path }} --batch reconcile
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ Description=acmetool HTTP redirector
|
||||
|
||||
[Service]
|
||||
Type=notify
|
||||
ExecStart=/usr/bin/acmetool redirector --service.uid=daemon --bind=127.0.0.1:402
|
||||
ExecStart={{ bin_path }} redirector --service.uid=daemon --bind=127.0.0.1:402
|
||||
Restart=always
|
||||
RestartSec=30
|
||||
|
||||
@@ -51,7 +51,7 @@ def get_resource(arg, pkg=__package__):
|
||||
return importlib.resources.files(pkg).joinpath(arg)
|
||||
|
||||
|
||||
def configure_remote_units(deployer, mail_domain, units) -> None:
|
||||
def configure_remote_units(deployer, mail_domain, units, **kwargs) -> None:
|
||||
remote_base_dir = "/usr/local/lib/chatmaild"
|
||||
remote_venv_dir = f"{remote_base_dir}/venv"
|
||||
remote_chatmail_inipath = f"{remote_base_dir}/chatmail.ini"
|
||||
@@ -63,6 +63,7 @@ def configure_remote_units(deployer, mail_domain, units) -> None:
|
||||
config_path=remote_chatmail_inipath,
|
||||
remote_venv_dir=remote_venv_dir,
|
||||
mail_domain=mail_domain,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
basename = fn if "." in fn else f"{fn}.service"
|
||||
@@ -226,7 +227,7 @@ class Deployer:
|
||||
res = files.directory(name=name, path=path, present=False, **kwargs)
|
||||
return self._update_restart_signals(path, res)
|
||||
|
||||
def download_executable(self, url, dest, sha256sum, extract=None):
|
||||
def download_executable(self, url, dest, sha256sum, extract=None, mode="755"):
|
||||
existing = host.get_fact(Sha256File, dest)
|
||||
if existing == sha256sum:
|
||||
return
|
||||
@@ -243,7 +244,7 @@ class Deployer:
|
||||
f"({dl_cmd}"
|
||||
f" && echo '{sha256sum} {tmp}' | sha256sum -c"
|
||||
f" && mv {tmp} {dest})",
|
||||
f"chmod 755 {dest}",
|
||||
f"chmod {mode} {dest}",
|
||||
],
|
||||
)
|
||||
self.need_restart = True
|
||||
|
||||
@@ -33,6 +33,7 @@ from .filtermail.deployer import FiltermailDeployer
|
||||
from .mtail.deployer import MtailDeployer
|
||||
from .nginx.deployer import NginxDeployer
|
||||
from .opendkim.deployer import OpendkimDeployer
|
||||
from .pins import IROH_ARTIFACTS, TURN_ARTIFACTS
|
||||
from .postfix.deployer import PostfixDeployer
|
||||
from .selfsigned.deployer import SelfSignedTlsDeployer
|
||||
from .www import build_webpages, find_merge_conflict, get_paths
|
||||
@@ -301,55 +302,48 @@ def check_config(config):
|
||||
|
||||
|
||||
class TurnDeployer(Deployer):
|
||||
bin_path = "/usr/local/bin/chatmail-turn"
|
||||
|
||||
def __init__(self, mail_domain):
|
||||
self.mail_domain = mail_domain
|
||||
self.units = ["turnserver"]
|
||||
|
||||
def install(self):
|
||||
(url, sha256sum) = {
|
||||
"x86_64": (
|
||||
"https://github.com/chatmail/chatmail-turn/releases/download/v0.4/chatmail-turn-x86_64-linux",
|
||||
"1ec1f5c50122165e858a5a91bcba9037a28aa8cb8b64b8db570aa457c6141a8a",
|
||||
),
|
||||
"aarch64": (
|
||||
"https://github.com/chatmail/chatmail-turn/releases/download/v0.4/chatmail-turn-aarch64-linux",
|
||||
"0fb3e792419494e21ecad536464929dba706bb2c88884ed8f1788141d26fc756",
|
||||
),
|
||||
}[host.get_fact(facts.server.Arch)]
|
||||
self.download_executable(url, "/usr/local/bin/chatmail-turn", sha256sum)
|
||||
(url, sha256sum) = TURN_ARTIFACTS[host.get_fact(facts.server.Arch)]
|
||||
self.download_executable(url, self.bin_path, sha256sum)
|
||||
|
||||
def configure(self):
|
||||
configure_remote_units(self, self.mail_domain, self.units)
|
||||
configure_remote_units(
|
||||
self, self.mail_domain, self.units, bin_path=self.bin_path
|
||||
)
|
||||
|
||||
def activate(self):
|
||||
activate_remote_units(self, self.units)
|
||||
|
||||
|
||||
class IrohDeployer(Deployer):
|
||||
bin_path = "/usr/local/bin/iroh-relay"
|
||||
config_path = "/etc/iroh-relay.toml"
|
||||
|
||||
def __init__(self, enable_iroh_relay):
|
||||
self.enable_iroh_relay = enable_iroh_relay
|
||||
|
||||
def install(self):
|
||||
(url, sha256sum) = {
|
||||
"x86_64": (
|
||||
"https://github.com/n0-computer/iroh/releases/download/v0.35.0/iroh-relay-v0.35.0-x86_64-unknown-linux-musl.tar.gz",
|
||||
"45c81199dbd70f8c4c30fef7f3b9727ca6e3cea8f2831333eeaf8aa71bf0fac1",
|
||||
),
|
||||
"aarch64": (
|
||||
"https://github.com/n0-computer/iroh/releases/download/v0.35.0/iroh-relay-v0.35.0-aarch64-unknown-linux-musl.tar.gz",
|
||||
"f8ef27631fac213b3ef668d02acd5b3e215292746a3fc71d90c63115446008b1",
|
||||
),
|
||||
}[host.get_fact(facts.server.Arch)]
|
||||
(url, sha256sum) = IROH_ARTIFACTS[host.get_fact(facts.server.Arch)]
|
||||
self.download_executable(
|
||||
url,
|
||||
"/usr/local/bin/iroh-relay",
|
||||
self.bin_path,
|
||||
sha256sum,
|
||||
extract="gunzip | tar -xf - ./iroh-relay -O",
|
||||
)
|
||||
|
||||
def configure(self):
|
||||
self.ensure_systemd_unit("iroh-relay.service")
|
||||
self.put_file("iroh-relay.toml", "/etc/iroh-relay.toml")
|
||||
self.ensure_systemd_unit(
|
||||
"iroh-relay.service.j2",
|
||||
bin_path=self.bin_path,
|
||||
config_path=self.config_path,
|
||||
)
|
||||
self.put_file("iroh-relay.toml", self.config_path)
|
||||
|
||||
def activate(self):
|
||||
self.ensure_service(
|
||||
|
||||
@@ -14,18 +14,9 @@ from cmdeploy.basedeploy import (
|
||||
configure_remote_units,
|
||||
is_in_container,
|
||||
)
|
||||
from cmdeploy.pins import DOVECOT_SHA256, DOVECOT_VERSION
|
||||
|
||||
DOVECOT_ARCHIVE_VERSION = "2.3.21+dfsg1-3"
|
||||
DOVECOT_PACKAGE_VERSION = f"1:{DOVECOT_ARCHIVE_VERSION}"
|
||||
|
||||
DOVECOT_SHA256 = {
|
||||
("core", "amd64"): "dd060706f52a306fa863d874717210b9fe10536c824afe1790eec247ded5b27d",
|
||||
("core", "arm64"): "e7548e8a82929722e973629ecc40fcfa886894cef3db88f23535149e7f730dc9",
|
||||
("imapd", "amd64"): "8d8dc6fc00bbb6cdb25d345844f41ce2f1c53f764b79a838eb2a03103eebfa86",
|
||||
("imapd", "arm64"): "178fa877ddd5df9930e8308b518f4b07df10e759050725f8217a0c1fb3fd707f",
|
||||
("lmtpd", "amd64"): "2f69ba5e35363de50962d42cccbfe4ed8495265044e244007d7ccddad77513ab",
|
||||
("lmtpd", "arm64"): "89f52fb36524f5877a177dff4a713ba771fd3f91f22ed0af7238d495e143b38f",
|
||||
}
|
||||
DOVECOT_PACKAGE_VERSION = f"1:{DOVECOT_VERSION}"
|
||||
|
||||
|
||||
class DovecotDeployer(Deployer):
|
||||
@@ -117,7 +108,7 @@ def _download_dovecot_package(package: str, arch: str) -> tuple[str | None, bool
|
||||
if DOVECOT_PACKAGE_VERSION in installed_versions:
|
||||
return None, False
|
||||
|
||||
url_version = DOVECOT_ARCHIVE_VERSION.replace("+", "%2B")
|
||||
url_version = DOVECOT_VERSION.replace("+", "%2B")
|
||||
deb_base = f"{pkg_name}_{url_version}_{arch}.deb"
|
||||
primary_url = f"https://download.delta.chat/dovecot/{deb_base}"
|
||||
fallback_url = f"https://github.com/chatmail/dovecot/releases/download/upstream%2F{url_version}/{deb_base}"
|
||||
|
||||
@@ -3,6 +3,7 @@ import os
|
||||
from pyinfra import facts, host
|
||||
|
||||
from cmdeploy.basedeploy import Deployer
|
||||
from cmdeploy.pins import FILTERMAIL_ARTIFACTS
|
||||
|
||||
|
||||
class FiltermailDeployer(Deployer):
|
||||
@@ -20,11 +21,7 @@ class FiltermailDeployer(Deployer):
|
||||
return
|
||||
|
||||
arch = host.get_fact(facts.server.Arch)
|
||||
url = f"https://github.com/chatmail/filtermail/releases/download/v0.7.4/filtermail-{arch}"
|
||||
sha256sum = {
|
||||
"x86_64": "484cb8dff083134aefba9fce4a6b7ef4784a0f0e28e5108ecf8bb9e58a44fd2c",
|
||||
"aarch64": "66aa0ca2ca9add7a12d92883d76f8786384092adfde24a3d3a1d0b1f30d23a9e",
|
||||
}[arch]
|
||||
url, sha256sum = FILTERMAIL_ARTIFACTS[arch]
|
||||
self.download_executable(url, self.bin_path, sha256sum)
|
||||
|
||||
def configure(self):
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
Description=Iroh relay
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/local/bin/iroh-relay --config-path /etc/iroh-relay.toml
|
||||
ExecStart={{ bin_path }} --config-path {{ config_path }}
|
||||
Restart=on-failure
|
||||
RestartSec=5s
|
||||
User=iroh
|
||||
@@ -1,10 +1,14 @@
|
||||
from pyinfra import facts, host
|
||||
from pyinfra.operations import apt
|
||||
from pyinfra.operations import apt, server
|
||||
|
||||
from cmdeploy.basedeploy import Deployer
|
||||
from cmdeploy.pins import FILTERMAIL_ARTIFACTS, MTAIL_ARTIFACTS
|
||||
|
||||
|
||||
class MtailDeployer(Deployer):
|
||||
bin_path = "/usr/local/bin/mtail"
|
||||
progs_dir = "/etc/mtail"
|
||||
|
||||
def __init__(self, mtail_address):
|
||||
self.mtail_address = mtail_address
|
||||
|
||||
@@ -12,19 +16,10 @@ class MtailDeployer(Deployer):
|
||||
# Uninstall mtail package to install a static binary.
|
||||
apt.packages(name="Uninstall mtail", packages=["mtail"], present=False)
|
||||
|
||||
(url, sha256sum) = {
|
||||
"x86_64": (
|
||||
"https://github.com/google/mtail/releases/download/v3.0.8/mtail_3.0.8_linux_amd64.tar.gz",
|
||||
"d55cb601049c5e61eabab29998dbbcea95d480e5448544f9470337ba2eea882e",
|
||||
),
|
||||
"aarch64": (
|
||||
"https://github.com/google/mtail/releases/download/v3.0.8/mtail_3.0.8_linux_arm64.tar.gz",
|
||||
"f748db8ad2a1e0b63684d4c8868cf6a373a20f7e6922e5ece601fff0ee00eb1a",
|
||||
),
|
||||
}[host.get_fact(facts.server.Arch)]
|
||||
(url, sha256sum) = MTAIL_ARTIFACTS[host.get_fact(facts.server.Arch)]
|
||||
self.download_executable(
|
||||
url,
|
||||
"/usr/local/bin/mtail",
|
||||
self.bin_path,
|
||||
sha256sum,
|
||||
extract="gunzip | tar -xf - mtail -O",
|
||||
)
|
||||
@@ -36,8 +31,31 @@ class MtailDeployer(Deployer):
|
||||
"mtail/mtail.service.j2",
|
||||
address=self.mtail_address or "127.0.0.1",
|
||||
port=3903,
|
||||
bin_path=self.bin_path,
|
||||
progs_dir=self.progs_dir,
|
||||
)
|
||||
self.put_file("mtail/delivered_mail.mtail", "/etc/mtail/delivered_mail.mtail")
|
||||
if self.mtail_address:
|
||||
self.put_file(
|
||||
"mtail/delivered_mail.mtail", f"{self.progs_dir}/delivered_mail.mtail"
|
||||
)
|
||||
url, sha256sum = FILTERMAIL_ARTIFACTS['mtail']
|
||||
self.download_executable(
|
||||
url,
|
||||
f"{self.progs_dir}/filtermail.mtail",
|
||||
sha256sum,
|
||||
mode="644",
|
||||
)
|
||||
if self.need_restart:
|
||||
# Check if all installed mtail rules compile or fail early
|
||||
# --one_shot to exit, --port 0 to not clash with running mtail.
|
||||
server.shell(
|
||||
name="Validate mtail programs",
|
||||
commands=[
|
||||
f"timeout 30 {self.bin_path} --compile_only --one_shot"
|
||||
f" --progs {self.progs_dir} --logs /dev/null"
|
||||
" --address 127.0.0.1 --port 0"
|
||||
],
|
||||
)
|
||||
|
||||
def activate(self):
|
||||
active = bool(self.mtail_address)
|
||||
|
||||
@@ -5,7 +5,7 @@ Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/bin/sh -c "journalctl -f -o short-iso -n 0 | /usr/local/bin/mtail --address={{ address }} --port={{ port }} --progs /etc/mtail --logtostderr --logs -"
|
||||
ExecStart=/bin/sh -c "journalctl -f -o short-iso -n 0 | {{ bin_path }} --address={{ address }} --port={{ port }} --progs {{ progs_dir }} --logtostderr --logs -"
|
||||
Restart=on-failure
|
||||
RestartSec=2s
|
||||
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
"""Versions, hashes, and download URLs for pre-built artifacts fetched during deploy."""
|
||||
|
||||
FILTERMAIL_VERSION = "v0.7.4"
|
||||
FILTERMAIL_ARTIFACTS = {
|
||||
"x86_64": (
|
||||
f"https://github.com/chatmail/filtermail/releases/download/{FILTERMAIL_VERSION}/filtermail-x86_64",
|
||||
"484cb8dff083134aefba9fce4a6b7ef4784a0f0e28e5108ecf8bb9e58a44fd2c",
|
||||
),
|
||||
"aarch64": (
|
||||
f"https://github.com/chatmail/filtermail/releases/download/{FILTERMAIL_VERSION}/filtermail-aarch64",
|
||||
"66aa0ca2ca9add7a12d92883d76f8786384092adfde24a3d3a1d0b1f30d23a9e",
|
||||
),
|
||||
"mtail": (
|
||||
f"https://raw.githubusercontent.com/chatmail/filtermail/{FILTERMAIL_VERSION}/contrib/filtermail.mtail",
|
||||
"948f688bb89ad47e6eb0fc8fa107e201a689f5adc264ff926be487a2a8562b51",
|
||||
),
|
||||
}
|
||||
|
||||
MTAIL_VERSION = "3.4.9"
|
||||
MTAIL_ARTIFACTS = {
|
||||
"x86_64": (
|
||||
f"https://github.com/jaqx0r/mtail/releases/download/v{MTAIL_VERSION}/mtail_{MTAIL_VERSION}_linux_amd64.tar.gz",
|
||||
"55f64a87f71955bb871c724b4aadf19fe9d854e6327196919c7fe44943427eab",
|
||||
),
|
||||
"aarch64": (
|
||||
f"https://github.com/jaqx0r/mtail/releases/download/v{MTAIL_VERSION}/mtail_{MTAIL_VERSION}_linux_arm64.tar.gz",
|
||||
"e0a2b66b372ca257d7daeb7ba10f9233a2192a1f9057618fccc6be5c854a2a3c",
|
||||
),
|
||||
}
|
||||
|
||||
DOVECOT_VERSION = "2.3.21+dfsg1-3"
|
||||
DOVECOT_SHA256 = {
|
||||
("core", "amd64"): "dd060706f52a306fa863d874717210b9fe10536c824afe1790eec247ded5b27d",
|
||||
("core", "arm64"): "e7548e8a82929722e973629ecc40fcfa886894cef3db88f23535149e7f730dc9",
|
||||
("imapd", "amd64"): "8d8dc6fc00bbb6cdb25d345844f41ce2f1c53f764b79a838eb2a03103eebfa86",
|
||||
("imapd", "arm64"): "178fa877ddd5df9930e8308b518f4b07df10e759050725f8217a0c1fb3fd707f",
|
||||
("lmtpd", "amd64"): "2f69ba5e35363de50962d42cccbfe4ed8495265044e244007d7ccddad77513ab",
|
||||
("lmtpd", "arm64"): "89f52fb36524f5877a177dff4a713ba771fd3f91f22ed0af7238d495e143b38f",
|
||||
}
|
||||
TURN_VERSION = "v0.4"
|
||||
TURN_ARTIFACTS = {
|
||||
"x86_64": (
|
||||
f"https://github.com/chatmail/chatmail-turn/releases/download/{TURN_VERSION}/chatmail-turn-x86_64-linux",
|
||||
"1ec1f5c50122165e858a5a91bcba9037a28aa8cb8b64b8db570aa457c6141a8a",
|
||||
),
|
||||
"aarch64": (
|
||||
f"https://github.com/chatmail/chatmail-turn/releases/download/{TURN_VERSION}/chatmail-turn-aarch64-linux",
|
||||
"0fb3e792419494e21ecad536464929dba706bb2c88884ed8f1788141d26fc756",
|
||||
),
|
||||
}
|
||||
|
||||
IROH_VERSION = "v0.35.0"
|
||||
IROH_ARTIFACTS = {
|
||||
"x86_64": (
|
||||
f"https://github.com/n0-computer/iroh/releases/download/{IROH_VERSION}/iroh-relay-{IROH_VERSION}-x86_64-unknown-linux-musl.tar.gz",
|
||||
"45c81199dbd70f8c4c30fef7f3b9727ca6e3cea8f2831333eeaf8aa71bf0fac1",
|
||||
),
|
||||
"aarch64": (
|
||||
f"https://github.com/n0-computer/iroh/releases/download/{IROH_VERSION}/iroh-relay-{IROH_VERSION}-aarch64-unknown-linux-musl.tar.gz",
|
||||
"f8ef27631fac213b3ef668d02acd5b3e215292746a3fc71d90c63115446008b1",
|
||||
),
|
||||
}
|
||||
@@ -5,5 +5,5 @@ After=network.target
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=vmail
|
||||
ExecStart=/usr/local/lib/chatmaild/venv/bin/chatmail-expire /usr/local/lib/chatmaild/chatmail.ini -v --remove
|
||||
ExecStart={execpath} {config_path} -v --remove
|
||||
|
||||
|
||||
@@ -5,5 +5,5 @@ After=network.target
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=vmail
|
||||
ExecStart=/usr/local/lib/chatmaild/venv/bin/chatmail-fsreport /usr/local/lib/chatmaild/chatmail.ini
|
||||
ExecStart={execpath} {config_path}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ After=network.target
|
||||
[Service]
|
||||
Type=simple
|
||||
Restart=always
|
||||
ExecStart=/usr/local/bin/chatmail-turn --realm {mail_domain} --socket /run/chatmail-turn/turn.socket
|
||||
ExecStart={bin_path} --realm {mail_domain} --socket /run/chatmail-turn/turn.socket
|
||||
|
||||
# Create /run/chatmail-turn
|
||||
RuntimeDirectory=chatmail-turn
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
"""Run the lua scripts we ship under lupa, which bundles Lua 5.4 like dovecot."""
|
||||
|
||||
import pytest
|
||||
from lupa import lua54
|
||||
|
||||
from cmdeploy.basedeploy import get_resource
|
||||
|
||||
|
||||
class Lua:
|
||||
"""A Lua runtime to load shipped scripts and mocks into."""
|
||||
|
||||
def __init__(self):
|
||||
self.rt = lua54.LuaRuntime(unpack_returned_tuples=True)
|
||||
self.g = self.rt.globals()
|
||||
|
||||
def load(self, path):
|
||||
self.rt.execute(get_resource(path).read_text())
|
||||
|
||||
def table(self, **kwargs):
|
||||
return self.rt.table(**kwargs)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def lua():
|
||||
return Lua()
|
||||
@@ -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."
|
||||
|
||||
@@ -211,7 +211,7 @@ class ImapConn:
|
||||
status, res = self.conn.select()
|
||||
if int(res[0]) == 0:
|
||||
raise ValueError("no messages in imap folder")
|
||||
status, results = self.conn.fetch("1:*", "(RFC822)")
|
||||
status, results = self.conn.fetch("1:*", "(BODY.PEEK[])")
|
||||
assert status == "OK"
|
||||
return results
|
||||
|
||||
|
||||
@@ -111,7 +111,7 @@ def test_download_dovecot_package_uses_archive_version_for_url_and_filename(
|
||||
|
||||
deb, changed = dovecot_deployer._download_dovecot_package("core", "amd64")
|
||||
|
||||
archive_version = dovecot_deployer.DOVECOT_ARCHIVE_VERSION.replace("+", "%2B")
|
||||
archive_version = dovecot_deployer.DOVECOT_VERSION.replace("+", "%2B")
|
||||
expected_deb = f"/root/dovecot-core_{archive_version}_amd64.deb"
|
||||
|
||||
# Verify the returned path uses archive version, not package version (with epoch)
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
"""Test the push_notification.lua we ship, against a mocked dovecot mail API."""
|
||||
|
||||
import textwrap
|
||||
|
||||
import pytest
|
||||
|
||||
USER1 = "user12345@chat.example.org"
|
||||
USER2 = "user67890@chat.example.org"
|
||||
|
||||
DOVECOT_MOCKS = textwrap.dedent("""
|
||||
function make_user(username)
|
||||
local function mailbox(_, name)
|
||||
record("mailbox " .. name)
|
||||
return {
|
||||
sync = function() record("sync") end,
|
||||
metadata_set = function(_, k, v)
|
||||
record("metadata_set " .. k .. "=" .. v)
|
||||
end,
|
||||
free = function() record("free") end,
|
||||
}
|
||||
end
|
||||
return {username = username, mailbox = mailbox}
|
||||
end
|
||||
""")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def script(lua):
|
||||
lua.rt.execute(DOVECOT_MOCKS)
|
||||
lua.load("dovecot/push_notification.lua")
|
||||
return lua
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def deliver(script):
|
||||
def deliver(recipient, sender):
|
||||
calls = []
|
||||
script.g.record = calls.append
|
||||
user = script.g.make_user(recipient)
|
||||
ctx = script.g.dovecot_lua_notify_begin_txn(user)
|
||||
event = script.table(mailbox="INBOX", from_address=sender)
|
||||
script.g.dovecot_lua_notify_event_message_new(ctx, event)
|
||||
script.g.dovecot_lua_notify_end_txn(ctx, True)
|
||||
return calls
|
||||
|
||||
return deliver
|
||||
|
||||
|
||||
def test_entry_points_have_the_names_dovecot_calls(script):
|
||||
assert script.g.dovecot_lua_notify_begin_txn is not None
|
||||
assert script.g.dovecot_lua_notify_event_message_new is not None
|
||||
assert script.g.dovecot_lua_notify_end_txn is not None
|
||||
|
||||
|
||||
def test_begin_txn_returns_the_user_as_event_context(script):
|
||||
user = script.g.make_user(USER1)
|
||||
ctx = script.g.dovecot_lua_notify_begin_txn(user)
|
||||
ctx.marker = "seen"
|
||||
assert user.marker == "seen"
|
||||
|
||||
|
||||
def test_incoming_message_notifies_metadata_server(deliver):
|
||||
assert deliver(USER1, sender=USER2) == [
|
||||
"mailbox INBOX",
|
||||
"sync",
|
||||
"metadata_set /private/messagenew=",
|
||||
"free",
|
||||
]
|
||||
|
||||
|
||||
def test_own_message_does_not_wake_the_sending_device(deliver):
|
||||
assert deliver(USER1, sender=USER1) == [
|
||||
"mailbox INBOX",
|
||||
"sync",
|
||||
"free",
|
||||
]
|
||||
|
||||
|
||||
def test_message_without_from_address_is_notified(deliver):
|
||||
assert deliver(USER1, sender=None) == [
|
||||
"mailbox INBOX",
|
||||
"sync",
|
||||
"metadata_set /private/messagenew=",
|
||||
"free",
|
||||
]
|
||||
@@ -3,6 +3,8 @@
|
||||
# For the full list of built-in configuration values, see the documentation:
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html
|
||||
|
||||
import os
|
||||
|
||||
# -- Project information -----------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
|
||||
|
||||
@@ -16,12 +18,24 @@ author = 'chatmail collective'
|
||||
extensions = [
|
||||
#'sphinx.ext.autodoc',
|
||||
#'sphinx.ext.viewdoc',
|
||||
'sphinx.ext.extlinks',
|
||||
'sphinxcontrib.mermaid',
|
||||
]
|
||||
|
||||
templates_path = ['_templates']
|
||||
exclude_patterns = []
|
||||
|
||||
# Repository links go through the roles below.
|
||||
# CI sets DOC_GITHUB_REF to the head commit of a pull request,
|
||||
gh_ref = os.environ.get("DOC_GITHUB_REF", "main")
|
||||
|
||||
extlinks = {
|
||||
"repofile": (f"https://github.com/chatmail/relay/blob/{gh_ref}/%s", "%s"),
|
||||
"repodir": (f"https://github.com/chatmail/relay/tree/{gh_ref}/%s", "%s"),
|
||||
}
|
||||
|
||||
# Warn about repository links spelled out in full instead of using the roles.
|
||||
extlinks_detect_hardcoded_links = True
|
||||
|
||||
|
||||
# -- Options for HTML output -------------------------------------------------
|
||||
|
||||
+1
-1
@@ -54,7 +54,7 @@ How can I upgrade my chatmail relay?
|
||||
------------------------------------
|
||||
|
||||
To upgrade to the latest ``main`` branch,
|
||||
``cd`` into your local checkout of `https://github.com/chatmail/relay/`_
|
||||
``cd`` into your local checkout of https://github.com/chatmail/relay/
|
||||
and run the following commands:
|
||||
|
||||
::
|
||||
|
||||
+39
-14
@@ -6,13 +6,13 @@ Technical overview
|
||||
Directories of the relay repository
|
||||
-----------------------------------
|
||||
|
||||
The `chatmail relay repository <https://github.com/chatmail/relay/tree/main/>`_
|
||||
The `chatmail relay repository <https://github.com/chatmail/relay>`_
|
||||
has four main directories.
|
||||
|
||||
``scripts/``
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
`scripts <https://github.com/chatmail/relay/tree/main/scripts>`_
|
||||
:repodir:`scripts`
|
||||
offers two convenience tools for beginners:
|
||||
|
||||
- ``initenv.sh`` installs a local virtualenv Python environment and
|
||||
@@ -71,7 +71,7 @@ The deployed system components of a chatmail relay are:
|
||||
``chatmaild/``
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
`chatmaild <https://github.com/chatmail/relay/tree/main/chatmaild>`_
|
||||
:repodir:`chatmaild`
|
||||
is a Python package containing several small services which handle
|
||||
authentication, trigger push notifications on new messages, ensure
|
||||
that outbound mails are encrypted, delete inactive users, and some
|
||||
@@ -83,25 +83,24 @@ that integrate with Dovecot and Postfix to achieve instant-onboarding
|
||||
and only relaying OpenPGP end-to-end messages encrypted messages. A
|
||||
short overview of ``chatmaild`` services:
|
||||
|
||||
- `doveauth <https://github.com/chatmail/relay/blob/main/chatmaild/src/chatmaild/doveauth.py>`_
|
||||
- :repofile:`doveauth <chatmaild/src/chatmaild/doveauth.py>`
|
||||
implements create-on-login address semantics and is used by Dovecot
|
||||
during IMAP login and by Postfix during SMTP/SUBMISSION login which
|
||||
in turn uses `Dovecot SASL
|
||||
<https://doc.dovecot.org/2.3/configuration_manual/authentication/dict/#complete-example-for-authenticating-via-a-unix-socket>`_
|
||||
to authenticate logins.
|
||||
|
||||
- `chatmail-metadata <https://github.com/chatmail/relay/blob/main/chatmaild/src/chatmaild/metadata.py>`_
|
||||
is contacted by a `Dovecot lua
|
||||
script <https://github.com/chatmail/relay/blob/main/cmdeploy/src/cmdeploy/dovecot/push_notification.lua>`_
|
||||
to store user-specific relay-side config. On new messages, it `passes
|
||||
the user’s push notification
|
||||
token <https://github.com/chatmail/relay/blob/main/chatmaild/src/chatmaild/notifier.py>`_
|
||||
- :repofile:`chatmail-metadata <chatmaild/src/chatmaild/metadata.py>`
|
||||
is contacted by a
|
||||
:repofile:`Dovecot lua script <cmdeploy/src/cmdeploy/dovecot/push_notification.lua>`
|
||||
to store user-specific relay-side config. On new messages, it
|
||||
:repofile:`passes the user’s push notification token <chatmaild/src/chatmaild/notifier.py>`
|
||||
to
|
||||
`notifications.delta.chat <https://delta.chat/en/help#instant-delivery>`_
|
||||
so the push notifications on the user’s phone can be triggered by
|
||||
Apple/Google/Huawei.
|
||||
|
||||
- `chatmail-expire <https://github.com/chatmail/relay/blob/main/chatmaild/src/chatmaild/expire.py>`_
|
||||
- :repofile:`chatmail-expire <chatmaild/src/chatmaild/expire.py>`
|
||||
deletes old messages, large messages, and entire mailboxes
|
||||
of users who have not logged in for longer than
|
||||
``delete_inactive_users_after`` days.
|
||||
@@ -109,15 +108,14 @@ short overview of ``chatmaild`` services:
|
||||
- ``chatmail-quota-expire`` is called by Dovecot's ``quota_warning`` mechanism
|
||||
and will automatically remove oldest messages to keep mailboxes well under ``max_mailbox_size``.
|
||||
|
||||
- `lastlogin <https://github.com/chatmail/relay/blob/main/chatmaild/src/chatmaild/lastlogin.py>`_
|
||||
- :repofile:`lastlogin <chatmaild/src/chatmaild/lastlogin.py>`
|
||||
is contacted by Dovecot when a user logs in and stores the date of
|
||||
the login.
|
||||
|
||||
``www/``
|
||||
~~~~~~~~~
|
||||
|
||||
`www <https://github.com/chatmail/relay/tree/main/www>`_ contains
|
||||
the html, css, and markdown files which make up a chatmail relay’s
|
||||
:repodir:`www` contains the html, css, and markdown files which make up a chatmail relay’s
|
||||
web page. Edit them before deploying to make your chatmail relay
|
||||
stand out.
|
||||
|
||||
@@ -249,6 +247,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
|
||||
:repofile:`appversions.json <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
|
||||
~~~~~~~~~~~~
|
||||
|
||||
|
||||
@@ -14,10 +14,11 @@ We know of three work-in-progress alternative implementation efforts:
|
||||
it to support all of the features and configuration settings required
|
||||
to operate as a chatmail relay.
|
||||
|
||||
- `Madmail <https://github.com/themadorg/madmail>`_: an
|
||||
experimental fork of `Maddy Mail Server <https://maddy.email/>`_, modified
|
||||
for chatmail deployments. It provides a single binary solution
|
||||
for running a chatmail relay.
|
||||
- `Madmail <https://github.com/themadorg/madmail>`_: a Rust-based
|
||||
single-binary chatmail relay. Madmail v2 is a rewrite of an earlier
|
||||
experimental fork of `Maddy Mail Server <https://maddy.email/>`_.
|
||||
It includes SMTP, IMAP, encryption enforcement, and real-time
|
||||
services (TURN/Iroh), and runs on Linux and Windows.
|
||||
|
||||
- `Chatmail Cookbook <https://github.com/feld/chatmail-cookbook>`_:
|
||||
A Chef Cookbook implementing a relay server. The project follows the
|
||||
|
||||
Reference in New Issue
Block a user