Overview
Melian manages multiple email accounts with full IMAP reading and SMTP sending. A tiered permission system controls autonomy per account, from fully autonomous replies to silent read-only monitoring. Accounts are polled on a configurable interval; unread messages flow through the attention pipeline for importance scoring. The dashboard surfaces any pending drafts awaiting approval.
Account Configuration
Each account is defined in config as an EmailAccountConfig. OAuth is used for Gmail; basic auth covers everything else. The check_interval field accepts cron expressions or durations like "30m".
export interface EmailAccountConfig {
name: string;
provider: "gmail" | "protonmail" | "generic";
email: string;
oauth?: {
client_id: string;
client_secret: string;
refresh_token: string | null;
};
auth?: { user: string; pass: string };
imap: { host: string; port: number; tls: boolean };
smtp?: { host: string; port: number; tls: boolean };
check_interval: string;
send_enabled?: boolean;
}Permission Tiers
export type PermissionTier = "autonomous" | "draft" | "blocked";| Tier | Behavior |
|---|---|
autonomous |
Melian composes and sends without approval |
draft |
Melian creates a pending draft; you approve or discard in the dashboard |
blocked |
Sending is rejected outright; read access only |
Cached Email Shape
Emails are stored locally in SQLite after fetching. The importance field is populated by the attention pipeline classifier.
export interface CachedEmail {
id: string;
account: string;
uid: number;
from_address: string;
from_name: string | null;
subject: string | null;
snippet: string | null;
date: number; // Unix timestamp
is_read: number; // 0 | 1
importance: number | null; // 0–3 from attention pipeline
}Draft Workflow
When Melian wants to send a reply and the recipient's tier is draft, the message is written to a pending drafts table rather than transmitted. The dashboard shows all pending drafts with approve / discard actions. On approval the message is sent via SMTP; on discard it is deleted.
Permissions are per-recipient-address, not per-account. The check(toAddress) method checks the destination address against the autonomous and blocked address lists to determine whether a given send is allowed, requires approval, or is blocked.
Compose → permission check(toAddress)
├── autonomous → send immediately
├── draft → write to drafts table → user reviews in dashboard
└── blocked → reject, log reasonTools
| Tool | Parameters | Description |
|---|---|---|
email_list_unread |
account?: string, limit?: number |
List unread messages, optionally filtered by account |
email_search |
query: string, account?: string, limit?: number |
Full-text search across cached messages |
email_read |
id: string |
Fetch and return full message body |
email_check |
- | Trigger an immediate IMAP poll on all accounts |
email_send |
to: string, subject: string, body: string, account: string, in_reply_to?: string |
Send or draft a message (respects permission tier) |
email_approve_draft |
id?: string |
Approve a pending draft and transmit it (uses most recent pending if omitted) |
email_discard_draft |
id?: string, reason?: string |
Delete a pending draft without sending (uses most recent pending if omitted) |
API Endpoints
| Method | Path | Description |
|---|---|---|
GET |
/email/accounts |
List configured accounts and their tiers |
POST |
/email/check |
Trigger an immediate poll |
GET |
/email/summary |
Unread counts and importance breakdown by account |
GET |
/email/search?q=&account= |
Search cached messages |
GET |
/email/drafts?status= |
List drafts (pending / approved / discarded) |
POST |
/email/drafts/:id/approve |
Approve and send a draft |
POST |
/email/drafts/:id/discard |
Discard a draft |
GET |
/email/:id |
Fetch a single cached message |
POST |
/email/send |
Send or draft a message via the API |