newsletter: operator accounts are not stale subscribers

info@inference.coop is on the Members list but has no members row, so the
reconcile classified the co-op's own inbox as a stale subscriber and would have
unsubscribed it.

The data model already distinguishes this: OPERATOR_EMAILS (default
info@inference.coop,bot@inference.coop) are infrastructure accounts — the SSO
owner and the bot — which the panel surfaces under "admins" and excludes from
member counts. They live outside the members table by design. The Loomio
reconciliation applies the same rule to the same accounts via LOOMIO_ALWAYS_KEEP
("must always remain in the group").

So operators become the third class here: neither members nor stale. Their
subscription is left exactly as it is (never unsubscribed, and the reconcile
never subscribes, so an operator who unsubscribes stays unsubscribed). The
response reports how many were left, and the log line carries counts only.
This commit is contained in:
Ubuntu committed 2026-10-26 16:54:53 +00:00
1 parent 250ca6e8f0
commit e55d8a3f1e
1 file changed
+24 -12
+24 -12
View File
@@ -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)}