introduce "mailboxes_dir" config ini option to avoid hardcoding /home/vmail/mail/....

in source code and to improve testability.
This commit is contained in:
holger krekel
2024-07-09 14:50:27 +02:00
parent 4b0600a453
commit c8661fd135
9 changed files with 59 additions and 34 deletions

View File

@@ -2,6 +2,11 @@
## untagged ## untagged
- BREAKING: new required chatmail.ini value
'mailboxes_dir = /home/vmail/mail/{mail_domain}'
reducing the hardcoding on that directory and improving testability.
([#351](https://github.com/deltachat/chatmail/pull/351))
- BREAKING: new required chatmail.ini value 'delete_inactive_users_after = 100' - BREAKING: new required chatmail.ini value 'delete_inactive_users_after = 100'
which removes users from database and mails after 100 days without any login. which removes users from database and mails after 100 days without any login.
([#350](https://github.com/deltachat/chatmail/pull/350)) ([#350](https://github.com/deltachat/chatmail/pull/350))

View File

@@ -3,17 +3,15 @@ from pathlib import Path
import iniconfig import iniconfig
def read_config(inipath, mail_basedir=None): def read_config(inipath):
assert Path(inipath).exists(), inipath assert Path(inipath).exists(), inipath
cfg = iniconfig.IniConfig(inipath) cfg = iniconfig.IniConfig(inipath)
params = cfg.sections["params"] params = cfg.sections["params"]
if mail_basedir is None: return Config(inipath, params=params)
mail_basedir = Path(f"/home/vmail/mail/{params['mail_domain']}")
return Config(inipath, params=params, mail_basedir=mail_basedir)
class Config: class Config:
def __init__(self, inipath, params, mail_basedir: Path): def __init__(self, inipath, params):
self._inipath = inipath self._inipath = inipath
self.mail_domain = params["mail_domain"] self.mail_domain = params["mail_domain"]
self.max_user_send_per_minute = int(params["max_user_send_per_minute"]) self.max_user_send_per_minute = int(params["max_user_send_per_minute"])
@@ -25,6 +23,7 @@ class Config:
self.password_min_length = int(params["password_min_length"]) self.password_min_length = int(params["password_min_length"])
self.passthrough_senders = params["passthrough_senders"].split() self.passthrough_senders = params["passthrough_senders"].split()
self.passthrough_recipients = params["passthrough_recipients"].split() self.passthrough_recipients = params["passthrough_recipients"].split()
self.mailboxes_dir = params["mailboxes_dir"].strip().rstrip("/")
self.filtermail_smtp_port = int(params["filtermail_smtp_port"]) self.filtermail_smtp_port = int(params["filtermail_smtp_port"])
self.postfix_reinject_port = int(params["postfix_reinject_port"]) self.postfix_reinject_port = int(params["postfix_reinject_port"])
self.iroh_relay = params.get("iroh_relay") self.iroh_relay = params.get("iroh_relay")
@@ -32,26 +31,40 @@ class Config:
self.privacy_mail = params.get("privacy_mail") self.privacy_mail = params.get("privacy_mail")
self.privacy_pdo = params.get("privacy_pdo") self.privacy_pdo = params.get("privacy_pdo")
self.privacy_supervisor = params.get("privacy_supervisor") self.privacy_supervisor = params.get("privacy_supervisor")
self.mail_basedir = mail_basedir
def _getbytefile(self): def _getbytefile(self):
return open(self._inipath, "rb") return open(self._inipath, "rb")
def get_user_maildir(self, addr): def get_user_maildir(self, addr):
if addr and addr != "." and "/" not in addr: if addr and addr != "." and "/" not in addr:
res = self.mail_basedir.joinpath(addr).resolve() res = Path(self.mailboxes_dir).joinpath(addr).resolve()
if res.is_relative_to(self.mail_basedir): if res.is_relative_to(self.mailboxes_dir):
return res return str(res)
raise ValueError(f"invalid address {addr!r}") raise ValueError(f"invalid address {addr!r}")
def write_initial_config(inipath, mail_domain): def write_initial_config(inipath, mail_domain, **overrides):
"""Write out default config file, using the specified config value overrides."""
from importlib.resources import files from importlib.resources import files
inidir = files(__package__).joinpath("ini") inidir = files(__package__).joinpath("ini")
content = ( source_inipath = inidir.joinpath("chatmail.ini.f")
inidir.joinpath("chatmail.ini.f").read_text().format(mail_domain=mail_domain) content = source_inipath.read_text().format(mail_domain=mail_domain)
)
# apply config overrides
newlines = []
for line in content.split("\n"):
newline = line.strip()
if newline and newline[0] not in "#[":
name, value = newline.split(" =", maxsplit=1)
value = overrides.get(name.strip(), value.strip())
newline = f"{name.strip()} = {value.strip()}"
newlines.append(newline)
content = "\n".join(newlines)
# apply testrun privacy overrides
if mail_domain.endswith(".testrun.org"): if mail_domain.endswith(".testrun.org"):
override_inipath = inidir.joinpath("override-testrun.ini") override_inipath = inidir.joinpath("override-testrun.ini")
privacy = iniconfig.IniConfig(override_inipath)["privacy"] privacy = iniconfig.IniConfig(override_inipath)["privacy"]

View File

@@ -68,7 +68,7 @@ def is_allowed_to_create(config: Config, user, cleartext_password) -> bool:
def get_user_data(db, config: Config, user): def get_user_data(db, config: Config, user):
if user == f"echo@{config.mail_domain}": if user == f"echo@{config.mail_domain}":
return dict( return dict(
home=f"/home/vmail/mail/{config.mail_domain}/echo@{config.mail_domain}", home=config.get_user_maildir(user),
uid="vmail", uid="vmail",
gid="vmail", gid="vmail",
) )
@@ -76,7 +76,7 @@ def get_user_data(db, config: Config, user):
with db.read_connection() as conn: with db.read_connection() as conn:
result = conn.get_user(user) result = conn.get_user(user)
if result: if result:
result["home"] = f"/home/vmail/mail/{config.mail_domain}/{user}" result["home"] = config.get_user_maildir(user)
result["uid"] = "vmail" result["uid"] = "vmail"
result["gid"] = "vmail" result["gid"] = "vmail"
return result return result
@@ -96,7 +96,7 @@ def lookup_passdb(db, config: Config, user, cleartext_password, last_login=None)
return None return None
return dict( return dict(
home=f"/home/vmail/mail/{config.mail_domain}/echo@{config.mail_domain}", home=config.get_user_maildir(user),
uid="vmail", uid="vmail",
gid="vmail", gid="vmail",
password=encrypt_password(password), password=encrypt_password(password),
@@ -114,7 +114,7 @@ def lookup_passdb(db, config: Config, user, cleartext_password, last_login=None)
"UPDATE users SET last_login=? WHERE addr=?", (last_login, user) "UPDATE users SET last_login=? WHERE addr=?", (last_login, user)
) )
userdata["home"] = f"/home/vmail/mail/{config.mail_domain}/{user}" userdata["home"] = config.get_user_maildir(user)
userdata["uid"] = "vmail" userdata["uid"] = "vmail"
userdata["gid"] = "vmail" userdata["gid"] = "vmail"
return userdata return userdata
@@ -127,7 +127,7 @@ def lookup_passdb(db, config: Config, user, cleartext_password, last_login=None)
conn.execute(q, (user, encrypted_password, last_login)) conn.execute(q, (user, encrypted_password, last_login))
print(f"Created address: {user}", file=sys.stderr) print(f"Created address: {user}", file=sys.stderr)
return dict( return dict(
home=f"/home/vmail/mail/{config.mail_domain}/{user}", home=config.get_user_maildir(user),
uid="vmail", uid="vmail",
gid="vmail", gid="vmail",
password=encrypted_password, password=encrypted_password,

View File

@@ -42,6 +42,9 @@ passthrough_recipients = xstore@testrun.org groupsbot@hispanilandia.net
# Deployment Details # Deployment Details
# #
# Directory where user mailboxes are stored
mailboxes_dir = /home/vmail/mail/{mail_domain}
# where the filtermail SMTP service listens # where the filtermail SMTP service listens
filtermail_smtp_port = 10080 filtermail_smtp_port = 10080
@@ -63,4 +66,3 @@ privacy_pdo =
# postal address of the privacy supervisor # postal address of the privacy supervisor
privacy_supervisor = privacy_supervisor =

View File

@@ -16,10 +16,11 @@ def make_config(tmp_path):
inipath = tmp_path.joinpath("chatmail.ini") inipath = tmp_path.joinpath("chatmail.ini")
def make_conf(mail_domain): def make_conf(mail_domain):
write_initial_config(inipath, mail_domain=mail_domain)
basedir = tmp_path.joinpath(f"vmail/{mail_domain}") basedir = tmp_path.joinpath(f"vmail/{mail_domain}")
basedir.mkdir(parents=True, exist_ok=True) basedir.mkdir(parents=True, exist_ok=True)
return read_config(inipath, mail_basedir=basedir) overrides = dict(mailboxes_dir=str(basedir))
write_initial_config(inipath, mail_domain=mail_domain, **overrides)
return read_config(inipath)
return make_conf return make_conf

View File

@@ -1,3 +1,5 @@
from pathlib import Path
import pytest import pytest
from chatmaild.config import read_config from chatmaild.config import read_config
@@ -35,11 +37,12 @@ def test_read_config_testrun(make_config):
def test_get_user_maildir(make_config): def test_get_user_maildir(make_config):
config = make_config("something.testrun.org") config = make_config("something.testrun.org")
assert config.mail_basedir.name == "something.testrun.org" mailboxes_dir = Path(config.mailboxes_dir)
assert mailboxes_dir.name == "something.testrun.org"
assert config.mail_domain == "something.testrun.org" assert config.mail_domain == "something.testrun.org"
path = config.get_user_maildir("user1@something.testrun.org") path = Path(config.get_user_maildir("user1@something.testrun.org"))
assert not path.exists() assert not path.exists()
assert path == config.mail_basedir.joinpath("user1@something.testrun.org") assert path == mailboxes_dir.joinpath("user1@something.testrun.org")
with pytest.raises(ValueError): with pytest.raises(ValueError):
config.get_user_maildir("") config.get_user_maildir("")

View File

@@ -1,4 +1,5 @@
import time import time
from pathlib import Path
from chatmaild.delete_inactive_users import delete_inactive_users from chatmaild.delete_inactive_users import delete_inactive_users
from chatmaild.doveauth import lookup_passdb from chatmaild.doveauth import lookup_passdb
@@ -8,9 +9,12 @@ def test_remove_stale_users(db, example_config):
new = time.time() new = time.time()
old = new - (example_config.delete_inactive_users_after * 86400) - 1 old = new - (example_config.delete_inactive_users_after * 86400) - 1
def get_user_path(addr):
return Path(example_config.get_user_maildir(addr))
def create_user(addr, last_login): def create_user(addr, last_login):
lookup_passdb(db, example_config, addr, "q9mr3faue", last_login=last_login) lookup_passdb(db, example_config, addr, "q9mr3faue", last_login=last_login)
md = example_config.get_user_maildir(addr) md = get_user_path(addr)
md.mkdir(parents=True) md.mkdir(parents=True)
md.joinpath("cur").mkdir() md.joinpath("cur").mkdir()
md.joinpath("cur", "something").mkdir() md.joinpath("cur", "something").mkdir()
@@ -33,19 +37,19 @@ def test_remove_stale_users(db, example_config):
# check pre and post-conditions for delete_inactive_users() # check pre and post-conditions for delete_inactive_users()
for addr in to_remove: for addr in to_remove:
assert example_config.get_user_maildir(addr).exists() assert get_user_path(addr).exists()
delete_inactive_users(db, example_config) delete_inactive_users(db, example_config)
for p in example_config.mail_basedir.iterdir(): for p in Path(example_config.mailboxes_dir).iterdir():
assert not p.name.startswith("old") assert not p.name.startswith("old")
for addr in to_remove: for addr in to_remove:
assert not get_user_path(addr).exists()
with db.read_connection() as conn: with db.read_connection() as conn:
assert not conn.get_user(addr) assert not conn.get_user(addr)
assert not example_config.get_user_maildir(addr).exists()
for addr in remain: for addr in remain:
assert example_config.get_user_maildir(addr).exists() assert get_user_path(addr).exists()
with db.read_connection() as conn: with db.read_connection() as conn:
assert conn.get_user(addr) assert conn.get_user(addr)

View File

@@ -114,10 +114,7 @@ def test_handle_dovecot_request(db, example_config):
assert res assert res
assert res[0] == "O" and res.endswith("\n") assert res[0] == "O" and res.endswith("\n")
userdata = json.loads(res[1:].strip()) userdata = json.loads(res[1:].strip())
assert ( assert userdata["home"].endswith("chat.example.org/some42123@chat.example.org")
userdata["home"]
== "/home/vmail/mail/chat.example.org/some42123@chat.example.org"
)
assert userdata["uid"] == userdata["gid"] == "vmail" assert userdata["uid"] == userdata["gid"] == "vmail"
assert userdata["password"].startswith("{SHA512-CRYPT}") assert userdata["password"].startswith("{SHA512-CRYPT}")

View File

@@ -67,7 +67,7 @@ userdb {
## ##
# Mailboxes are stored in the "mail" directory of the vmail user home. # Mailboxes are stored in the "mail" directory of the vmail user home.
mail_location = maildir:/home/vmail/mail/%d/%u mail_location = maildir:{{ config.mailboxes_dir }}/%u
namespace inbox { namespace inbox {
inbox = yes inbox = yes