Auto-Block Repeat Offenders: Building a Contact Management Bot
Your contact list grows with every email. Most contacts are fine — legitimate senders, services you signed up for, colleagues. But some are spam sources that keep sending even after their emails are rejected by your spam filter. They accumulate "unset" contacts with high email counts and zero value.
In this guide, we build a Python bot that reviews your contacts, blocks repeat spam offenders, and optionally whitelists trusted senders — all through the Cleanbox API.
The strategy
| Contact profile | Action | Logic |
|---|---|---|
| Unset, 50+ emails, mostly spam | Block | High volume + never interacted = spam |
| Unset, from known junk TLDs | Block | .xyz, .top, .click, .buzz domains |
| Unset, Finance or Business category, 10+ emails | Whitelist | Important category + consistent sending = trusted |
| Unset, Shopping category, has subscriptions | Keep unset | May want to unsubscribe, not block |
Step 1: Fetch all unset contacts
import requests
import re
API_BASE = "https://api.cleanbox.app/v1"
API_KEY = "your_api_key_here"
HEADERS = {"Authorization": f"Bearer {API_KEY}"}
def get(path, params=None):
r = requests.get(f"{API_BASE}{path}", headers=HEADERS, params=params)
r.raise_for_status()
return r.json()
def put(path, data):
r = requests.put(f"{API_BASE}{path}", headers=HEADERS, json=data)
r.raise_for_status()
return r.json()
def get_all_unset_contacts():
"""Fetch all contacts with state 'unset', paginated."""
contacts = []
page = 1
while True:
data = get("/contacts", {
"state": "unset",
"sort": "emailCount",
"dir": "desc",
"page": page,
"per_page": 100
})
contacts.extend(data["data"])
if page >= data["total_pages"]:
break
page += 1
return contacts
We use GET /v1/contacts?state=unset&sort=emailCount&dir=desc to get all unset contacts sorted by email volume. The highest-volume contacts are reviewed first.
Step 2: Define the rules
JUNK_TLDS = {".xyz", ".top", ".click", ".buzz", ".icu", ".club", ".loan", ".work"}
TRUSTED_CATEGORIES = {"Finance", "Business Services", "Productivity & Tools"}
BLOCK_THRESHOLD = 50 # emails from unset contact
WHITELIST_THRESHOLD = 10 # emails from trusted category
def extract_tld(email):
"""Extract TLD from email address."""
domain = email.split("@")[1] if "@" in email else ""
parts = domain.rsplit(".", 1)
return f".{parts[-1]}" if len(parts) > 1 else ""
def classify_contact(contact):
"""Determine what action to take for a contact."""
email = contact["email"]
count = contact["email_count"]
category = contact.get("category", {})
category_name = category.get("name", "") if category else ""
tld = extract_tld(email)
# Rule 1: Block junk TLDs
if tld in JUNK_TLDS:
return "block", f"Junk TLD: {tld}"
# Rule 2: Block high-volume unset contacts
if count >= BLOCK_THRESHOLD:
return "block", f"High volume: {count} emails, never interacted"
# Rule 3: Whitelist trusted categories with consistent sending
if category_name in TRUSTED_CATEGORIES and count >= WHITELIST_THRESHOLD:
return "whitelist", f"Trusted category: {category_name}, {count} emails"
# Default: no action
return None, None
Step 3: Apply actions
def apply_actions(contacts, dry_run=True):
"""Review contacts and apply block/whitelist actions."""
stats = {"blocked": 0, "whitelisted": 0, "skipped": 0}
for contact in contacts:
action, reason = classify_contact(contact)
if action == "block":
print(f" BLOCK {contact['email']} - {reason}")
if not dry_run:
put(f"/contacts/{contact['uuid']}", {"state": "blocked"})
stats["blocked"] += 1
elif action == "whitelist":
print(f" TRUST {contact['email']} - {reason}")
if not dry_run:
put(f"/contacts/{contact['uuid']}", {"state": "whitelisted"})
stats["whitelisted"] += 1
else:
stats["skipped"] += 1
return stats
The PUT /v1/contacts/{uuid} endpoint updates the contact state. Setting it to "blocked" means all future email from this sender is rejected. Setting it to "whitelisted" means their email bypasses spam checks and Shield rules.
Step 4: Run with dry-run safety
import sys
def main():
dry_run = "--apply" not in sys.argv
if dry_run:
print("DRY RUN - no changes will be made (use --apply to execute)")
else:
print("LIVE RUN - changes will be applied")
print("
Fetching unset contacts...")
contacts = get_all_unset_contacts()
print(f"Found {len(contacts)} unset contacts
")
stats = apply_actions(contacts, dry_run=dry_run)
print(f"
Summary:")
print(f" Blocked: {stats['blocked']}")
print(f" Whitelisted: {stats['whitelisted']}")
print(f" Skipped: {stats['skipped']}")
if dry_run:
print("
Run with --apply to execute these changes")
if __name__ == "__main__":
main()
Usage
# Preview what would happen (safe)
python3 contact_bot.py
# Actually apply changes
python3 contact_bot.py --apply
# Schedule weekly cleanup
0 6 * * 1 cd /path/to/script && python3 contact_bot.py --apply >> /var/log/contact_bot.log 2>&1
Safety considerations
- Always dry-run first. Review the output before using
--apply. Blocking a legitimate sender is hard to undo (the contact state can be changed back, but bounced emails during that time are lost). - Start with high thresholds. Set
BLOCK_THRESHOLDhigh (100+) initially and lower it over time as you gain confidence in the rules. - Exclude recent contacts. Add a check for
created_at— do not block contacts less than 7 days old, as they may not have had time to establish a pattern. - Log everything. Keep a log of every action taken so you can audit and reverse mistakes.
Extending the bot
- Mute instead of block: For contacts that are noisy but not spam (social media notifications), use state
mutedinstead ofblocked. Their email is delivered but marked as read. - Category-based rules: Use
GET /v1/categoriesto fetch all 20 categories and create nuanced rules per category. - Combine with messages data: Fetch recent messages per contact to check spam scores before deciding. A contact with many emails but consistently low spam scores is probably legitimate.