From e683d13f32c7a49dc1576f684f2eb0eea2c2ba07 Mon Sep 17 00:00:00 2001 From: missytake Date: Tue, 8 Jul 2025 16:30:54 +0200 Subject: [PATCH 1/5] doveauth: add invite_token to override nocreate file --- chatmaild/src/chatmaild/config.py | 1 + chatmaild/src/chatmaild/doveauth.py | 7 ++++--- chatmaild/src/chatmaild/newemail.py | 5 ++++- cmdeploy/src/cmdeploy/__init__.py | 9 +++++---- cmdeploy/src/cmdeploy/nginx/nginx.conf.j2 | 1 + 5 files changed, 15 insertions(+), 8 deletions(-) diff --git a/chatmaild/src/chatmaild/config.py b/chatmaild/src/chatmaild/config.py index 0cac3ea4..b59e48dd 100644 --- a/chatmaild/src/chatmaild/config.py +++ b/chatmaild/src/chatmaild/config.py @@ -31,6 +31,7 @@ def __init__(self, inipath, params): self.username_min_length = int(params["username_min_length"]) self.username_max_length = int(params["username_max_length"]) self.password_min_length = int(params["password_min_length"]) + self.invite_token = params["invite_token"] self.passthrough_senders = params["passthrough_senders"].split() self.passthrough_recipients = params["passthrough_recipients"].split() self.filtermail_smtp_port = int(params["filtermail_smtp_port"]) diff --git a/chatmaild/src/chatmaild/doveauth.py b/chatmaild/src/chatmaild/doveauth.py index e6292a36..70589214 100644 --- a/chatmaild/src/chatmaild/doveauth.py +++ b/chatmaild/src/chatmaild/doveauth.py @@ -24,10 +24,11 @@ def encrypt_password(password: str): def is_allowed_to_create(config: Config, user, cleartext_password) -> bool: """Return True if user and password are admissable.""" if os.path.exists(NOCREATE_FILE): - logging.warning(f"blocked account creation because {NOCREATE_FILE!r} exists.") - return False + if config.invite_token and config.invite_token not in cleartext_password: + logging.warning(f"blocked account creation because {NOCREATE_FILE!r} exists.") + return False - if len(cleartext_password) < config.password_min_length: + if len(cleartext_password.replace(config.invite_token, "")) < config.password_min_length: logging.warning( "Password needs to be at least %s characters long", config.password_min_length, diff --git a/chatmaild/src/chatmaild/newemail.py b/chatmaild/src/chatmaild/newemail.py index fbf976af..cf483ff2 100644 --- a/chatmaild/src/chatmaild/newemail.py +++ b/chatmaild/src/chatmaild/newemail.py @@ -3,6 +3,7 @@ """CGI script for creating new accounts.""" import json +import os import random import secrets import string @@ -20,7 +21,9 @@ def create_newemail_dict(config: Config): secrets.choice(ALPHANUMERIC_PUNCT) for _ in range(config.password_min_length + 3) ) - return dict(email=f"{user}@{config.mail_domain}", password=f"{password}") + redirect_uri = os.getenv("REQUEST_URI") + invite_token = redirect_uri[5:] if redirect_uri != "/new" else "" + return dict(email=f"{user}@{config.mail_domain}", password=f"{invite_token}{password}") def print_new_account(): diff --git a/cmdeploy/src/cmdeploy/__init__.py b/cmdeploy/src/cmdeploy/__init__.py index 20558d06..e35260ba 100644 --- a/cmdeploy/src/cmdeploy/__init__.py +++ b/cmdeploy/src/cmdeploy/__init__.py @@ -346,7 +346,7 @@ def _install_dovecot_package(package: str, arch: str): src=url, dest=deb_filename, sha256sum=sha256, - cache_time=9999999999999, # never redownload the package + cache_time=60 * 60 * 24 * 365 * 10, # never redownload the package ) apt.deb(name=f"Install dovecot-{package}", src=deb_filename) @@ -709,9 +709,10 @@ def deploy_chatmail(config_path: Path, disable_mail: bool) -> None: packages="postfix", ) - _install_dovecot_package("core", host.get_fact(facts.server.Arch)) - _install_dovecot_package("imapd", host.get_fact(facts.server.Arch)) - _install_dovecot_package("lmtpd", host.get_fact(facts.server.Arch)) + if not "dovecot.service" in host.get_fact(SystemdEnabled): + _install_dovecot_package("core", host.get_fact(facts.server.Arch)) + _install_dovecot_package("imapd", host.get_fact(facts.server.Arch)) + _install_dovecot_package("lmtpd", host.get_fact(facts.server.Arch)) apt.packages( name="Install nginx", diff --git a/cmdeploy/src/cmdeploy/nginx/nginx.conf.j2 b/cmdeploy/src/cmdeploy/nginx/nginx.conf.j2 index 8d27394c..7e72093f 100644 --- a/cmdeploy/src/cmdeploy/nginx/nginx.conf.j2 +++ b/cmdeploy/src/cmdeploy/nginx/nginx.conf.j2 @@ -90,6 +90,7 @@ http { fastcgi_pass unix:/run/fcgiwrap.socket; include /etc/nginx/fastcgi_params; fastcgi_param SCRIPT_FILENAME /usr/lib/cgi-bin/newemail.py; + fastcgi_param QUERY_STRING $query_string; } # Old URL for compatibility with e.g. printed QR codes. From 3fd11a471ae65a4e00f5f1303cd89eb79297c5e5 Mon Sep 17 00:00:00 2001 From: missytake Date: Tue, 8 Jul 2025 16:44:28 +0200 Subject: [PATCH 2/5] fix lint --- chatmaild/src/chatmaild/config.py | 2 +- chatmaild/src/chatmaild/doveauth.py | 9 +++++++-- chatmaild/src/chatmaild/newemail.py | 8 +++++--- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/chatmaild/src/chatmaild/config.py b/chatmaild/src/chatmaild/config.py index b59e48dd..bab53bbd 100644 --- a/chatmaild/src/chatmaild/config.py +++ b/chatmaild/src/chatmaild/config.py @@ -31,7 +31,7 @@ def __init__(self, inipath, params): self.username_min_length = int(params["username_min_length"]) self.username_max_length = int(params["username_max_length"]) self.password_min_length = int(params["password_min_length"]) - self.invite_token = params["invite_token"] + self.invite_token = params.get("invite_token", "") self.passthrough_senders = params["passthrough_senders"].split() self.passthrough_recipients = params["passthrough_recipients"].split() self.filtermail_smtp_port = int(params["filtermail_smtp_port"]) diff --git a/chatmaild/src/chatmaild/doveauth.py b/chatmaild/src/chatmaild/doveauth.py index 70589214..f1cc38fe 100644 --- a/chatmaild/src/chatmaild/doveauth.py +++ b/chatmaild/src/chatmaild/doveauth.py @@ -25,10 +25,15 @@ def is_allowed_to_create(config: Config, user, cleartext_password) -> bool: """Return True if user and password are admissable.""" if os.path.exists(NOCREATE_FILE): if config.invite_token and config.invite_token not in cleartext_password: - logging.warning(f"blocked account creation because {NOCREATE_FILE!r} exists.") + logging.warning( + f"blocked account creation because {NOCREATE_FILE!r} exists." + ) return False - if len(cleartext_password.replace(config.invite_token, "")) < config.password_min_length: + if ( + len(cleartext_password.replace(config.invite_token, "")) + < config.password_min_length + ): logging.warning( "Password needs to be at least %s characters long", config.password_min_length, diff --git a/chatmaild/src/chatmaild/newemail.py b/chatmaild/src/chatmaild/newemail.py index cf483ff2..6ca39cba 100644 --- a/chatmaild/src/chatmaild/newemail.py +++ b/chatmaild/src/chatmaild/newemail.py @@ -21,9 +21,11 @@ def create_newemail_dict(config: Config): secrets.choice(ALPHANUMERIC_PUNCT) for _ in range(config.password_min_length + 3) ) - redirect_uri = os.getenv("REQUEST_URI") - invite_token = redirect_uri[5:] if redirect_uri != "/new" else "" - return dict(email=f"{user}@{config.mail_domain}", password=f"{invite_token}{password}") + redirect_uri = os.getenv("REQUEST_URI", "/new") + invite_token = "" if redirect_uri == "/new" else redirect_uri[5:] + return dict( + email=f"{user}@{config.mail_domain}", password=f"{invite_token}{password}" + ) def print_new_account(): From 635bda1b1958594dcf1442560b9b83e4c7a208b8 Mon Sep 17 00:00:00 2001 From: missytake Date: Wed, 9 Jul 2025 01:19:46 +0200 Subject: [PATCH 3/5] tests: ensure valid invite token in password overrides nocreate file --- chatmaild/src/chatmaild/doveauth.py | 2 +- .../src/chatmaild/tests/test_doveauth.py | 34 +++++++++++++++---- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/chatmaild/src/chatmaild/doveauth.py b/chatmaild/src/chatmaild/doveauth.py index f1cc38fe..7127ef58 100644 --- a/chatmaild/src/chatmaild/doveauth.py +++ b/chatmaild/src/chatmaild/doveauth.py @@ -24,7 +24,7 @@ def encrypt_password(password: str): def is_allowed_to_create(config: Config, user, cleartext_password) -> bool: """Return True if user and password are admissable.""" if os.path.exists(NOCREATE_FILE): - if config.invite_token and config.invite_token not in cleartext_password: + if not config.invite_token or config.invite_token not in cleartext_password: logging.warning( f"blocked account creation because {NOCREATE_FILE!r} exists." ) diff --git a/chatmaild/src/chatmaild/tests/test_doveauth.py b/chatmaild/src/chatmaild/tests/test_doveauth.py index 078bec42..4feb44cf 100644 --- a/chatmaild/src/chatmaild/tests/test_doveauth.py +++ b/chatmaild/src/chatmaild/tests/test_doveauth.py @@ -64,12 +64,34 @@ def test_dont_overwrite_password_on_wrong_login(dictproxy): assert res["password"] == res2["password"] -def test_nocreate_file(monkeypatch, tmpdir, dictproxy): - p = tmpdir.join("nocreate") - p.write("") - monkeypatch.setattr(chatmaild.doveauth, "NOCREATE_FILE", str(p)) - dictproxy.lookup_passdb("newuser12@chat.example.org", "zequ0Aimuchoodaechik") - assert not dictproxy.lookup_userdb("newuser12@chat.example.org") +@pytest.mark.parametrize( + ["nocreate_file", "account", "invite_token", "password"], + [ + (False, True, "asdf", "asdfasdmaimfelsgwerw"), + (False, True, "asdf", "z9873240187420913798"), + (False, True, "", "dsaiujfw9fjiwf9w"), + (True, True, "asdf", "asdfmosadkdkfwdofkw"), + (True, False, "asdf", "z9873240187420913798"), + (True, False, "", "dsaiujfw9fjiwf9w"), + ], +) +def test_nocreate_file( + monkeypatch, + tmpdir, + dictproxy, + example_config, + nocreate_file: bool, + account: bool, + invite_token: str, + password: str, +): + if nocreate_file: + p = tmpdir.join("nocreate") + p.write("") + monkeypatch.setattr(chatmaild.doveauth, "NOCREATE_FILE", str(p)) + example_config.invite_token = invite_token + dictproxy.lookup_passdb("newuser12@chat.example.org", password) + assert bool(dictproxy.lookup_userdb("newuser12@chat.example.org")) == account def test_handle_dovecot_request(dictproxy): From 62bd9007f53e35993eb4c73d163b58e895af67e5 Mon Sep 17 00:00:00 2001 From: missytake Date: Wed, 9 Jul 2025 09:01:05 +0200 Subject: [PATCH 4/5] nginx: pass on invite tokens even for GET requests --- cmdeploy/src/cmdeploy/nginx/nginx.conf.j2 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmdeploy/src/cmdeploy/nginx/nginx.conf.j2 b/cmdeploy/src/cmdeploy/nginx/nginx.conf.j2 index 7e72093f..644b35b8 100644 --- a/cmdeploy/src/cmdeploy/nginx/nginx.conf.j2 +++ b/cmdeploy/src/cmdeploy/nginx/nginx.conf.j2 @@ -84,7 +84,7 @@ http { if ($request_method = GET) { # Redirect to Delta Chat, # which will in turn do a POST request. - return 301 dcaccount:https://{{ config.domain_name }}/new; + return 301 dcaccount:https://{{ config.domain_name }}$request_uri; } fastcgi_pass unix:/run/fcgiwrap.socket; @@ -101,7 +101,7 @@ http { # Redirects are only for browsers. location /cgi-bin/newemail.py { if ($request_method = GET) { - return 301 dcaccount:https://{{ config.domain_name }}/new; + return 301 dcaccount:https://{{ config.domain_name }}$request_uri; } fastcgi_pass unix:/run/fcgiwrap.socket; From 00afbb4c22b2bc656a2c564c7e9b9aa6f6b2fe7f Mon Sep 17 00:00:00 2001 From: missytake Date: Wed, 9 Jul 2025 12:42:25 +0200 Subject: [PATCH 5/5] add changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fcd987ed..dd3156c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## untagged +- Enable invite-only chatmail relays with invite tokens + that can override disabled account creation + ([#600](https://github.com/chatmail/relay/pull/600)) + - Expire push notification tokens after 90 days ([#583](https://github.com/chatmail/relay/pull/583))