# Inbox Triage — stop reading your inbox, let the AIOS read it for you

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

```python
# 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_human` plus 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 `intent` and `urgency` enums to your world.
- Add a sender allow or deny list before the model pass to save tokens on newsletters.
- Pipe `queue_draft` into 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.
