Skip to content
Oswaldo Rodríguez

← All projects

2026Solo developer — CLI, classifier and OpenCode skill

mail-triage — Junk mail triage CLI

A local CLI that sorts your junk mail into delete, rescue or review — with reasons.

  • Python
  • Microsoft Graph
  • SQLite
  • OpenCode
  • Keychain
  • Cloudflare
  • Email
  • Junk Mail

Problem

My junk mail folder fills with two very different populations: scams impersonating banks and Microsoft, and legitimate promotions I actually subscribed to. Deleting everything risks losing the receipt I was waiting for; ignoring the folder means the real phishing hides among the discount codes.

Provider-side spam filters give a binary answer and no reason, and cloud mail cleaners want full mailbox access. I wanted a local tool that classifies each message, tells me why, and never moves anything without my confirmation.

Approach

Local-first, no cloud services

Everything runs on Python's standard library plus SQLite and macOS Keychain, storing decisions and learning from my corrections on disk. Nothing leaves the machine.

I rejected cloud mail-cleaning services: they require root access to the mailbox over OAuth or password, and the data they see is more sensitive than the problem warrants.

A classifier with explicit reasons

Every message gets a 0–1 risk score and a human-readable reason: suspected impersonation, a reply-to different from the sender, a brand-new domain, SPF/DKIM failures, urgency cues. Legitimate mail rescued to the inbox prints why it was rescued.

I rejected a black-box model: without reasons I cannot audit false positives, and one wrong auto-delete teaches you never to trust the tool again.

Human-in-the-loop, learning from corrections

Nothing is deleted or moved without an explicit confirmation, and `undo` reverses the last batch. Corrections feed allowed/blocked domain lists; a domain gets an automatic rule only after three matching explicit corrections.

I rejected automatic deletion: the cost of a mistake (losing a receipt, an appointment, a contract) is asymmetric, so the design keeps a human as the final gate.

Provider-agnostic core, one backend today

The engine talks to a `MailProvider` interface; only Outlook (Microsoft Graph, device-code flow) is implemented today, with Gmail and generic IMAP ready to plug in without touching the classifier. `scan`, `purge`, `undo` and `feedback` all accept `--provider`.

I rejected hard-wiring Graph into the classifier: the triage logic should not change when the transport does.

Results

  • Open-sourced with CI tests (GitHub Actions) and an OpenCode skill wrapping the CLI for interactive use.
  • Every triage run produces an auditable report: score and reason per message, and full undo for any batch.