diff --git a/chatmaild/src/chatmaild/config.py b/chatmaild/src/chatmaild/config.py index 30f73789..d81e2bc9 100644 --- a/chatmaild/src/chatmaild/config.py +++ b/chatmaild/src/chatmaild/config.py @@ -4,6 +4,8 @@ from pathlib import Path import iniconfig +echobot_password_path = Path("/run/echobot/password") + def read_config(inipath): assert Path(inipath).exists(), inipath @@ -45,21 +47,18 @@ class Config: return res raise ValueError(f"invalid address {addr!r}") - def get_user_dict(self, addr, enc_password=None): + def get_user_dict(self, addr, enc_password): home = self.get_user_maildir(addr) - res = dict(home=str(home), uid="vmail", gid="vmail") - if enc_password is not None: - res["password"] = enc_password - return res + return dict( + addr=addr, home=str(home), uid="vmail", gid="vmail", password=enc_password + ) def set_user_password(self, addr, enc_password): # reading and writing user data needs to be atomic # to allow concurrent logins to succeed. + assert not addr.startswith("echo@"), addr userdir = self.get_user_maildir(addr) - try: - userdir.mkdir() - except FileExistsError: - pass + userdir.mkdir(exist_ok=True) password_path = userdir.joinpath("password") password_path_tmp = userdir.joinpath("password.tmp") password_path_tmp.write_text(enc_password) diff --git a/chatmaild/src/chatmaild/doveauth.py b/chatmaild/src/chatmaild/doveauth.py index cc34f17e..ef7454ad 100644 --- a/chatmaild/src/chatmaild/doveauth.py +++ b/chatmaild/src/chatmaild/doveauth.py @@ -4,7 +4,7 @@ import logging import os import sys -from .config import Config, read_config +from .config import Config, echobot_password_path, read_config from .dictproxy import DictProxy NOCREATE_FILE = "/etc/chatmail-nocreate" @@ -131,12 +131,17 @@ class AuthDictProxy(DictProxy): def lookup_userdb(self, user): userdir = self.config.get_user_maildir(user) - password_path = userdir.joinpath("password") - result = {} - if password_path.exists(): - result = dict(addr=user, password=password_path.read_text()) - result.update(self.config.get_user_dict(user)) - return result + if user.startswith("echo@"): + password_path = echobot_password_path + else: + password_path = userdir.joinpath("password") + + try: + enc_password = password_path.read_text() + except FileNotFoundError: + return {} + else: + return self.config.get_user_dict(user, enc_password=enc_password) def lookup_passdb(self, user, cleartext_password): userdata = self.lookup_userdb(user) diff --git a/chatmaild/src/chatmaild/echo.py b/chatmaild/src/chatmaild/echo.py index 980b1dc6..c0f45fca 100644 --- a/chatmaild/src/chatmaild/echo.py +++ b/chatmaild/src/chatmaild/echo.py @@ -6,11 +6,13 @@ it will echo back any message that has non-empty text and also supports the /hel import logging import os +import subprocess import sys from deltachat_rpc_client import Bot, DeltaChat, EventType, Rpc, events -from chatmaild.config import read_config +from chatmaild.config import echobot_password_path, read_config +from chatmaild.doveauth import encrypt_password from chatmaild.newemail import create_newemail_dict hooks = events.HookCollection() @@ -85,10 +87,16 @@ def main(): password = bot.account.get_config("mail_pw") else: password = create_newemail_dict(config)["password"] - config.set_user_password(addr, password) + + echobot_password_path.write_text(encrypt_password(password)) + # Give the user which doveauth runs as access to the password file. + subprocess.check_call( + ["/usr/bin/setfacl", "-m", "user:vmail:r", echobot_password_path], + ) if not bot.is_configured(): bot.configure(addr, password) + bot.run_forever() diff --git a/chatmaild/src/chatmaild/lastlogin.py b/chatmaild/src/chatmaild/lastlogin.py index 9adadef7..ebec6058 100644 --- a/chatmaild/src/chatmaild/lastlogin.py +++ b/chatmaild/src/chatmaild/lastlogin.py @@ -6,7 +6,7 @@ from .config import read_config from .dictproxy import DictProxy # this file's mtime reflects the last login-time for a user -LAST_LOGIN = "last-login" +LAST_LOGIN = "password" def get_daytimestamp(timestamp) -> int: @@ -16,31 +16,16 @@ def get_daytimestamp(timestamp) -> int: def write_last_login_to_userdir(userdir, timestamp): target = userdir.joinpath(LAST_LOGIN) timestamp = get_daytimestamp(timestamp) - try: - st = target.stat() - except FileNotFoundError: - # only happens on initial login - userdir.mkdir(exist_ok=True) - target.touch() + st = target.stat() + if int(st.st_mtime) != timestamp: os.utime(target, (timestamp, timestamp)) - else: - if st.st_mtime < timestamp: - os.utime(target, (timestamp, timestamp)) def get_last_login_from_userdir(userdir) -> int: if "@" not in userdir.name: return get_daytimestamp(time.time()) target = userdir.joinpath(LAST_LOGIN) - try: - return int(target.stat().st_mtime) - except FileNotFoundError: - # during migration many directories will not have last-login file - # so we write it here to the current time - target.touch() - timestamp = get_daytimestamp(time.time()) - os.utime(target, (timestamp, timestamp)) - return timestamp + return int(target.stat().st_mtime) class LastLoginDictProxy(DictProxy): diff --git a/chatmaild/src/chatmaild/tests/test_config.py b/chatmaild/src/chatmaild/tests/test_config.py index 8f840887..25f870f2 100644 --- a/chatmaild/src/chatmaild/tests/test_config.py +++ b/chatmaild/src/chatmaild/tests/test_config.py @@ -63,12 +63,6 @@ def test_config_userstate_paths(make_config, tmp_path): def test_config_get_user_dict(make_config, tmp_path): config = make_config("something.testrun.org") - data = config.get_user_dict("user1@something.org") - assert data["home"] - assert data["uid"] == "vmail" - assert data["gid"] == "vmail" - assert "password" not in data - addr = "user1@something.org" enc_password = "l1k2j31lk2j3l1k23j123" data = config.get_user_dict(addr, enc_password=enc_password) diff --git a/chatmaild/src/chatmaild/tests/test_delete_inactive_users.py b/chatmaild/src/chatmaild/tests/test_delete_inactive_users.py index df39db18..e546d76d 100644 --- a/chatmaild/src/chatmaild/tests/test_delete_inactive_users.py +++ b/chatmaild/src/chatmaild/tests/test_delete_inactive_users.py @@ -8,6 +8,7 @@ from chatmaild.lastlogin import get_last_login_from_userdir, write_last_login_to def test_login_timestamps(tmp_path): userdir = tmp_path.joinpath("someuser@chat.example.org") userdir.mkdir() + userdir.joinpath("password").touch() write_last_login_to_userdir(userdir, timestamp=100000) assert get_last_login_from_userdir(userdir) == 86400 diff --git a/chatmaild/src/chatmaild/tests/test_lastlogin.py b/chatmaild/src/chatmaild/tests/test_lastlogin.py index 3d02f8a6..498da412 100644 --- a/chatmaild/src/chatmaild/tests/test_lastlogin.py +++ b/chatmaild/src/chatmaild/tests/test_lastlogin.py @@ -1,6 +1,7 @@ import time import pytest +from chatmaild.doveauth import AuthDictProxy from chatmaild.lastlogin import ( LastLoginDictProxy, get_last_login_from_userdir, @@ -16,6 +17,9 @@ def testaddr(): def test_handle_dovecot_request_last_login(testaddr, example_config): dictproxy = LastLoginDictProxy(config=example_config) + authproxy = AuthDictProxy(config=example_config) + authproxy.lookup_passdb(testaddr, "1l2k3j1l2k3jl123") + userdir = dictproxy.config.get_user_maildir(testaddr) # set last-login info for user @@ -41,8 +45,9 @@ def test_handle_dovecot_request_last_login(testaddr, example_config): def test_login_timestamp(testaddr, example_config): dictproxy = LastLoginDictProxy(config=example_config) + authproxy = AuthDictProxy(config=example_config) + authproxy.lookup_passdb(testaddr, "1l2k3j1l2k3jl123") userdir = dictproxy.config.get_user_maildir(testaddr) - userdir.mkdir() write_last_login_to_userdir(userdir, timestamp=100000) assert get_last_login_from_userdir(userdir) == 86400