Apps
# Mail — Deft Email Client
A three-pane email client — folders | message list side by side on top,
the message reader below — with in-panel compose, reply/reply-all/forward,
flags, move/delete, search, attachments and a local SQLite cache. IMAP
sync and SMTP delivery run through `deft/imap` / `deft/smtp` on a
background worker coroutine, so the UI never blocks on the network.
## Running
Build the TUI hosts first (`./mk ui ansi release` or `./mk ui tui release`),
then start a workspace with the mail panel — either the launcher
(tags `app`) or the bundled workspace:
```
retro ws mail # uses .deft/workspaces/mail.dft
retra ws mail # ANSI/SSH host
````
Or add it to any workspace:
%{ :id "mail"
:app "mail"
:source ".deft/packages/mail/src/mail.dft"
:focus true }
## Configuration
The easiest way is the in-app form: Account → Add Account…
(`Ctrl+N`) or Account → Edit Account… (`Ctrl+E`). Fill in the
fields with Tab, `Ctrl+Enter` saves. Save writes the account into
`:mail/accounts` (and `:mail/active-account` when unset) and persists
the registry with `config/save-overrides` — set `:mail/persist-accounts`
to `false` to keep edits in memory only. Passwords are not part of the
form or the registry; set them in the environment (see
Passwords).
Accounts can also be edited by hand in the config registry (see
`packages/mail/config.dft` for defaults; override in `.deft/config.dft`
or with the config app). Servers, ports and transport live on the
account:
%{
:mail/accounts %{
:proton %{
:imap-host "127.0.0.1" :imap-port 1143 :imap-tls false
:smtp-host "127.0.0.1" :smtp-port 1025 :smtp-tls false
:user "me@example.com"
:from "me@example.com" }
:fastmail %{
:imap-host "imap.fastmail.com" :imap-port 993 :imap-tls true
:smtp-host "smtp.fastmail.com" :smtp-port 465 :smtp-tls true
:user "me@example.com"
:from "me@example.com" } }
:mail/active-account "fastmail"
:mail/sync-interval-ms 60000
:mail/page-size 200
:mail/sent-folder "Sent"
:mail/trash-folder "Trash"
:mail/mark-read true }
### Transport
| Setting | Use |
|---------|-----|
| `:imap-tls true` / `:smtp-tls true` | implicit TLS (993 / 465, default) |
| `:imap-starttls true` / `:smtp-starttls true` | plaintext connect + STARTTLS upgrade (143 / 587); takes precedence over `:*-tls` |
| both false | plaintext, for local bridges (Proton Bridge 1143 / 1025) |
TLS is always verified against the system CA bundle
(`SSL_CERT_FILE`/`SSL_CERT_DIR` to point at a private CA).
Proton Bridge speaks plaintext on loopback: use
`:imap-host "127.0.0.1" :imap-port 1143 :imap-tls false :imap-starttls false`
(and `:smtp-host "127.0.0.1" :smtp-port 1025` likewise), with `:user` set to
the account's full email address. Bridge's STARTTLS certificate is
self-signed, so STARTTLS only works if you export Bridge's certificate and
launch the host with `SSL_CERT_FILE=/path/to/bridge-cert.pem`.
### Passwords
Resolution order per account:
1. `DEFT_MAILPASS_<ACCOUNT>` from the environment — the account name
uppercased with non-alphanumerics replaced by `_`, so account
`proton` reads `DEFT_MAILPASS_PROTON` — or `DEFT_MAILPASS` as a
fallback for single-account setups;
2. a one-time `dialog/password` prompt per session, kept in memory only.
Passwords are never stored in the config registry. For example:
```
DEFT_MAILPASS_PROTON=bridge-password retra ws mail
````
## Keys
Normal mode (rebindable through the keys app / `keys.dft`):
| Key | Action |
|-----|--------|
| `j`/`k`, `↓`/`↑` | move selection (or scroll the reader) |
| `PgUp`/`PgDn`, `Home`/`End` | page / jump |
| `Enter` | open folder (folders pane) or message |
| `Tab` | cycle pane focus; `1`/`2`/`3` focus folders/list/reader |
| `g`, `Ctrl+R` | refresh the open folder |
| `/` | IMAP search |
| `c` | compose |
| `r` / `a` / `f` | reply / reply-all / forward |
| `s` | toggle `\Flagged` |
| `u` | toggle read/unread |
| `Ctrl+Space` | mark/unmark message (batch select) |
| `d`, `Del` | move to Trash |
| `m` | move to folder (menu) |
| `n` / `p` | next / previous message |
| `Ctrl+A` | attach a file (compose) |
| `Ctrl+S` | save first attachment |
| `Ctrl+N` / `Ctrl+E` | add / edit an account (form) |
| `Ctrl+O` | switch account |
| `Esc` | back to the list / discard compose / cancel the form |
| `q` | close the panel |
The Account menu mirrors these (`Add Account…`, `Edit Account…`,
`Switch Account`).
`Ctrl+Space` marks the current message and moves the cursor down;
pressing it again unmarks. While messages are marked, `d` (delete),
`m` (move), `s` (flag) and `u` (read/unread) apply to every marked
message, and the marks clear afterwards.
Compose mode has its own fixed editing keys: `Tab`/`Enter` next field,
arrows and `Home`/`End` move the cursor, `Backspace`/`Delete` edit,
`Ctrl+Enter` sends, `Esc` cancels.
## Behaviour
- Cache — folders, headers, bodies and flags are cached in SQLite at
`.deft/local/mail/mail.db` (override with `:mail/db-path`). Browsing and
reading work offline; the reader serves cached bodies without a round
trip.
- Sync — one command in flight per connection, serialised through a
queue owned by the worker. The open folder refreshes every
`:mail/sync-interval-ms` while idle; `\Seen` is set on open when
`:mail/mark-read` is true.
- Send — SMTP `send` (best-effort recipients, rejected addresses are
reported) then APPEND of the same message to `:mail/sent-folder` with
`\Seen` when the IMAP connection is live.
- HTML mail — messages with no `text/plain` part get a best-effort
tag-strip to readable text (links become their anchor text).
- Attachments — the reader lists them; `Ctrl+S` saves via a file
dialog. Forward carries attachments along; compose attaches via
`Ctrl+A` (content type guessed from the extension).
## Limits
- One account is connected/synced at a time; switch with `Ctrl+O`.
- IMAP one-command-in-flight is enforced by the worker queue; waiting
commands run in order.
- No threading, no IMAP IDLE (periodic refresh only), no persistent
drafts, no GPG/S-MIME.
- `net/*` is native-only: the app does not run in the wasm32-wasi runtime.
## Tests
Hermetic: store, compose/send, HTML fallback and sync run against the
packaged fake IMAP/SMTP servers; the panel's key/dialog/compose logic is
tested headlessly.
```
cd packages/mail
deft --prelude test/prelude.dft test test/test_store.dft
deft --prelude test/prelude.dft test test/test_html_text.dft
deft --prelude test/prelude.dft test test/test_sync.dft
deft --prelude test/prelude.dft test test/test_send.dft
deft --prelude test/prelude.dft test test/test_panel.dft
````