mail-agent/SPEC.md

9.8 KiB

Mail Agent Specification

Overview

IMAP-based email triage agent with multi-tier escalation. Runs as a service, processes incoming mail, auto-handles obvious cases, escalates uncertain ones.

Architecture

New Mail (IMAP IDLE push)
    │
    ▼
┌─────────────────────────┐
│  L1: Cheap Model        │  Fireworks llama-v3p1-8b-instruct
│  - Spam → delete        │
│  - Newsletter → archive │
│  - Receipt → archive    │
│  - Obvious junk → gone  │
│  - Uncertain → L2       │
└─────────────────────────┘
    │
    ▼
┌─────────────────────────┐
│  L2: James (Opus)       │  via Gateway
│  - Review context       │
│  - Draft reply?         │
│  - Handle or escalate   │
└─────────────────────────┘
    │
    ▼
┌─────────────────────────┐
│  L3: Johan              │  Signal notification
│  - Important stuff      │
│  - Needs human decision │
└─────────────────────────┘

Accounts (Multi-account support)

First account:

  • ID: proton
  • Host: 127.0.0.1
  • IMAP Port: 1143
  • Username: tj@jongsma.me
  • Password: BlcMCKtNDfqv0cq1LmGR9g
  • TLS: STARTTLS

REST API

Accounts

GET    /accounts                     # List all configured accounts
POST   /accounts                     # Add account
DELETE /accounts/{id}                # Remove account

Mailboxes

GET    /accounts/{id}/mailboxes      # List folders

Messages

GET    /accounts/{id}/messages
       ?folder=INBOX
       &unread=true
       &from=sender@example.com      # Search by sender
       &limit=50
       &offset=0

GET    /accounts/{id}/messages/{uid}?folder=INBOX
       # Returns full message with body, attachments list

PATCH  /accounts/{id}/messages/{uid}?folder=INBOX
       Body: {"seen": true}          # Mark as read
       Body: {"seen": false}         # Mark as unread  
       Body: {"folder": "Archive"}   # Move to folder

DELETE /accounts/{id}/messages/{uid}?folder=INBOX
       # Delete message

Actions

POST   /accounts/{id}/unsubscribe/{uid}?folder=INBOX
       # Find unsubscribe link in email, execute it

Push Events (IMAP IDLE)

GET    /accounts/{id}/events?folder=INBOX
       # SSE stream, emits on new mail:
       # data: {"type": "new", "uid": 123, "from": "...", "subject": "..."}

Triage Pipeline

L1 Triage (Cheap Model)

  • Model: Fireworks accounts/fireworks/models/llama-v3p1-8b-instruct
  • Cost: ~$0.20/1M tokens (very cheap)

L1 Prompt:

Classify this email. Respond with JSON only.

From: {from}
Subject: {subject}
Preview: {first 500 chars}

Categories:
- spam: Obvious spam, phishing, scams
- newsletter: Marketing, newsletters, promotions
- receipt: Order confirmations, invoices (not shipping)
- shipping: Shipping/delivery updates (picked up, in transit, delivered)
- notification: Automated notifications (GitHub, services)
- personal: From a real person, needs attention
- important: Urgent, financial, legal, medical
- uncertain: Not sure, needs human review

For shipping emails, also extract:
- carrier: UPS, FedEx, USPS, DHL, etc.
- status: ordered, picked_up, in_transit, out_for_delivery, delivered
- item: Brief description of what's being shipped
- expected_date: Expected delivery date (if available)

Response format:
{"category": "...", "confidence": 0.0-1.0, "reason": "brief reason"}

For shipping:
{"category": "shipping", "confidence": 0.9, "reason": "...", 
 "shipping": {"carrier": "UPS", "status": "picked_up", "item": "E3-1275 Server", "expected_date": "2026-02-03"}}

L1 Actions:

Category Confidence > 0.8 Confidence < 0.8
spam Delete → L2
newsletter Archive → L2
receipt Archive + label → L2
shipping → Dashboard + Archive → L2
notification Archive → L2
personal → L2 → L2
important → L2 + flag → L2 + flag
uncertain → L2 → L2

Shipping Dashboard Integration

Dashboard API: http://100.123.216.65:9200 (James Dashboard)

When shipping email detected:

  1. POST to /api/news with shipping update
  2. Archive the email
  3. Track status in local state

Dashboard payload:

POST /api/news
{
  "title": "📦 E3-1275 Server",
  "body": "Picked up by UPS. Expected Feb 3rd.",
  "type": "info",
  "source": "shipping",
  "url": null
}

Status progression:

  • ordered → "Order confirmed"
  • picked_up → "Picked up by {carrier}"
  • in_transit → "In transit"
  • out_for_delivery → "Out for delivery"
  • delivered → "Delivered ✓"

Auto-cleanup:

  • When status = delivered, set a flag
  • Next day, DELETE the news item from dashboard
  • Track shipments in local JSON: ~/.config/mail-agent/shipments.json

L2 Triage (James/Opus)

  • Receives escalated emails via Gateway hook
  • Full context review
  • Can: archive, delete, draft reply, escalate to Johan

