← all builds

Inbox Triage Skill

emailtriagedrafts
Download .md

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

Adapt it

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.