delete users from mailboxes_dir

This commit is contained in:
holger krekel
2024-07-10 02:42:33 +02:00
parent ad151c2cc1
commit 4dbb19db46
5 changed files with 80 additions and 57 deletions

View File

@@ -2,28 +2,65 @@
Remove inactive users Remove inactive users
""" """
import os
import shutil import shutil
import sys import sys
import time import time
from pathlib import Path
from .config import read_config from .config import read_config
from .database import Database from .database import Database
from .doveauth import iter_userdb_lastlogin_before
LAST_LOGIN = "last-login"
def delete_inactive_users(db, config, CHUNK=100): def write_last_login_to_userdir(userdir, timestamp):
target = userdir.joinpath(LAST_LOGIN)
timestamp = int(timestamp // 86400 * 86400)
try:
st = target.stat()
except FileNotFoundError:
# only happens on initial login
userdir.mkdir(exist_ok=True)
target.write_text("")
os.utime(target, (timestamp, timestamp))
else:
if st.st_mtime < timestamp:
os.utime(target, (timestamp, timestamp))
def get_last_login_from_userdir(userdir):
target = userdir.joinpath(LAST_LOGIN)
try:
return int(target.stat().st_mtime)
except FileNotFoundError:
target.write_text("")
timestamp = int(time.time() // 86400 * 86400)
os.utime(target, (timestamp, timestamp))
return timestamp
def delete_inactive_users(db, config, chunksize=100):
cutoff_date = time.time() - config.delete_inactive_users_after * 86400 cutoff_date = time.time() - config.delete_inactive_users_after * 86400
pending = []
old_users = iter_userdb_lastlogin_before(db, cutoff_date) def remove(userdir):
chunks = (old_users[i : i + CHUNK] for i in range(0, len(old_users), CHUNK)) shutil.rmtree(userdir, ignore_errors=True)
for sublist in chunks: pending.append(userdir.name)
for user in sublist: if len(pending) > chunksize:
user_mail_dir = config.get_user_maildir(user) clear_pending()
shutil.rmtree(user_mail_dir, ignore_errors=True)
def clear_pending():
with db.write_transaction() as conn: with db.write_transaction() as conn:
for user in sublist: for user in pending:
conn.execute("DELETE FROM users WHERE addr = ?", (user,)) conn.execute("DELETE FROM users WHERE addr = ?", (user,))
pending[:] = []
for userdir in Path(config.mailboxes_dir).iterdir():
if get_last_login_from_userdir(userdir) < cutoff_date:
remove(userdir)
clear_pending()
def main(): def main():

View File

@@ -123,18 +123,7 @@ def lookup_passdb(db, config: Config, user, cleartext_password):
def iter_userdb(db) -> list: def iter_userdb(db) -> list:
"""Get a list of all user addresses.""" """Get a list of all user addresses."""
with db.read_connection() as conn: with db.read_connection() as conn:
rows = conn.execute( rows = conn.execute("SELECT addr from users").fetchall()
"SELECT addr from users",
).fetchall()
return [x[0] for x in rows]
def iter_userdb_lastlogin_before(db, cutoff_date):
"""Get a list of users where last login was before cutoff_date."""
with db.read_connection() as conn:
rows = conn.execute(
"SELECT addr FROM users WHERE last_login < ?", (cutoff_date,)
).fetchall()
return [x[0] for x in rows] return [x[0] for x in rows]

View File

@@ -18,16 +18,6 @@ class Metadata:
def get_metadata_dict(self, addr): def get_metadata_dict(self, addr):
return FileDict(self.vmail_dir / addr / "metadata.json") return FileDict(self.vmail_dir / addr / "metadata.json")
def write_login_timestamp(self, addr, timestamp):
# day resolution is enough for timestamp
timestamp = int(timestamp) // 86400 * 86400
target_file = self.vmail_dir.joinpath(addr, "last-login")
try:
target_file.write_text(str(timestamp))
except FileNotFoundError:
target_file.parent.mkdir()
target_file.write_text(str(timestamp))
def add_token_to_addr(self, addr, token): def add_token_to_addr(self, addr, token):
with self.get_metadata_dict(addr).modify() as data: with self.get_metadata_dict(addr).modify() as data:
tokens = data.setdefault(self.DEVICETOKEN_KEY, []) tokens = data.setdefault(self.DEVICETOKEN_KEY, [])

View File

@@ -1,19 +1,37 @@
import time import time
from chatmaild.delete_inactive_users import delete_inactive_users from chatmaild.delete_inactive_users import (
delete_inactive_users,
get_last_login_from_userdir,
write_last_login_to_userdir,
)
from chatmaild.doveauth import lookup_passdb from chatmaild.doveauth import lookup_passdb
def test_remove_stale_users(db, example_config): def test_login_timestamps(tmp_path):
userdir = tmp_path.joinpath("someuser")
userdir.mkdir()
write_last_login_to_userdir(userdir, timestamp=100000)
assert get_last_login_from_userdir(userdir) == 86400
write_last_login_to_userdir(userdir, timestamp=200000)
assert get_last_login_from_userdir(userdir) == 86400 * 2
write_last_login_to_userdir(userdir, timestamp=200000)
assert get_last_login_from_userdir(userdir) == 86400 * 2
def test_delete_inactive_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 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")
md = example_config.get_user_maildir(addr) md = example_config.get_user_maildir(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()
write_last_login_to_userdir(md, timestamp=last_login)
# create some stale and some new accounts # create some stale and some new accounts
to_remove = [] to_remove = []

View File

@@ -3,6 +3,7 @@ import time
import pytest import pytest
import requests import requests
from chatmaild.delete_inactive_users import get_last_login_from_userdir
from chatmaild.metadata import ( from chatmaild.metadata import (
Metadata, Metadata,
MetadataDictProxy, MetadataDictProxy,
@@ -92,10 +93,10 @@ def test_notifier_remove_without_set(metadata, testaddr):
assert not metadata.get_tokens_for_addr(testaddr) assert not metadata.get_tokens_for_addr(testaddr)
<<<<<<< HEAD
def test_handle_dovecot_request_lookup_fails(dictproxy, testaddr): def test_handle_dovecot_request_lookup_fails(dictproxy, testaddr):
res = dictproxy.handle_dovecot_request(f"Lpriv/123/chatmail\t{testaddr}") res = dictproxy.handle_dovecot_request(f"Lpriv/123/chatmail\t{testaddr}")
======= assert res == "N\n"
def test_metadata_login_timestamp(metadata, testaddr): def test_metadata_login_timestamp(metadata, testaddr):
timestamp = metadata.vmail_dir.joinpath(testaddr).mkdir() timestamp = metadata.vmail_dir.joinpath(testaddr).mkdir()
metadata.write_login_timestamp(testaddr, timestamp=100000) metadata.write_login_timestamp(testaddr, timestamp=100000)
@@ -107,14 +108,6 @@ def test_metadata_login_timestamp(metadata, testaddr):
assert int(timestamp) == 86400 * 2 assert int(timestamp) == 86400 * 2
def test_handle_dovecot_request_lookup_fails(notifier, metadata, testaddr):
res = handle_dovecot_request(
f"Lpriv/123/chatmail\t{testaddr}", {}, notifier, metadata
)
>>>>>>> 317d30f (write last login differently)
assert res == "N\n"
def test_handle_dovecot_request_happy_path(dictproxy, testaddr, token): def test_handle_dovecot_request_happy_path(dictproxy, testaddr, token):
metadata = dictproxy.metadata metadata = dictproxy.metadata
transactions = dictproxy.transactions transactions = dictproxy.transactions
@@ -151,37 +144,33 @@ def test_handle_dovecot_request_happy_path(dictproxy, testaddr, token):
assert queue_item.path.exists() assert queue_item.path.exists()
<<<<<<< HEAD
def test_handle_dovecot_protocol_set_devicetoken(dictproxy):
=======
def test_handle_dovecot_request_last_login(notifier, metadata, testaddr, token): def test_handle_dovecot_request_last_login(notifier, metadata, testaddr, token):
transactions = {} dictproxy = MetadataDictProxy(notifier=notifier, metadata=metadata)
userdir = metadata.vmail_dir.joinpath(testaddr) userdir = metadata.vmail_dir.joinpath(testaddr)
# set last-login info for user # set last-login info for user
tx = "1111" tx = "1111"
msg = f"B{tx}\t{testaddr}" msg = f"B{tx}\t{testaddr}"
res = handle_dovecot_request(msg, transactions, notifier, metadata) res = dictproxy.handle_dovecot_request(msg)
assert not res assert not res
assert transactions == {tx: dict(addr=testaddr, res="O\n")} assert dictproxy.transactions == {tx: dict(addr=testaddr, res="O\n")}
timestamp = int(time.time()) timestamp = int(time.time())
msg = f"S{tx}\tshared/last-login/{testaddr}\t{timestamp}" msg = f"S{tx}\tshared/last-login/{testaddr}\t{timestamp}"
res = handle_dovecot_request(msg, transactions, notifier, metadata) res = dictproxy.handle_dovecot_request(msg)
assert not res assert not res
assert len(transactions) == 1 assert len(dictproxy.transactions) == 1
read_timestamp = int(userdir.joinpath("last-login").read_text()) read_timestamp = get_last_login_from_userdir(userdir)
assert read_timestamp == timestamp // 86400 * 86400 assert read_timestamp == timestamp // 86400 * 86400
msg = f"C{tx}" msg = f"C{tx}"
res = handle_dovecot_request(msg, transactions, notifier, metadata) res = dictproxy.handle_dovecot_request(msg)
assert res == "O\n" assert res == "O\n"
assert len(transactions) == 0 assert len(dictproxy.transactions) == 0
def test_handle_dovecot_protocol_set_devicetoken(metadata, notifier): def test_handle_dovecot_protocol_set_devicetoken(dictproxy):
>>>>>>> 317d30f (write last login differently)
rfile = io.BytesIO( rfile = io.BytesIO(
b"\n".join( b"\n".join(
[ [