# mailx -- Email Client

A local-first email client with IMAP sync, full offline reading, and a standalone native app. Replaces Thunderbird/Outlook.

> **Disclaimer:** This is a personal project written for my own use. I provide it as-is with no promises of support, stability, or fitness for any particular purpose. Use at your own risk.

MIT License -- Copyright (c) 2026 Bob Frankston

## Installation

```bash
npm install -g @bobfrankston/mailx
mailx
```

Requires Node.js 22 or later.

The package installs two equivalent commands: **`mailx`** and **`bobmail`**. They're the same binary — `bobmail` is provided as an alternative for environments where `mailx` collides with another tool (most commonly the BSD `mailx`/Heirloom mailx at `/usr/bin/mailx` on macOS and some Linux distributions). Use whichever name doesn't conflict on your system.

On Windows, the native WebView2 app launches automatically via IPC (no HTTP server needed). Use `mailx --server` for browser mode at `http://127.0.0.1:9333`.

## First-Time Setup

On first run, mailx shows a setup form. Enter your name and email address.

### Gmail / Google Workspace

No password needed. mailx detects Gmail via MX records, opens a browser for OAuth2 sign-in, and auto-configures Google Drive for settings sync. Custom domains hosted on Google Workspace are detected automatically.

### Outlook / Hotmail / Live

No password needed. OAuth2 sign-in opens in your browser.

### Yahoo

Use an **app password** (not your regular password):
1. Go to **Yahoo Account Settings** > **Account Security**
2. Click **Generate app password**
3. Paste the generated password into mailx

### AOL

Use an **app password**:
1. Go to **AOL Account Settings** > **Account Security**
2. Click **Generate app password**
3. Paste the generated password into mailx

### iCloud