L3 Escalation (Johan)

  • Uses existing Clawdbot Signal integration (no hardcoded number needed)
  • POST to gateway, let it route to Johan via Signal
  • Summary of what needs attention
  • Only for actually important stuff

Escalation via Gateway:

POST to gateway → routes to Johan's Signal via existing channel config

Configuration

# config.yaml
server:
  host: 127.0.0.1
  port: 8025

accounts:
  proton:
    host: 127.0.0.1
    port: 1143
    username: tj@jongsma.me
    password: BlcMCKtNDfqv0cq1LmGR9g
    tls: starttls
    folders:
      watch: [INBOX]
      archive: Archive
      spam: Spam

triage:
  enabled: true
  l1:
    provider: fireworks
    model: accounts/fireworks/models/llama-v3p1-8b-instruct
    api_key: ${FIREWORKS_API_KEY}
  l2:
    gateway_url: http://localhost:18080  # or however gateway is reached
    # Hook mechanism TBD
  l3:
    gateway_url: http://localhost:18080  # Uses existing Signal integration
    
  shipping:
    dashboard_url: http://100.123.216.65:9200
    auto_cleanup_days: 1  # Remove from dashboard 1 day after delivered

  rules:
    always_escalate_from:
      - "*@inou.com"
      - "*@kaseya.com"
    auto_archive_from:
      - "*@github.com"
      - "noreply@*"
    auto_delete_from:
      - known-spam-domains.txt

Tech Stack

  • Language: Python 3.11+
  • Framework: FastAPI
  • IMAP: imapclient (IDLE support)
  • Async: asyncio + anyio
  • LLM: httpx for Fireworks API

Files Structure

mail-agent/
├── SPEC.md              # This file
├── README.md            # Usage docs
├── config.yaml          # Configuration
├── requirements.txt     # Dependencies
├── src/
│   ├── __init__.py
│   ├── main.py          # FastAPI app entry
│   ├── config.py        # Config loading
│   ├── models.py        # Pydantic models
│   ├── imap/
│   │   ├── __init__.py
│   │   ├── client.py    # IMAP connection
│   │   ├── idle.py      # IDLE push handler
│   │   └── parser.py    # Email parsing
│   ├── api/
│   │   ├── __init__.py
│   │   ├── accounts.py  # Account endpoints
│   │   ├── messages.py  # Message endpoints
│   │   └── events.py    # SSE endpoint
│   ├── triage/
│   │   ├── __init__.py
│   │   ├── l1.py        # L1 cheap model triage
│   │   ├── l2.py        # L2 escalation to James
│   │   └── l3.py        # L3 escalation to Johan
│   └── actions/
│       ├── __init__.py
│       └── unsubscribe.py  # Unsubscribe handler
├── systemd/
│   └── mail-agent.service
└── tests/
    └── ...

Systemd Service

[Unit]
Description=Mail Agent
After=network.target protonmail-bridge.service

[Service]
Type=simple
User=johan
WorkingDirectory=/home/johan/dev/mail-agent
ExecStart=/home/johan/dev/mail-agent/.venv/bin/python -m src.main
Restart=always
RestartSec=10
Environment=FIREWORKS_API_KEY=...

[Install]
WantedBy=default.target

Environment Variables

  • FIREWORKS_API_KEY — For L1 model
  • MAIL_AGENT_CONFIG — Path to config.yaml (default: ./config.yaml)

Shipment Tracking State

Stored in ~/.config/mail-agent/shipments.json:

{
  "shipments": [
    {
      "id": "ups-1234567890",
      "carrier": "UPS",
      "item": "E3-1275 Server",
      "status": "picked_up",
      "expected_date": "2026-02-03",
      "dashboard_news_id": "abc123",
      "last_updated": "2026-01-30T22:00:00Z",
      "delivered_at": null
    }
  ]
}
  • On new shipping email: upsert shipment, update dashboard
  • On delivered: set delivered_at, schedule cleanup
  • Cleanup job: delete from dashboard after 1 day

First Account (Pre-configured)

The Proton Bridge account is already running:

  • Service: systemctl --user status protonmail-bridge
  • IMAP: 127.0.0.1:1143
  • Account: tj@jongsma.me
  • Bridge password: BlcMCKtNDfqv0cq1LmGR9g

Open Questions

  1. Gateway hook mechanism: How does L2/L3 escalation reach James/Johan? POST to gateway? Will check gateway code for webhook/hook endpoint.
  2. Johan's Signal number: RESOLVED: Use existing Clawdbot Signal integration via gateway
  3. Fireworks API key: RESOLVED: Available in env

Build Checklist

  • Project scaffold
  • Config loading
  • IMAP client with IDLE
  • REST API endpoints
  • L1 triage with Fireworks
  • Shipping detection + dashboard integration
  • Shipment tracking state + auto-cleanup
  • L2 escalation hook (gateway)
  • L3 escalation via gateway → Signal
  • Systemd service
  • Test with Proton account
  • README documentation