354 lines
9.8 KiB
Markdown
354 lines
9.8 KiB
Markdown
# 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:**
|
|
```json
|
|
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
|
|
|
|
```yaml
|
|
# 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
|
|
```ini
|
|
[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`:
|
|
```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
|