mirror of
https://github.com/chatmail/relay.git
synced 2026-05-10 16:04:37 +00:00
delete users from mailboxes_dir
This commit is contained in:
@@ -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():
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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, [])
|
||||||
|
|||||||
@@ -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 = []
|
||||||
|
|||||||
@@ -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(
|
||||||
[
|
[
|
||||||
|
|||||||
Reference in New Issue
Block a user