Blacklist & Blocklist¶
Files: app/core/models/blacklist.py (legacy), app/core/models/organization_blocklist.py (new)
Overview¶
Transitioning from a legacy Blacklist system (recipient status = Blacklisted) to a dedicated Blocklist system (separate blocklist_entries table).
Blacklist (Legacy -- deprecated)¶
Simple dataclass with addresses and emails dicts for in-memory lookup. Marked DEPRECATED[ticket:PR-1612]. Being replaced by BlocklistLookup.
BlocklistEntry (New)¶
| Field | Description |
|---|---|
id |
Primary key |
organization_id |
Owning organization |
email |
Lowercased email; empty string for address-only entries |
address_hash |
Normalized {address1}{address2}{zip}; empty string for email-only entries |
address_raw |
JSON of original address; NULL for email-only entries |
reason |
Why blocked |
created_at, updated_at |
Timestamps |
At least one of email or address_hash must be non-empty (enforced by all callers, not by a DB constraint). The unique constraint (organization_id, email, address_hash) prevents exact duplicates but permits the same email to appear once email-only and once paired with an address.
BlocklistLookup: In-memory structure with emails and addresses dicts for O(1) lookups during recipient sync. Entries with empty email are excluded from the emails dict; entries with empty address_hash are excluded from the addresses dict (so an email-only entry cannot falsely match a recipient with an empty parsed address).
Direct DB lookups (is_email_blocked, is_address_blocked, get_all_blocked_for_recipient): empty inputs short-circuit so they cannot mass-match email-only or address-only entries. get_all_blocked_for_recipient with both inputs empty returns an empty list without querying.
Hybrid approach (during transition)¶
recipient_sync.py fetches BlocklistLookup from new table AND falls back to legacy campaign_recipients with Blacklisted status. Both combined into a single lookup for unified filtering.
API (routes/organization_blocklist_routes.py)¶
- POST
/blocklist-- Create entry from{email?, address?, reason?}. At least one ofemailoraddressmust be provided. Whenaddressis provided it must includeaddress1andzip. Nullifies existing attributions for the email when supplied. - GET
/blocklist-- List with pagination/filtering - DELETE
/blocklist/{id}-- Remove entry - GET
/blocklist/check-- Check if email/address is blocked - POST
/blocklist/upload-- Bulk CSV upload. The CSV must contain either anemailcolumn or all of{address_1, postal_code, country}(or both). Each row is validated individually: a row needs an email OR a full address, otherwise it is counted asskipped_missing_data. Rows with email-only insert withaddress_hash = "".
Key files¶
- Legacy:
core/models/blacklist.py - New:
core/models/organization_blocklist.py - Migration:
scripts/migrate_blacklist_to_blocklist.py - Integration:
methods/campaigns/recipient_sync.py