A sanitised reference implementation. Drop it into Claude Code, point it at your own mailbox, adapt the prompts. Generic on purpose: no provider lock in, no real credentials, no business specifics.
The idea
You do not need to read your inbox. You need to act on the three messages in it that actually require you. Everything else is either noise or a reply you would write the same way every time.
This layer reads every unread message, classifies it, drafts a reply in your voice, and surfaces only the ones that need a human decision. Nothing is sent automatically. You review and send. The win is that you stop reading the inbox, not that the machine speaks for you unattended.
IMAP unread ──> [classify: intent + urgency] ──> [draft reply in your voice]
│ │
needs a human │ │ routine
v v
surface to you (chat/push) ready to send draft (queued)
Minimal reference implementation
# inbox_triage.py — reference only. Swap in your own mailbox creds + LLM client.
import imaplib, email, json
from email.header import decode_header
# --- 1. pull unread (any IMAP mailbox; use an app password, never your main one) ---
def fetch_unread(host, user, password, limit=25):
M = imaplib.IMAP4_SSL(host)
M.login(user, password)
M.select("INBOX")
_, data = M.search(None, "UNSEEN")
ids = data[0].split()[:limit]
out = []
for i in ids:
_, msg_data = M.fetch(i, "(BODY.PEEK[])") # PEEK = do not mark as read
msg = email.message_from_bytes(msg_data[0][1])
out.append({
"id": i.decode(),
"from": str(msg.get("From")),
"subject": _dec(msg.get("Subject")),
"body": _plain_text(msg)[:4000],
})
M.logout()
return out
# --- 2. classify + draft in one model pass ---
TRIAGE_PROMPT = """You triage an inbox owner's mail. For each message return JSON:
{ "intent": "sales|support|admin|personal|newsletter|spam",
"urgency": "now|today|whenever|none",
"needs_human": true|false, // true only if a real decision is required
"draft_reply": "<a reply in the owner's voice, or empty if none needed>" }
Owner voice: short, warm, direct. Never send money, commit to dates, or share private
data without a human. When unsure, set needs_human true and leave the draft empty."""
def triage(msg, llm_json):
return llm_json(system=TRIAGE_PROMPT, user=json.dumps(msg)) # your model call
# --- 3. route ---
def run(messages, llm_json, notify, queue_draft):
for m in messages:
t = triage(m, llm_json)
if t["needs_human"] or t["urgency"] in ("now", "today"):
notify(m, t) # surface to you (chat / push)
if t["draft_reply"]:
queue_draft(m, t["draft_reply"]) # save as a draft; never auto send
# everything else: left unread safe, handled, or ignored as noise
def _dec(s):
if not s: return ""
parts = decode_header(s)
return "".join(p.decode(enc or "utf-8", "ignore") if isinstance(p, bytes) else p
for p, enc in parts)
def _plain_text(msg):
if msg.is_multipart():
for part in msg.walk():
if part.get_content_type() == "text/plain":
return part.get_payload(decode=True).decode("utf-8", "ignore")
return ""
return (msg.get_payload(decode=True) or b"").decode("utf-8", "ignore")
Why it works
- PEEK, do not mark read.
BODY.PEEK[]leaves messages unread so the human still owns the mailbox state. The triage is a read layer, not a takeover. - One pass does classify and draft. Cheaper and the draft is grounded in the same read the classifier used.
- Surface the few, queue the rest. You only ever look at
needs_humanplus anything urgent. The routine replies wait as drafts you approve in one tap. - Never auto send. The model proposes, you dispose. In anything with money, dates, or
private data, the prompt forces
needs_human.
Adapt it
- Tune the
intentandurgencyenums to your world. - Add a sender allow or deny list before the model pass to save tokens on newsletters.
- Pipe
queue_draftinto wherever you actually reply from (mail client drafts, a review queue, a chat approval flow). - For a daily rhythm, run it on a schedule and push a one line summary: how many handled, how many waiting on you.
Built for my own group, runs every day; this is the generalised version. Take it, point it at your own inbox, tell me what you would change.