diff --git a/app/main.py b/app/main.py index 6bb6b3b..f86f00d 100644 --- a/app/main.py +++ b/app/main.py @@ -417,16 +417,18 @@ async def listmonk_sync(email: str, subscribe: bool) -> None: async def listmonk_reconcile() -> dict: """Reconcile the Members newsletter list against the active membership. - The newsletter list must mirror the portal's ACTIVE members. Subscribers - who are not active members are stale (deleted accounts, never-activated - test accounts, lapsed members) and are removed: - - emails that exist in the members table but are inactive are taken off - the list (the subscriber record stays as a suppression marker); - - emails that are not members at all are taken off the list too, and the - record is KEPT. A DELETE would destroy the subscriber's consent - history, and a stray test account is indistinguishable from a real - person who subscribed outside the portal (or whose row was removed and - who may come back). Nobody is purged automatically. + The newsletter list mirrors the portal's ACTIVE members. Subscribers who are + not active members fall into two classes, and only one is acted on: + - emails that exist in the members table but are inactive, and emails that + are not members at all, are taken off the list (the subscriber record + stays as a suppression marker). A DELETE would destroy the subscriber's + consent history, and a stray test account is indistinguishable from a + real person who subscribed outside the portal, so nobody is purged + automatically; + - OPERATOR_EMAILS — infrastructure accounts, which the panel surfaces + under "admins" and excludes from member counts — are left exactly as + they are. They are neither members nor stale. The Loomio + reconciliation applies the same rule through LOOMIO_ALWAYS_KEEP. Best-effort: never raises, so a newsletter outage can't break the sweep. """ @@ -474,9 +476,14 @@ async def listmonk_reconcile() -> dict: page += 1 removed, emails = 0, [] by_reason = {"inactive member": 0, "not a member": 0} + # Operator accounts are neither members nor stale; never touch their + # subscription (LOOMIO_ALWAYS_KEEP is the same rule for the forum). + operators_kept = sum( + 1 for s in subs if (s.get("email") or "").lower() in OPERATOR_EMAILS + ) for s in subs: email = (s.get("email") or "").lower() - if not email or email in active: + if not email or email in active or email in OPERATOR_EMAILS: continue sid = s.get("id") # Both classes leave the list the same way: unsubscribe, keep the @@ -505,7 +512,12 @@ async def listmonk_reconcile() -> dict: removed, ", ".join(f"{k}: {v}" for k, v in by_reason.items() if v), ) - return {"removed": removed, "emails": emails} + if operators_kept: + logger.info( + "Listmonk reconcile: left %d operator address(es) on the list", + operators_kept, + ) + return {"removed": removed, "emails": emails, "operators_kept": operators_kept} except Exception as e: logger.warning("Listmonk reconcile error: %s", e) return {"removed": 0, "emails": [], "error": str(e)}