email-cli 0.6.1

Agent-friendly email CLI for Resend
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
<div align="center">

# Email CLI

**Send, receive, and manage email from your terminal. Built for AI agents.**

<br />

[![Star this repo](https://img.shields.io/github/stars/199-biotechnologies/email-cli?style=for-the-badge&logo=github&label=%E2%AD%90%20Star%20this%20repo&color=yellow)](https://github.com/199-biotechnologies/email-cli/stargazers)
&nbsp;&nbsp;
[![Follow @longevityboris](https://img.shields.io/badge/Follow_%40longevityboris-000000?style=for-the-badge&logo=x&logoColor=white)](https://x.com/longevityboris)

<br />

[![Crates.io](https://img.shields.io/crates/v/email-cli?style=for-the-badge&logo=rust&logoColor=white&label=crates.io)](https://crates.io/crates/email-cli)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue?style=for-the-badge)](LICENSE)
[![Rust](https://img.shields.io/badge/Rust-1.85+-orange?style=for-the-badge&logo=rust&logoColor=white)](https://www.rust-lang.org/)
[![Homebrew](https://img.shields.io/badge/Homebrew-tap-FBB040?style=for-the-badge&logo=homebrew&logoColor=white)](https://github.com/199-biotechnologies/homebrew-tap)

---

A single binary that gives your AI agent a real email address. Send, receive, reply, draft, sync, and manage contacts, broadcasts, segments, and topics through Resend -- all from the command line. No IMAP. No browser inbox. No MCP server. Just a local SQLite mailbox and 70+ commands.

[Why This Exists]#why-this-exists | [Install]#install | [How It Works]#how-it-works | [Commands]#commands | [Mailing-list Workflows]#mailing-list-workflows | [Configuration]#configuration | [Contributing]#contributing

</div>

## Why This Exists

AI agents need to send and receive email. The existing options are bad:

- **IMAP/SMTP** requires complex server configuration, credential management, and connection handling. Agents struggle with it.
- **Email APIs** work for sending, but agents need a local mailbox to track threads, drafts, and read status.
- **Browser-based inboxes** are not scriptable. Agents cannot use them.

Email CLI solves this by wrapping the [Resend API](https://resend.com) in a local-first CLI. Your agent gets a verified email address, a local SQLite mailbox, and structured JSON output with semantic exit codes. It calls `agent-info` once to learn every command, then works from memory.

You can also use it as a human. It works the same way.

## Install

### One-line install (pre-built binary, no Rust required)

```bash
curl -fsSL https://raw.githubusercontent.com/199-biotechnologies/email-cli/main/install.sh | sh
```

### Homebrew

```bash
brew tap 199-biotechnologies/tap
brew install email-cli
```

### Cargo

```bash
cargo install email-cli
```

### Update

```bash
email-cli update           # self-update from GitHub Releases
email-cli update --check   # check without installing
```

## Quick Start

```bash
# 1. Add your Resend API key
email-cli profile add default --api-key-env RESEND_API_KEY

# 2. Verify domain connectivity
email-cli profile test default

# 3. Register a sending identity
email-cli account add agent@yourdomain.com \
  --profile default \
  --name "Agent" \
  --default

# 4. Send an email
email-cli send \
  --to someone@example.com \
  --subject "Hello from email-cli" \
  --text "Sent from the terminal."

# 5. Sync and read incoming mail
email-cli sync
email-cli inbox ls
email-cli inbox read 1 --mark-read

# 6. Reply (threads correctly with In-Reply-To + References)
email-cli reply 1 --text "Got it, thanks."

# 7. Reply All (preserves CC recipients)
email-cli reply 1 --all --text "Thanks everyone."

# 8. Forward
email-cli forward 1 --to colleague@example.com --text "FYI"

# 9. Thread a new send into an existing conversation
email-cli send --reply-to-msg 1 --to someone@example.com --text "Following up"
```

## How It Works

Three concepts:

1. **Profile** -- a Resend API key. You can have multiple profiles for different Resend accounts.
2. **Account** -- a sender/receiver identity (`agent@yourdomain.com`). Each account belongs to a profile.
3. **Local mailbox** -- a SQLite database that stores messages, drafts, attachments, and sync cursors.

Resend handles delivery. Email CLI handles the local operating model that agents need: read tracking, threading, drafts, batch sends, and structured output.

```
┌────────────────────────────────┐
│         Your Agent / You       │
│    (Claude, Codex, Gemini)     │
└──────────────┬─────────────────┘
               │  CLI commands
               ▼
┌────────────────────────────────┐
│          email-cli             │
│   50+ commands, JSON output,   │
│   semantic exit codes          │
└──────────┬─────────┬───────────┘
           │         │
     ┌─────▼──┐  ┌───▼────────┐
     │ SQLite │  │ Resend API │
     │ local  │  │  (send,    │
     │ store  │  │  receive,  │
     │        │  │  domains)  │
     └────────┘  └────────────┘
```

## Commands

### Core Email

| Command | What it does |
|---|---|
| `send` | Send email (--reply-to-msg for threading, --cc, --bcc, --attach) |
| `reply <id>` | Reply with proper threading headers (--all for Reply All) |
| `forward <id>` | Forward a message (--to, --cc, --text for preamble) |
| `sync` | Pull sent and received messages into local store |
| `inbox ls` | List messages (filter by account, unread status) |
| `inbox read <id>` | Read a message, optionally mark as read |
| `inbox search <q>` | Search messages by keyword |
| `inbox delete <id>` | Delete a message |
| `inbox archive <id>` | Archive a message |
| `draft create` | Save a local draft with attachment snapshots |
| `draft list` | List all drafts |
| `draft show <id>` | View draft details |
| `draft edit <id>` | Edit a draft |
| `draft send <id>` | Send a draft and delete it |
| `draft delete <id>` | Delete a draft |
| `attachments list <id>` | List attachments on a message |
| `attachments get <id> <att>` | Download an attachment to disk |

### Account Management

| Command | What it does |
|---|---|
| `profile add <name>` | Add or update a Resend API profile |
| `profile list` | List all profiles |
| `profile test <name>` | Test API connectivity |
| `account add <email>` | Register an email identity |
| `account list` | List all accounts |
| `account use <email>` | Set the default sending account |
| `signature set <account>` | Set per-account signature |
| `signature show <account>` | Show signature |

### Resend Management

| Command | What it does |
|---|---|
| `domain list` | List your Resend domains |
| `domain get <id>` | Get domain details with DNS records |
| `domain create --name <d>` | Register a new domain |
| `domain verify <id>` | Trigger domain verification |
| `domain delete <id>` | Delete a domain |
| `domain update <id>` | Update tracking settings |
| `batch send --file <path>` | Send batch emails from a JSON file |
| `email list` | List sent emails via Resend GET /emails (each row includes `last_event` for polling) |
| `api-key list` | List API keys |
| `api-key create --name <n>` | Create an API key |
| `api-key delete <id>` | Delete an API key |

### The Audience Section

In the Resend dashboard, "Audience" is the section name in the left sidebar. It groups four primitives — **Contacts**, **Properties**, **Segments**, and **Topics** — each with its own CLI command in email-cli:

| Resend dashboard tab | email-cli command | Purpose |
|---|---|---|
| Contacts | `contact` | Individual email addresses |
| Properties | `contact-property` | Typed custom-field schemas (e.g. `company`, `plan`) |
| Segments | `segment` | Groupings of contacts (a contact can be in multiple segments) |
| Topics | `topic` | User-facing subscription preferences for broadcasts |

> The pre-November-2025 "Audiences as a grouping primitive" model has been replaced. Contacts are now flat — each contact can belong to zero, one, or multiple segments. There is no `audience` noun in this CLI; use `segment` instead.

### Contacts

| Command | What it does |
|---|---|
| `contact list` | List contacts (`--limit` 1-100, `--after <id>` for cursor pagination) |
| `contact get <id_or_email>` | Get a contact by id or email (surfaces custom properties) |
| `contact create --email <addr>` | Create a contact (`--first-name`, `--last-name`, `--unsubscribed`, `--properties '{"k":"v"}'`, `--segments seg1,seg2`, `--topics top_xxx:opt_in,top_yyy:opt_out`) |
| `contact update <id_or_email>` | Update contact fields (`--first-name`, `--last-name`, `--unsubscribed`, `--properties`) |
| `contact delete <id_or_email>` | Delete a contact |

### Segments

| Command | What it does |
|---|---|
| `segment list` | List segments |
| `segment get <id>` | Get segment details |
| `segment create --name <n>` | Create a segment |
| `segment delete <id>` | Delete a segment |
| `segment contacts <id>` | List the contacts in a segment |
| `segment contact-add --contact <id_or_email> --segment <id>` | Add a contact to a segment |
| `segment contact-remove --contact <id_or_email> --segment <id>` | Remove a contact from a segment |
| `segment contact-list --contact <id_or_email>` | List the segments a contact belongs to |

### Custom Contact Properties

Resend's contact properties are typed key/value fields you attach to contacts. Use them for merge tags in broadcasts and templates. **You must define a property's schema before assigning values to a contact.**

| Command | What it does |
|---|---|
| `contact-property list` | List defined contact-property schemas |
| `contact-property get <id>` | Get a contact-property schema |
| `contact-property create --key company --property-type string` | Define a property (`--fallback` for default value, `--property-type` `string`/`number`) |
| `contact-property update <id> --fallback "N/A"` | Update a property's fallback value (add `--as-number` if the property is numeric) |
| `contact-property delete <id>` | Delete a contact-property schema |
| `contact create --properties '{"company":"Acme","plan":"pro"}'` | Assign property values when creating a contact |

### Topics (Granular Subscription Preferences)

Topics power Resend's granular unsubscribe flow on broadcasts. Each contact can be opted in or out of any topic, and broadcasts wired to a topic auto-substitute the right unsubscribe URL.

| Command | What it does |
|---|---|
| `topic list` | List topics |
| `topic get <id>` | Get topic details |
| `topic create --name "Weekly Digest"` | Create a topic (`--description`, `--default-subscription opt_in`/`opt_out`, `--visibility public`/`private`) |
| `topic update <id>` | Update topic fields (`--name`, `--description`, `--default-subscription`, `--visibility`) |
| `topic delete <id>` | Delete a topic |
| `topic contact-set --contact <id> --topic <id> --subscription opt_in` | Subscribe or unsubscribe a contact |
| `topic contact-list --contact <id>` | List a contact's topic subscriptions |

### Broadcasts

Broadcasts are Resend's native campaign sending. Use these instead of `batch send` when you want auto unsubscribe wiring (`{{{RESEND_UNSUBSCRIBE_URL}}}`), server-side scheduling, and Resend's internal queue throttling.

| Command | What it does |
|---|---|
| `broadcast list` | List broadcasts |
| `broadcast get <id>` | Get broadcast details |
| `broadcast create --segment-id <id> --from <addr> --subject <subj> --html <body>` | Create a broadcast (`--text`, `--name`, `--reply-to`, `--topic-id`, `--scheduled-at`, `--send` for inline send). `--audience-id` is accepted as a legacy alias. |
| `broadcast update <id>` | Update a draft broadcast (`--segment-id`, `--from`, `--subject`, `--html`, `--text`, `--name`, `--reply-to`, `--topic-id`) |
| `broadcast send <id>` | Send (or schedule via `--scheduled-at`) |
| `broadcast delete <id>` | Delete (cancels scheduled broadcasts) |

### Agent Tooling

| Command | What it does |
|---|---|
| `agent-info` | Machine-readable JSON capability manifest |
| `skill install` | Install skill file for Claude/Codex/Gemini |
| `skill status` | Check skill installation status |
| `completions <shell>` | Generate shell completions (bash/zsh/fish) |

## Mailing-list Workflows

Email CLI is the Resend client for [`mailing-list-cli`](https://github.com/199-biotechnologies/mailing-list-cli) and any other tool that needs to wrap a complete mailing-list workflow on top of Resend. Three primitives matter:

1. **Segments** group contacts. (Audiences, in pre-November-2025 terms.)
2. **Contact properties** are typed custom fields on each contact. Define the schema, then assign values.
3. **Topics** drive granular per-contact subscription preferences for broadcasts.

### End-to-end example

```bash
# 1. Define a custom field schema (do this once per property)
email-cli contact-property create --key company --property-type string
email-cli contact-property create --key plan --property-type string --fallback "free"

# 2. Create a topic for granular unsubscribes
email-cli topic create --name "Weekly Digest" --default-subscription opt_in
# → returns topic id, e.g. top_xyz

# 3. Create a segment to group contacts
email-cli segment create --name "Beta users"
# → returns segment id, e.g. seg_abc123

# 4. Add a contact with custom properties and add them to the segment in one shot
email-cli contact create \
  --email alice@example.com \
  --first-name Alice \
  --properties '{"company":"Acme","plan":"pro"}' \
  --segments seg_abc123

# 5. Subscribe the contact to the topic
email-cli topic contact-set \
  --contact alice@example.com \
  --topic top_xyz \
  --subscription opt_in

# 6. Create and send a broadcast (auto unsubscribe URL via {{{RESEND_UNSUBSCRIBE_URL}}})
email-cli broadcast create \
  --segment-id seg_abc123 \
  --topic-id top_xyz \
  --from "Acme <hello@yourdomain.com>" \
  --subject "This week at Acme" \
  --html '<p>Hi {{first_name}},</p><p>Latest digest...</p><p><a href="{{{RESEND_UNSUBSCRIBE_URL}}}">Unsubscribe</a></p>'
# → returns broadcast id, e.g. brd_456

email-cli broadcast send brd_456

# 7. Poll delivery status (replaces a webhook listener for the basic case)
email-cli email list --limit 100
```

> Note: Wiring `--topic-id` on the broadcast tells Resend which topic the unsubscribe link should opt the recipient out of. Without `--topic-id`, the unsubscribe URL still works but uses an account-wide unsubscribe.

### Why polling beats running a second webhook listener

`email list` returns each email's `last_event` (`sent`, `delivered`, `bounced`, `complained`, `opened`, `clicked`). For the suppression-driving path -- delivered/bounced/complained -- polling is enough and avoids the operational cost of a second webhook URL pointed at your tool. For full engagement history (open → click → unsubscribe), keep using `webhook listen` -- it stores every event into the local SQLite events table, which `events list` queries.

## Email Threading

Every outgoing email gets a unique `Message-ID` header (`<uuid@yourdomain.com>`). Replies set `In-Reply-To` and `References` per RFC 5322, so threads display correctly in Gmail, Outlook, and Apple Mail.

| Action | Headers Set | Threading |
|---|---|---|
| `send` | Message-ID | New conversation |
| `send --reply-to-msg <id>` | Message-ID, In-Reply-To, References | Threads into existing conversation |
| `reply <id>` | Message-ID, In-Reply-To, References | Threads into existing conversation |
| `reply <id> --all` | Same + preserves CC | Threads with all recipients |
| `forward <id>` | Message-ID only | New conversation (per RFC) |

## Agent Integration

Email CLI follows the [agent-cli-framework](https://github.com/199-biotechnologies/agent-cli-framework) patterns. Any agent that speaks structured JSON can use it.

### Capability Discovery

```bash
email-cli agent-info
```

Returns a JSON manifest of every command, flag, exit code, and output format. An agent calls this once and works from memory.

### Structured Output

All commands produce JSON when piped or when you pass `--json`:

```json
{
  "version": "1",
  "status": "success",
  "data": { ... }
}
```

Errors include actionable suggestions:

```json
{
  "version": "1",
  "status": "error",
  "error": {
    "code": "config_error",
    "message": "no default account configured",
    "suggestion": "Run profile add / account add to configure"
  }
}
```

### Semantic Exit Codes

| Code | Meaning | Agent action |
|---|---|---|
| 0 | Success | Continue |
| 1 | Transient error (network) | Retry |
| 2 | Configuration error | Fix setup |
| 3 | Bad input | Fix arguments |
| 4 | Rate limited | Wait and retry |

### Skill Self-Install

```bash
email-cli skill install
```

Writes a skill file to `~/.claude/skills/email-cli/`, `~/.codex/skills/email-cli/`, and `~/.gemini/skills/email-cli/`. The skill tells agents the CLI exists and to run `agent-info` for full details.

## Configuration

### Local State

All data lives in `~/.local/share/email-cli/email-cli.db` (override with `--db <path>`). Sibling directories:

- `draft-attachments/` -- snapshots of files attached to drafts
- `downloads/` -- fetched attachments (configurable via `--output`)

### Database Tables

| Table | Purpose |
|---|---|
| `profiles` | API key storage |
| `accounts` | Email identities with signatures |
| `messages` | Sent and received email with full metadata |
| `drafts` | Local drafts with attachment snapshots |
| `attachments` | Attachment metadata and local file paths |
| `sync_state` | Per-account sync cursor positions |

SQLite runs with WAL mode, busy timeout, and foreign keys enabled.

### Security

- API keys live in the local SQLite database. Treat `email-cli.db` as sensitive.
- Use `--api-key-env VAR_NAME` or `--api-key-file path` instead of passing keys directly.
- Attachment filenames are sanitized before writing to disk.
- Each send includes a UUID `Idempotency-Key` header.

### Requirements

- A [Resend]https://resend.com API key with sending enabled
- A verified Resend domain (enable receiving on the domain for inbox sync)
- Rust 1.85+ if building from source (edition 2024)

## Contributing

Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.

## License

MIT -- see [LICENSE](LICENSE).

---
<div align="center">

Built by [Boris Djordjevic]https://github.com/longevityboris at [199 Biotechnologies]https://github.com/199-biotechnologies | [Paperfoot AI]https://paperfoot.ai

<br />

**If this is useful to you:**

[![Star this repo](https://img.shields.io/github/stars/199-biotechnologies/email-cli?style=for-the-badge&logo=github&label=%E2%AD%90%20Star%20this%20repo&color=yellow)](https://github.com/199-biotechnologies/email-cli/stargazers)
&nbsp;&nbsp;
[![Follow @longevityboris](https://img.shields.io/badge/Follow_%40longevityboris-000000?style=for-the-badge&logo=x&logoColor=white)](https://x.com/longevityboris)

</div>