legal: permanent do-not-contact for dataspindle.com + close re-import gap
David Sgro (PA OAG complaint BCP-26-05-025816) opted out 2026-04-13; response emailed to the AG 2026-06-11. To make the suppression bulletproof and keep the response's representations true: - Added a legal do-not-contact list (DO_NOT_CONTACT_DOMAINS/_EMAILS) to _email_exclusions.py with dataspindle.com / dave@dataspindle.com; folded into BLOCKED_EMAIL_DOMAINS and is_blocked(). - listmonk_import.upsert_subscriber now refuses to import/re-confirm any suppressed address. This closes the exact gap that re-added him on 2026-04-26: the duplicate-import branch re-added an existing unsubscribed subscriber to lists with status=confirmed, overriding the opt-out.
This commit is contained in:
parent
32623d36b8
commit
773c443079
3 changed files with 56 additions and 2 deletions
20
docs/legal/AG-response-David-Sgro-SENT-NOTE.md
Normal file
20
docs/legal/AG-response-David-Sgro-SENT-NOTE.md
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
# PA AG Complaint BCP-26-05-025816 (David Sgro / dataspindle.com) — Status
|
||||||
|
|
||||||
|
- **Complaint filed:** 2026-04-13 by David Sgro (Quakertown, PA) with PA OAG
|
||||||
|
Bureau of Consumer Protection, agent Brett W. Mauser.
|
||||||
|
- **AG mediation letter:** dated 2026-05-07, 21-day response window.
|
||||||
|
- **Response sent:** emailed to the AG on 2026-06-11 (per Justin).
|
||||||
|
|
||||||
|
## Suppression status (must remain true)
|
||||||
|
- dave@dataspindle.com is `blocklisted` in the main listmonk (global, excluded
|
||||||
|
from all campaigns). Not present in the HC listmonk.
|
||||||
|
- dataspindle.com + dave@dataspindle.com added to the legal do-not-contact list
|
||||||
|
in `scripts/_email_exclusions.py` (DO_NOT_CONTACT_DOMAINS / _EMAILS).
|
||||||
|
- `listmonk_import.upsert_subscriber` now refuses to (re-)import or re-confirm
|
||||||
|
any suppressed/do-not-contact address — closes the duplicate-import gap that
|
||||||
|
re-added him to FCC lists on 2026-04-26.
|
||||||
|
|
||||||
|
## Root-cause of the re-contact
|
||||||
|
A duplicate import in `listmonk_import.py` re-added an existing (previously
|
||||||
|
unsubscribed) subscriber to lists with status="confirmed", overriding the
|
||||||
|
opt-out. Fixed by the do-not-contact gate above.
|
||||||
|
|
@ -57,10 +57,24 @@ MICROSOFT_CONSUMER_DOMAINS: frozenset[str] = frozenset({
|
||||||
"hotmail.fr", "live.co.uk", "outlook.es", "passport.com", "windowslive.com",
|
"hotmail.fr", "live.co.uk", "outlook.es", "passport.com", "windowslive.com",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# Legal / complaint do-not-contact list. Addresses and domains here must NEVER
|
||||||
|
# be cold-mailed or re-imported, independent of consumer-domain reputation
|
||||||
|
# rules. Add a domain or a specific address when someone makes a formal
|
||||||
|
# do-not-contact / opt-out demand we are honoring (e.g. a regulator complaint).
|
||||||
|
# dataspindle.com / dave@dataspindle.com -- David Sgro, PA OAG complaint
|
||||||
|
# BCP-26-05-025816; opted out 2026-04-13, permanently suppressed.
|
||||||
|
DO_NOT_CONTACT_DOMAINS: frozenset[str] = frozenset({
|
||||||
|
"dataspindle.com",
|
||||||
|
})
|
||||||
|
DO_NOT_CONTACT_EMAILS: frozenset[str] = frozenset({
|
||||||
|
"dave@dataspindle.com",
|
||||||
|
})
|
||||||
|
|
||||||
# The full set of consumer domains we refuse to cold-mail. Extend here as we
|
# The full set of consumer domains we refuse to cold-mail. Extend here as we
|
||||||
# discover other reputation-sensitive providers.
|
# discover other reputation-sensitive providers.
|
||||||
BLOCKED_EMAIL_DOMAINS: frozenset[str] = (
|
BLOCKED_EMAIL_DOMAINS: frozenset[str] = (
|
||||||
YAHOO_FAMILY_DOMAINS | GOOGLE_CONSUMER_DOMAINS | MICROSOFT_CONSUMER_DOMAINS
|
YAHOO_FAMILY_DOMAINS | GOOGLE_CONSUMER_DOMAINS | MICROSOFT_CONSUMER_DOMAINS
|
||||||
|
| DO_NOT_CONTACT_DOMAINS
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -72,4 +86,5 @@ def domain_of(email: str) -> str:
|
||||||
|
|
||||||
|
|
||||||
def is_blocked(email: str) -> bool:
|
def is_blocked(email: str) -> bool:
|
||||||
return domain_of(email) in BLOCKED_EMAIL_DOMAINS
|
e = (email or "").strip().lower()
|
||||||
|
return e in DO_NOT_CONTACT_EMAILS or domain_of(e) in BLOCKED_EMAIL_DOMAINS
|
||||||
|
|
|
||||||
|
|
@ -75,8 +75,27 @@ def api_post(path: str, payload: dict, session: requests.Session) -> Optional[di
|
||||||
def upsert_subscriber(email: str, name: str, lists: list[int],
|
def upsert_subscriber(email: str, name: str, lists: list[int],
|
||||||
attribs: dict, session: requests.Session) -> Optional[int]:
|
attribs: dict, session: requests.Session) -> Optional[int]:
|
||||||
"""Create or update a Listmonk subscriber. Returns subscriber ID or None."""
|
"""Create or update a Listmonk subscriber. Returns subscriber ID or None."""
|
||||||
|
clean = (email or "").lower().strip()
|
||||||
|
|
||||||
|
# Do-not-contact / legal suppression gate. NEVER (re-)import or re-confirm a
|
||||||
|
# suppressed address. This is the guard that was missing when a duplicate
|
||||||
|
# import re-added a previously-unsubscribed contact to lists with
|
||||||
|
# status="confirmed" (the David Sgro / dataspindle.com case). is_blocked
|
||||||
|
# covers both consumer-domain reputation blocks and the legal do-not-contact
|
||||||
|
# list in scripts/_email_exclusions.py.
|
||||||
|
try:
|
||||||
|
from scripts._email_exclusions import is_blocked
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
from _email_exclusions import is_blocked # type: ignore
|
||||||
|
except Exception:
|
||||||
|
is_blocked = lambda _e: False # noqa: E731 (fail-open only if module missing)
|
||||||
|
if is_blocked(clean):
|
||||||
|
LOG.info("Skipping suppressed/do-not-contact address: %s", clean)
|
||||||
|
return None
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
"email": email.lower().strip(),
|
"email": clean,
|
||||||
"name": (name or "").strip() or email.split("@")[0],
|
"name": (name or "").strip() or email.split("@")[0],
|
||||||
"status": "enabled",
|
"status": "enabled",
|
||||||
"lists": lists,
|
"lists": lists,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue