Auto-Unsubscribe & Newsletter Digest

Overview

A cron job that scans Ben’s maildir for newsletter/marketing emails, auto-unsubscribes, moves them out of Inbox, and sends a weekly digest so he can re-subscribe to anything he actually wants.

Decisions

Environment

Architecture

Two-layer approach:

Layer 1: Server-side Sieve (delivery-time filtering)

Since ManageSieve is enabled, we can deploy sieve rules that filter at delivery time. This means newsletters get filed before they hit the Inbox — iOS Mail and mbsync both see the already-filtered state.

Sieve script capabilities:

Deploy via: sievec on server, or ManageSieve protocol client from beryllium.

Layer 2: Client-side Python script (unsubscribe + digest)

Runs on beryllium via systemd user timer.

Phase 1: Classify

For each email in Inbox/new/ and Inbox/cur/:

  1. Parse headers with email stdlib
  2. Classify as newsletter if ANY of:
    • Has List-Unsubscribe header AND sender is not on allowlist
    • Has Precedence: bulk or Precedence: list
    • Has X-Mailer matching known bulk senders (Substack, Mailchimp, SendGrid, Constant Contact, etc.)
    • From address matches known newsletter patterns (substack.com, email., no-reply@, newsletter@*)
  3. Classify as transactional (skip) if:
    • From matches known transactional senders (banks, utilities, order confirmations)
    • Subject matches patterns: “receipt”, “order confirmation”, “password reset”, “verification”
  4. Classify as personal (skip) if:
    • From address is in contacts allowlist
    • No bulk/list headers
    • Reply-To matches From (not a marketing redirect)

Phase 2: Unsubscribe

For emails classified as newsletter:

  1. Extract List-Unsubscribe header
  2. If List-Unsubscribe-Post: List-Unsubscribe=One-Click is present:
    • Send HTTP POST to the unsubscribe URL (RFC 8058 one-click)
  3. Else if URL is mailto::
    • Send an email to the unsubscribe address (via msmtp/sendmail)
  4. Else if URL is https://:
    • Send HTTP POST/GET to the URL
  5. Log: timestamp, from, subject, unsubscribe method, success/fail

Phase 3: Move

Phase 4: Weekly Digest

Once per week (Sunday 8pm ET), compile and email a digest to ben@bensima.com:

Subject: Newsletter Filter Digest — Week of Mar 10

Unsubscribed this week: 23 emails from 14 senders

NEW senders unsubscribed:
  - Oura <no-reply@m.ouraring.com> (1 email)
  - ParkWhiz <hello@email.parkwhiz.com> (1 email)
  - Glenmoor Country Club <membership@glenmoorcc.com> (1 email)

RECURRING senders unsubscribed:
  - Marketing Co <promo@example.com> (4 emails)
  ...

To keep a sender, add to allowlist:
  echo "sender@example.com" >> ~/.config/newsletter-filter/allowlist

Failed unsubscribes (manual action needed):
  - Rumble <newsletter@rumble.com> — HTTP 403

Phase 5: Allowlist/Blocklist Management

Config dir: ~/.config/newsletter-filter/

Files:

Pre-seeded allowlist:

# Newsletters Ben likes
@substack.com
pragmaticengineer
semianalysis
ainews
latentspace
latent.space
johndavidebert
johndavideb
curtisyarvin
graymir

# Mailing lists
chicken-hackers@nongnu.org
lists.tailrecursion.com
buffoverflow

# Transactional (never unsubscribe)
questdiagnostics.com
rippling.com
goodleap.com
apple.com

NOTE on Substack: @substack.com is broadly allowlisted because Ben likes Substack content generally. The classifier should treat all substack.com senders as allowlisted. Individual substack newsletters can be blocklisted if unwanted.

Scheduling

Implementation Details

Dependencies

Rate Limiting

Safety

Backlog Processing

iOS Mail Compatibility

mu Integration

mbsync Interaction

File Structure

Omni/Cloud/Mail/NewsletterFilter.py   # Main script
Omni/Cloud/Mail/newsletter-filter.sieve  # Server-side sieve rules (optional)

The script should be a single-file Python script with no external dependencies beyond stdlib + requests.

CLI:

python NewsletterFilter.py --dry-run     # Classify + report, no actions
python NewsletterFilter.py --live        # Classify + unsubscribe + move
python NewsletterFilter.py --digest      # Generate and send weekly digest
python NewsletterFilter.py --backlog     # Higher rate limit for initial cleanup

Deployment Plan

  1. Write Python script with dry-run mode
  2. Deploy sieve rules for ongoing filtering
  3. Run dry-run, send Ben the report
  4. Review allowlist with Ben
  5. Run with --live flag
  6. Set up systemd timers