fix: simplify mailu sync retry

This commit is contained in:
Brad Stein 2026-01-21 05:32:49 -03:00
parent f332549a2d
commit 06d73a5307
2 changed files with 81 additions and 24 deletions

View File

@ -39,11 +39,21 @@ class MailuUserSyncResult:
updated: int = 0 updated: int = 0
skipped: int = 0 skipped: int = 0
failures: int = 0 failures: int = 0
mailboxes: int = 0
@dataclass(frozen=True)
class MailuSyncContext:
username: str
user_id: str
mailu_email: str
app_password: str
updated: int
display_name: str
class PasswordTooLongError(RuntimeError): class PasswordTooLongError(RuntimeError):
pass pass
mailboxes: int = 0
@dataclass @dataclass
@ -216,14 +226,17 @@ class MailuService:
return True return True
return self._is_service_account(user, username) return self._is_service_account(user, username)
def _sync_user(self, conn: psycopg.Connection, user: dict[str, Any]) -> MailuUserSyncResult: def _build_sync_context(
self,
user: dict[str, Any],
) -> tuple[MailuSyncContext | None, MailuUserSyncResult | None]:
username = self._username(user) username = self._username(user)
if self._should_skip_user(user, username): if self._should_skip_user(user, username):
return MailuUserSyncResult(skipped=1) return None, MailuUserSyncResult(skipped=1)
user_id = self._user_id(user) user_id = self._user_id(user)
if not user_id: if not user_id:
return MailuUserSyncResult(failures=1) return None, MailuUserSyncResult(failures=1)
attrs = user.get("attributes") attrs = user.get("attributes")
if not isinstance(attrs, dict): if not isinstance(attrs, dict):
@ -237,46 +250,79 @@ class MailuService:
enabled, updates, app_password = self._prepare_updates(username, attrs, mailu_email) enabled, updates, app_password = self._prepare_updates(username, attrs, mailu_email)
if not enabled: if not enabled:
return MailuUserSyncResult(skipped=1) return None, MailuUserSyncResult(skipped=1)
if not self._apply_updates(user_id, updates, username): if not self._apply_updates(user_id, updates, username):
return MailuUserSyncResult(failures=1) return None, MailuUserSyncResult(failures=1)
updated = 1 if updates else 0 updated = 1 if updates else 0
display_name = _display_name(user)
return (
MailuSyncContext(
username=username,
user_id=user_id,
mailu_email=mailu_email,
app_password=app_password,
updated=updated,
display_name=display_name,
),
None,
)
def _ensure_mailbox_with_retry(
self,
conn: psycopg.Connection,
ctx: MailuSyncContext,
) -> tuple[bool, bool, bool]:
mailbox_ok = False mailbox_ok = False
failed = False
rotated = False rotated = False
failed = False
try: try:
mailbox_ok = self._ensure_mailbox(conn, mailu_email, app_password, _display_name(user)) mailbox_ok = self._ensure_mailbox(conn, ctx.mailu_email, ctx.app_password, ctx.display_name)
except PasswordTooLongError as exc: except PasswordTooLongError:
rotated = True rotated = True
app_password = random_password(24) app_password = random_password(24)
try: try:
keycloak_admin.set_user_attribute(username, MAILU_APP_PASSWORD_ATTR, app_password) keycloak_admin.set_user_attribute(ctx.username, MAILU_APP_PASSWORD_ATTR, app_password)
logger.info( logger.info(
"mailu app password rotated", "mailu app password rotated",
extra={ extra={
"event": "mailu_sync", "event": "mailu_sync",
"status": "updated", "status": "updated",
"detail": "app password exceeded bcrypt limit", "detail": "app password exceeded bcrypt limit",
"username": username, "username": ctx.username,
}, },
) )
mailbox_ok = self._ensure_mailbox(conn, mailu_email, app_password, _display_name(user)) mailbox_ok = self._ensure_mailbox(conn, ctx.mailu_email, app_password, ctx.display_name)
except Exception as retry_exc: except Exception as retry_exc:
self._log_sync_error(username, str(retry_exc)) self._log_sync_error(ctx.username, str(retry_exc))
failed = True failed = True
except Exception as exc: except Exception as exc:
self._log_sync_error(username, str(exc)) self._log_sync_error(ctx.username, str(exc))
failed = True failed = True
result = MailuUserSyncResult(skipped=1, updated=updated) return mailbox_ok, failed, rotated
if rotated:
result = MailuUserSyncResult(skipped=1, updated=max(updated, 1)) @staticmethod
def _build_sync_result(
updated: int,
mailbox_ok: bool,
failed: bool,
rotated: bool,
) -> MailuUserSyncResult:
if failed: if failed:
result = MailuUserSyncResult(failures=1, updated=updated) return MailuUserSyncResult(failures=1, updated=updated)
elif mailbox_ok: if mailbox_ok:
result = MailuUserSyncResult(processed=1, updated=updated, mailboxes=1) return MailuUserSyncResult(processed=1, updated=updated, mailboxes=1)
return result if rotated:
return MailuUserSyncResult(skipped=1, updated=max(updated, 1))
return MailuUserSyncResult(skipped=1, updated=updated)
def _sync_user(self, conn: psycopg.Connection, user: dict[str, Any]) -> MailuUserSyncResult:
ctx, early = self._build_sync_context(user)
if early is not None:
return early
mailbox_ok, failed, rotated = self._ensure_mailbox_with_retry(conn, ctx)
return self._build_sync_result(ctx.updated, mailbox_ok, failed, rotated)
def _ensure_mailbox( def _ensure_mailbox(
self, self,

View File

@ -466,9 +466,16 @@ def test_mailu_sync_retries_on_password_limit(monkeypatch) -> None:
mailu_system_password="", mailu_system_password="",
) )
monkeypatch.setattr("ariadne.services.mailu.settings", dummy_settings) monkeypatch.setattr("ariadne.services.mailu.settings", dummy_settings)
monkeypatch.setattr("ariadne.services.mailu.keycloak_admin.ready", lambda: True) monkeypatch.setattr(mailu_module.keycloak_admin, "ready", lambda: True)
update_calls: list[tuple[str, dict[str, object]]] = []
monkeypatch.setattr( monkeypatch.setattr(
"ariadne.services.mailu.keycloak_admin.iter_users", mailu_module.keycloak_admin,
"update_user_safe",
lambda user_id, payload: update_calls.append((user_id, payload)),
)
monkeypatch.setattr(
mailu_module.keycloak_admin,
"iter_users",
lambda *args, **kwargs: [ lambda *args, **kwargs: [
{ {
"id": "1", "id": "1",
@ -485,7 +492,8 @@ def test_mailu_sync_retries_on_password_limit(monkeypatch) -> None:
set_calls: list[tuple[str, str, str]] = [] set_calls: list[tuple[str, str, str]] = []
monkeypatch.setattr( monkeypatch.setattr(
"ariadne.services.mailu.keycloak_admin.set_user_attribute", mailu_module.keycloak_admin,
"set_user_attribute",
lambda username, key, value: set_calls.append((username, key, value)), lambda username, key, value: set_calls.append((username, key, value)),
) )
@ -513,8 +521,11 @@ def test_mailu_sync_retries_on_password_limit(monkeypatch) -> None:
summary = svc.sync("provision", force=True) summary = svc.sync("provision", force=True)
assert summary.processed == 1 assert summary.processed == 1
assert update_calls
assert call_count["count"] == 2 assert call_count["count"] == 2
assert set_calls assert set_calls
def test_mailu_sync_skips_disabled(monkeypatch) -> None: def test_mailu_sync_skips_disabled(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace( dummy_settings = types.SimpleNamespace(
mailu_domain="bstein.dev", mailu_domain="bstein.dev",