Use an **app-specific password**:
1. Go to [appleid.apple.com](https://appleid.apple.com) > **Sign-In and Security**
2. Click **App-Specific Passwords** > **Generate**
3. Paste the generated password into mailx

### Other Providers (Dovecot, Fastmail, etc.)

Enter your IMAP password. mailx auto-detects server settings via MX records and DNS. If detection fails, use **Manual setup (advanced)** in the setup form, or create an accounts file and import it (see below).

### Adding Accounts via File

You can create an `accounts.jsonc` file and import it into mailx. This is useful for adding accounts that aren't easily configured through the setup form (custom IMAP servers, non-standard ports, etc.).

**Step 1.** Create a file (e.g., `my-accounts.jsonc`) with your account(s):

```jsonc
{
  "accounts": [
    {
      "email": "you@example.com",
      "name": "Your Name",
      "label": "Work",                   // Display name in folder tree
      "imap": {
        "host": "imap.example.com",      // IMAP server hostname
        "port": 993,                     // 993 for TLS (default)
        "tls": true,
        "user": "you@example.com",       // IMAP username (default: email)
        "password": "your-password"
      },
      "smtp": {
        "host": "smtp.example.com",      // SMTP server hostname
        "port": 587,                     // 587 for STARTTLS (default)
        "tls": true,
        "user": "you@example.com",
        "password": "your-password"
      }
    }
  ]
}
```

For known providers, most fields are optional:

```jsonc
{
  "accounts": [
    { "email": "you@gmail.com", "name": "Your Name" },
    { "email": "you@yahoo.com", "name": "Your Name", "password": "app-password-here" },
    {
      "email": "you@yourserver.com",
      "name": "Your Name",
      "label": "Personal",
      "imap": { "host": "imap.yourserver.com", "user": "you", "password": "pass" },
      "smtp": { "host": "smtp.yourserver.com", "user": "you", "password": "pass" }
    }
  ]
}
```

**Step 2.** Import into mailx:

```bash
mailx --import my-accounts.jsonc
```

This merges the accounts into your Google Drive settings (deduplicates by email address). Existing accounts are preserved.

### Gmail OAuth Prerequisites

Gmail OAuth requires a one-time Google Cloud setup:
1. Go to [Google Cloud Console](https://console.cloud.google.com/) and create a project
2. Enable the **Gmail API** (and **People API** for contacts)
3. Create **OAuth 2.0 credentials** (Desktop app type)
4. Download `credentials.json` to the iflow package directory
5. First connection opens a browser for consent. Tokens refresh automatically.

## Usage

### Layout

A vertical **icon rail** sits on the far left (Thunderbird / Dovecot style) with one-click access to Compose, Inbox, All Inboxes, and Settings; Calendar / Tasks / Contacts slots are placeholders until those features land. The rail is always visible on wide and medium screens; it folds into the hamburger menu on narrow ones.

### Reading Mail

- Click a **folder** to see its messages
- Click a **message** to read it in the preview pane
- **All Inboxes** combines inboxes from all accounts (appears with 2+ accounts)
- A small filled teal dot before the date means the message body is downloaded locally (offline-ready); a hollow circle means not yet prefetched. Prefetch runs as a background task every ~60 s independent of folder sync.
- Unread counts show on folders; sub-folder counts bubble up to collapsed parents
- Use the **folder search** box to find folders by name

### Composing

- **Ctrl+N** -- New message
- **Ctrl+R** -- Reply
- **Ctrl+Shift+R** -- Reply All
- **Ctrl+F** -- Forward
- From dropdown lets you pick which account to send from; reply auto-detects which identity to reply from based on which of your addresses the mail was sent to
- Contact autocomplete searches Google Contacts as you type in To/Cc/Bcc
- **Cc / Bcc** are hidden by default — click the toggle buttons next to To to show them
- **Attach** opens a file picker; attachments show as chips with remove buttons
- Drafts auto-save 1.5s after you stop typing, plus a 5s safety-net interval, plus on window close
- Compose window close asks Save / Discard / Cancel if there's content
- Address validation (`local@domain.tld`) runs on To/Cc/Bcc/From before sending — invalid addresses are refused
- **Editor shortcuts**: Ctrl+K insert link, Ctrl+Shift+K remove link, Ctrl+Shift+X strikethrough, Ctrl+Shift+7/8 ordered/bullet list, Ctrl+]/[ indent/outdent, Ctrl+Shift+C color, Ctrl+\ clear formatting. Native spell-check via WebView2 (right-click to add to dictionary).
- **Link editor modal**: Ctrl+K opens a two-field dialog (text + URL) with Remove-link button; hovering any link in the editor shows a floating URL preview
- **Paste URL** auto-links: paste a bare URL over a selection and it wraps it, or paste into empty space to insert as a link

### Managing Messages

- **Delete** or **Ctrl+D** -- Delete selected messages (moves to Trash; second delete in Trash is a hard delete + EXPUNGE)
- **Ctrl+Z** -- Undo the last **delete or move** (whichever came last, 60s window)
- **Ctrl+A** -- Select all messages in the list
- **Drag and drop** -- Move messages to a folder by dragging them
- **Right-click a message → Move to folder…** -- Searchable folder picker; useful on narrow layouts where the folder tree is hidden
- Click the **star** column to flag/unflag a message
- **Unsubscribe** button appears when the message has a List-Unsubscribe header. One-click (RFC 8058) when the sender advertises `List-Unsubscribe-Post: List-Unsubscribe=One-Click`; otherwise opens the URL or a pre-filled compose for `mailto:` lists.
- **Right-click on a From/To/Cc address** -- Copy name, Copy address, Copy both, Add to contacts, or Reply/Reply All/Forward
- **Right-click in the message body → Translate** (opt-in) -- Uses the configured AI provider; select text first to translate just the selection. Off by default; enable under **Settings → AI translate**.
- **Preview pane zoom** -- Ctrl+wheel, Ctrl+= / Ctrl+- / Ctrl+0, or right-click menu (Zoom in/out/reset, Copy, Select all). Persisted across messages.
- **Cross-folder search results** show the folder name for each hit
- **Empty Trash / Empty Junk** -- Right-click the folder in the tree → Empty (confirmation prompt)

### Searching

Type in the search bar and press **Enter**. Use the dropdown to search:
- **All folders** -- Every account and folder
- **This folder** -- Current folder only (filters as you type, server search on Enter)
- **IMAP server** -- Server-side search (useful for messages older than your sync window)

Supports regex: wrap pattern in `/slashes/`.

### Folder Operations

Right-click a folder for options:
- Mark all read
- New subfolder
- Rename / Delete folder
- Empty (Trash and Junk only)

### View Options

Under **View** in the toolbar:
- **Two-line view** -- Show preview snippet below each subject
- **Preview pane** -- Toggle the message reader panel
- **Preview snippets** -- Show snippet text in message rows
- **Flagged only** -- Show only starred messages
- **Folder counts** -- Show total message counts on all folders

### Settings

Under **Settings** in the toolbar:
- **Editor** -- Choose between Quill (default) and tiptap for compose
- **AI autocomplete** -- Enable LLM-powered writing suggestions (Ollama, Claude, or OpenAI)
- **AI translate** -- Enable the right-click Translate item in the message viewer (off by default; uses the same provider as autocomplete)
- **AI proofread** -- Enable the proofread path (off by default; provider method available, UI wiring in progress)

All AI features are opt-in. Provider + API key live in the autocomplete settings; toggling a feature only controls whether that specific capability calls out to the provider.

### Keyboard Shortcuts

| Key | Action |
|-----|--------|
| Ctrl+N | New message |
| Ctrl+R | Reply |
| Ctrl+Shift+R | Reply All |
| Ctrl+F | Forward |
| Delete / Ctrl+D | Delete |
| Ctrl+Z | Undo last delete or move |
| Ctrl+A | Select all |
| F5 | Sync all folders |
| Escape | Clear search / close menus |

**In the compose editor:**

| Key | Action |
|-----|--------|
| Ctrl+K | Insert / edit link (opens dialog with text + URL fields) |
| Ctrl+Shift+K | Remove link |
| Ctrl+B / Ctrl+I / Ctrl+U | Bold / Italic / Underline |
| Ctrl+Shift+X | Strikethrough |
| Ctrl+Shift+7 / 8 | Ordered / Bullet list |
| Ctrl+] / Ctrl+[ | Indent / Outdent |
| Ctrl+Shift+C | Set text color |
| Ctrl+\ | Clear formatting |
| Ctrl+Enter | Send |
| Escape | Close (prompts Save / Discard / Cancel) |

**In the preview pane:**

| Key | Action |
|-----|--------|
| Ctrl+wheel | Zoom in/out |
| Ctrl+= / Ctrl+- | Zoom in / out |
| Ctrl+0 | Reset zoom |
| Delete | Delete message (also works with focus in preview) |

## Command Line

```
mailx                        Start the app (native window via IPC)
mailx --email <addr>          First-time setup with this email (skips prompt)
mailx --verbose               Show log output in terminal (default: log file only)
mailx --import <file>         Import accounts.jsonc into Google Drive
mailx --server                Start HTTP server for dev/remote (http://localhost:9333)

mailx -kill                   Kill running mailx processes + clean up WAL files
mailx -repair                 Re-sync message metadata (fix garbled subjects)
mailx -rebuild                Wipe local cache and re-download everything from IMAP
mailx -setup                  Interactive first-time setup (CLI)
mailx -test                   Test IMAP/SMTP connectivity for all accounts
mailx -reauth                 Clear cached OAuth tokens; next start re-consents
mailx -v                      Show version
```

### -repair vs -rebuild

**-repair** clears message metadata (subjects, flags, sender names) from the database but preserves your downloaded `.eml` message files. Folder sync state resets so mailx re-fetches all envelopes on next run. Use this when subjects show garbled characters.

**-rebuild** deletes the entire database and message store, then re-downloads everything from IMAP. Accounts and settings are preserved. Use this for a clean start or to reclaim disk space.

## Configuration

All config files live in `~/.mailx/` (on Windows: `C:\Users\You\.mailx\`).

### config.jsonc (per-machine, not synced)

Points to shared settings and controls local behavior. Created automatically for Gmail accounts.

```jsonc
{
  "sharedDir": { "provider": "gdrive", "path": "mailx", "folderId": "..." },
  "storePath": "C:/Users/You/.mailx/mailxstore",
  "historyDays": 90,
  "accountOverrides": {
    "work": { "historyDays": 7 },
    "archive": { "enabled": false }
  }
}
```

**sharedDir** can be:
- `{ "provider": "gdrive", "path": "mailx" }` -- Google Drive via API (auto-configured for Gmail)
- `"C:/Users/You/OneDrive/home/.mailx"` -- Local/network path

### accounts.jsonc (shared via Google Drive or local)

For known providers, only `email` is required -- IMAP/SMTP settings fill automatically.

```jsonc
{
  "accounts": [
    { "email": "you@gmail.com", "name": "Your Name" },
    { "email": "you@icloud.com", "name": "Your Name", "password": "xxxx-xxxx-xxxx-xxxx" },
    {
      "email": "you@example.com",
      "name": "Your Name",
      "label": "Work",
      "spam": "_spam",                    // Optional: enables the ⚠ Spam button in the viewer toolbar
      "imap": { "host": "imap.example.com", "port": 993, "tls": true, "user": "you", "password": "..." },
      "smtp": { "host": "smtp.example.com", "port": 587, "tls": true, "user": "you", "password": "..." }
    }
  ]
}
```

**Optional per-account fields:**

| Field | Type | Purpose |
|-------|------|---------|
| `spam` | string | IMAP folder path to send messages when the **Spam** (⚠) button is pressed. The button is hidden until this is set. Use the exact folder path on the server (e.g., `"_spam"`, `"INBOX/Spam"`, `"[Gmail]/Spam"`). |
| `label` | string | Display name in the folder tree (overrides auto-detected). |
| `defaultSend` | bool | Use this account's SMTP when From doesn't match any account. |
| `relayDomains` | string[] | Domains to skip in Delivered-To chain (e.g., `["m.connectivity.xyz"]`). |
| `deliveredToPrefix` | string[] | Strip these prefixes from Delivered-To to recover the clean alias (e.g., `["bobf-ma-", "bobf-"]` — order matters, longest first). |
| `identityDomains` | string[] | Domains where Delivered-To becomes the reply From (e.g., `["bob.ma"]`). |

**Auto-detected providers:**

| Domain | IMAP | SMTP | Auth | Label |
|--------|------|------|------|-------|
| gmail.com | imap.gmail.com:993 | smtp.gmail.com:587 | OAuth2 | Gmail |
| outlook.com, hotmail.com | outlook.office365.com:993 | smtp.office365.com:587 | OAuth2 | Outlook |
| yahoo.com | imap.mail.yahoo.com:993 | smtp.mail.yahoo.com:587 | App password | Yahoo |
| aol.com | imap.aol.com:993 | smtp.aol.com:587 | App password | AOL |
| icloud.com | imap.mail.me.com:993 | smtp.mail.me.com:587 | App password | iCloud |

Google Workspace and Microsoft 365 custom domains are detected automatically via MX records.

### preferences.jsonc (shared)

```jsonc
{
  "ui": { "theme": "dark", "editor": "quill", "fontSize": 15 },
  "sync": { "intervalMinutes": 5, "historyDays": 30 },
  "autocomplete": {
    "enabled": false,
    "provider": "ollama",
    "ollamaUrl": "http://localhost:11434",
    "ollamaModel": "qwen2.5-coder:1.5b"
  }
}
```

### allowlist.jsonc (shared)

Remote content (images, tracking pixels) is blocked by default. Whitelist trusted senders:

```jsonc
{
  "senders": ["boss@example.com"],
  "domains": ["example.com"],
  "recipients": ["team@our-group.com"]
}
```

### Multi-Machine Setup

Settings sync via Google Drive API. Each machine has its own `config.jsonc`:

**Desktop** (full history):
```jsonc
{ "sharedDir": { "provider": "gdrive", "path": "mailx" }, "historyDays": 0 }
```

**Laptop** (recent only):
```jsonc
{ "sharedDir": { "provider": "gdrive", "path": "mailx" }, "historyDays": 30,
  "accountOverrides": { "archive": { "enabled": false } } }
```

Both machines share accounts, preferences, and allowlist via Google Drive.

## Data Storage

| Path | Synced? | Purpose |
|------|---------|---------|
| config.jsonc | No | Local config pointer + overrides |
| accounts.jsonc | Yes | IMAP/SMTP account configs |
| preferences.jsonc | Yes | UI and sync settings |
| allowlist.jsonc | Yes | Remote content allow-list |
| mailx.db | No | SQLite metadata (headers, contacts, sync state) |
| mailxstore/ | No | Cached message bodies (.eml per message) |
| logs/ | No | Daily log files |

**Safe to delete:** mailxstore/ (re-downloaded on sync), mailx.db (re-created on startup), logs/ (anytime).

## Logs

Daily log files: `~/.mailx/logs/mailx-YYYY-MM-DD.log`

Use `mailx --verbose` to see logs in the terminal instead.

## Troubleshooting

**Blank window or no response** -- Run `mailx -kill` then `mailx` to restart.

**OAuth timeout** -- If the browser auth window doesn't appear or times out (5 minutes), close mailx and try again.

**Garbled subjects** -- Run `mailx -repair` to re-sync metadata from IMAP.

**Sync stuck or corrupt data** -- Run `mailx -rebuild` for a clean start. Accounts and settings are preserved.

**"Too many connections"** -- mailx uses up to 5 IMAP connections per account. Close other email clients if you hit your server's limit.

**App password rejected (Yahoo/AOL/iCloud)** -- Make sure you're using an app-specific password, not your regular login. Generate a new one from your account security settings.

## Architecture

- **IPC-first** -- Default mode uses msger (Rust/WebView2) with bidirectional IPC. No TCP server, no CLOSE_WAIT zombies.
- **Host abstraction** -- mailx talks to the WebView host through `@bobfrankston/mailx-host`, which picks msger (default) or msgview (Electron, planned for Mac and niche Linux) at runtime. Override with `MAILX_HOST=msger|msgview`.
- **HTTP fallback** -- `--server` flag starts Express for browser/remote access.
- **Local-first** -- Changes update local DB immediately; background worker syncs to IMAP.
- **Offline reading** -- Full message bodies cached as .eml files.
- **IMAP IDLE** -- Instant new-mail notifications + periodic sync fallback.
- **Outbox** -- Queued in IMAP Outbox folder with multi-machine interlock via flags.
- **Remote content blocking** -- HTML sanitized server-side, CSP in iframe, per-sender allow-list.
- **Search** -- SQLite FTS5 full-text index + IMAP server search + regex filtering.
- **Connection management** -- Semaphore limits 5 connections per account; exponential backoff on server limits.
- **Cloud sync** -- Google Drive API for settings portability (no filesystem mount required).
