Apps

Mail

# 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. mail ## 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 ````