# rmut
A mutt replacement in Rust: terminal mail client with mutt keybindings,
built on ratatui. See [docs/PLAN.md](docs/PLAN.md) for the roadmap.
## Status
**2.0**: rmut is a daily-drivable mutt replacement: mutt's index,
pager and compose workflow under mutt's default keys, over maildir,
mbox and IMAP/SMTP (TLS, OAuth2), with PGP through gpg(1). 2.0 marks
the parity roadmap finishing, not a break: every default mutt key
does something, a muttrc imports (`--import-muttrc`) and loads
without errors, and every setting in the parity fixture
(`tests/e2e/muttrc-parity.rc`, the hundred-odd that turn up in every
dotfiles repo) is carried over or answered by what rmut does
instead. The config format did not change; a 1.x config is a 2.0
config.
What is there, briefly: an index with threads (References and
In-Reply-To, plus mutt's grouping by subject for mail that carries
neither, folding, thread operations), mutt's patterns for limit,
search, tag and delete, sort orders, `%`-format strings for the
index, status and pager lines, colours by pattern and by depth of
quoting, a sidebar and a folder browser with new-mail counts, a
compose menu with attachments (files, messages, renames, dispositions),
PGP sign/encrypt, postpone and recall, Fcc, identities and hooks
(folder, message, reply, fcc, crypt), aliases and query_command
completion, macros and mutt's enter-command (`:set`, `:bind`,
`:color`, the `un*` family, `reset`), IMAP with a local cache, IDLE,
transparent reconnect, server-side search and folder management, mbox
with write-back, notmuch search into a virtual mailbox, undo and
undo-send, and a man page (`docs/rmut.1`).
Not there, on purpose: S/MIME, POP3, scoring, MH/MMDF folders,
compressed-folder hooks, an embedded scripting language and an HTML
rendering engine. Still to come in 2.x: `$tunnel` and accept-once
for unknown certificates, the PGP odds (opportunistic encryption,
inline PGP), and the envelope odds (`$dsn_*`, `$use_envelope_from`,
`$reply_self`, ...). The round-by-round history is in
[docs/PLAN.md](docs/PLAN.md).
A pty-driven e2e suite (including fake IMAP/SMTP servers and a
stub gpg) lives in `tests/e2e/`.
## Install & run
```sh
cargo install rmut # the `rmut` binary
# or from a checkout:
cargo install --path crates/rmut
# or during development:
cargo run -p rmut -- ~/Maildir # or: just run ~/Maildir
```
Tagged releases on GitHub carry a prebuilt x86_64-linux binary with
the man page; `man docs/rmut.1` previews the manual from a checkout.
```
usage: rmut [MAILDIR | MBOX | imap:ACCOUNT[/FOLDER]] (-V version, -h help)
rmut --import-muttrc [-w] [MUTTRC]
```
Without an argument rmut looks where mutt looks: the first configured
mailbox, then `[mail] folder`, `$MAIL` (a maildir or an mbox file),
`~/Maildir`, `~/Mail`, `~/mail`, and finally the `/var/mail/$USER`
spool. A directory of maildirs answers with its `inbox`. When nothing
turns up it says where it looked, and how to make one:
```sh
mkdir -p ~/Mail/inbox/{cur,new,tmp} && rmut ~/Mail/inbox
```
The same mail in a window is `rmut-egui`, the egui front end over the
same session:
```sh
cargo install rmut-egui
just install-desktop # and a menu entry for it
```
`just install-desktop` writes
`~/.local/share/applications/rmut-egui.desktop` from
[crates/rmut-egui/dist](crates/rmut-egui/dist), pinning `Exec` to the
installed binary because a desktop session rarely carries
`~/.cargo/bin` on its `PATH`; a packager installs the file as it
stands. The entry claims no `mailto:` handler on purpose: the window
takes a mailbox, not a draft, and `mailto:` is the terminal binary's.
## The screen
Four regions, as in mutt: the help bar at the top, the mailbox or the
message, the status bar, and the message line under it. The message
line is where prompts, notes and errors go, so the status bar always
says which mailbox you are in and what is in it, whatever else is
happening.
## Keys
Index: `j`/`k` move, `Enter` view, `=`/`*` first/last, PgUp/PgDn or
Ctrl+B/Ctrl+F page, `d`/`u` delete/undelete, `F` flag, `N` toggle
read, `t` tag + `;` apply the next mark, save, copy, pipe, print or
bounce to all tagged, `z` undo
the last of those (or cancel a held send), `s` save
(copy to a mailbox + mark deleted), `$` sync changes to disk (asks
before purging deleted messages, like mutt: Enter takes the yes, and
`[mail] delete = "yes"` skips the question), Space page down,
Ctrl+L repaint, `!` shell command, Ctrl+Z suspend (fg brings it
back), `o` sort
(`d`ate `f`rom t`o` `s`ubject si`z`e `t`hreads `y`label `u`nsorted,
uppercase reverses),
Alt+v/Alt+V fold thread/all, Alt+d/Alt+u/Alt+t delete/undelete/tag a
whole thread, Ctrl+D/Ctrl+U the same for a subthread, Alt+n/Alt+p
type a number then Enter to jump to that message, `H`/`M` move to the
top/middle of the page, `@` show the
sender's full address, `%` toggle the mailbox read-only,
step between threads, Ctrl+R/Alt+r mark a thread/subthread read, `P`
jump to the parent, `#` break a thread, `&` link the tagged messages
under the cursor (all of these want thread sort), Alt+s/Alt+C
decode-save/decode-copy (the message as the pager shows it), `Y` edit the
X-Label, `V` show the version, Alt+l show the active limit, `l` limit,
`/` search
+ `n` next (Alt+/ searches backwards, and `n` then keeps going that
way; `n` wraps around the ends unless `[mail] wrap_search = false`),
`c` open mailbox by path (Alt+c opens it read-only; Tab completes
mailboxes,
account folders and nearby maildirs; empty Tab opens the folder
browser), `y` folder browser (with
new/unseen counts; folders holding new mail show bold), `G` check
for new mail now, `B` toggle the sidebar (Ctrl+N/Ctrl+P move its
highlight, Ctrl+O opens the highlighted mailbox), `v` attachments,
`m` compose, `r` reply, `g` group
reply, `L` list-reply, `f` forward, `C` copy to a mailbox (no delete mark), `|` pipe
the raw message to a shell command, `b` bounce (resend as-is to new
recipients, with a Resent-\* block), `e` edit the raw message (mutt's
edit; the changed result replaces the original), Alt+e edit as a new
draft (resend), `a` add the sender to the alias file (nick prompted, local
part prefilled), `p` print (pipes the message to `mail.print`,
default `lpr`), `~` mark-message (a stroke becomes a hotkey that
jumps back to the message), Esc+L list-action (the message's List-*
headers: help, post, subscribe, unsubscribe, archives, owner; mailto:
ones compose), and, unbound as in mutt but bindable by name,
`next-unread-mailbox`, `purge-message` (delete past the trash),
`error-history` and `what-key`, `:` run a config command (see **Enter-command**
below), `q` quit (writes changes; asks before purging
deletions, like mutt), `x` abort without saving. Leaving a mailbox
ages unread new mail to old (`O`), mutt's mark_old.
Pager: `j`/`k` scroll, `Space`/`-` page down/up, `J`/`K` next/previous
message, `d` delete and advance, `u`/`F`/`t` undelete/flag/tag without
leaving the message, `h` toggle full headers,
`v` attachments, `m`/`r`/`g`/`L`/`f` compose/reply/list-reply/forward,
`p` print,
`s` save, `C`/`|`/`b` copy/pipe/bounce, Alt+s/Alt+C decode-save/copy,
`e`/Alt+e edit raw/resend,
`:` run a config command, `q`/`i` back. Space past the end opens the next message and wrapped
lines carry a leading `+` marker, like mutt. Replies ask mutt's
ask-yes questions: Reply-To (when the header is set), "No subject,
abort?", and "Include message in reply?"; Enter takes the yes.
Attachments: `Enter` view a text part, `s` save part to a file.
Patterns (limit/search): `~f x` from, `~s x` subject, `~b x` body,
`~t x` to, `~c x` cc, `~C x` to-or-cc, `~e x` sender, `~h x` any
header, `~i x` Message-ID, `~x x` References, `~d spec` date,
`~r spec` received date, `~m spec` index range, `~z spec` size range,
`~=` duplicate (same Message-ID twice), `~(P)`/`~<(P)`/`~>(P)` thread,
parent or child matches P, `~v` folded thread head, `~$` unreferenced,
`~y x` X-Label, `~L x` from-or-to, `~B x` whole message (headers and
body),
`~N` new, `~O` old, `~R` read, `~Q` replied, `~F` flagged, `~D` deleted,
`~U` unread, `~T` tagged,
`~l` addressed to a known mailing list, `~u` to a subscribed list,
`~p` addressed to me,
`~P` sent by me, `~A` every message; a bare (single) word with no `~`
expands through `[mail] simple_search` (mutt's, default `~f %s | ~s %s`,
so subject or from); set it to `~f %s | ~s %s | ~b %s` to search the
body too. `x` is a
case-insensitive regex (`"quotes"` keep spaces; an invalid regex falls
back to plain substring). `~d` takes a day or range
(`24/12/2026`, `1/6/2026-30/6/2026`, `24/12-`, `-1/1/2027`) or an
offset: `<1w` (within), `>2d` (older than), `=3d` (that day); units
y m w d H M, and `~r` takes the same specs against delivery time.
`~m` counts the index as shown (`~m 10-20`, `~m 5-`, `~m -20`), with
`.` for the selected message and `$` for the last (`~m .-$`). `~z`
takes `>100K`, `<2M`, `1K-2M`, or a plain byte count (K/M/G are
powers of 1024). Adjacent terms AND, `!` negates, `|` ORs, `()`
groups: `!~D (~f jane | ~t jane) ~d <1m`.
## IMAP
`rmut imap:work` (or `imap:work/Archive`) opens an account folder;
`c` and the folder browser `y` take the same specs, and `y` lists the
account's folders via LIST. In the browser, `C` creates a folder
(type an `imap:account/name` spec for a remote one), `d` deletes the
selected mailbox (confirmed), `r` renames it, and `s`/`u`
subscribe/unsubscribe: CREATE / DELETE / RENAME / SUBSCRIBE on the
open account, or the filesystem for a local maildir. Messages are mirrored into a cache maildir
under `~/.cache/rmut/imap/` (headers up front, full bodies fetched on
first view), so the index is fast and old mail reopens offline. `$`
pushes your changes to the server (flags via UID STORE, deletes via
EXPUNGE). New mail is announced by **IDLE** (RFC 2177, on a second
connection) and shows up within a second; when the server doesn't
support IDLE, the NOOP poll (`poll_seconds`) picks it up as before.
The folder browser asks the server for UNSEEN counts (STATUS). A
connection dropped by laptop sleep or a server timeout is transparently
reopened and the operation retried once; polls fetch only new arrivals
unless the server reported flag changes or expunges. The
password comes from `password_command` (e.g. `pass show mail/work`),
run once per session, or from a stored `password`, if you accept a
secret sitting in the config file (keep it chmod 600).
The connection runs on a thread of its own, so a slow server does not
stop the screen: the index keeps drawing, the keys keep working, the
message line says what is happening ("fetching the message... (Ctrl+G
aborts)"), and **Ctrl+G** gives up on it the way mutt's does, after
which the next operation reconnects. The poll for new mail, `$` sync,
fetching a message body and the sidebar's unread counts all work this
way; opening a mailbox and listing folders still wait for the server.
A server that does not answer costs seconds, not the OS default of
about two minutes with nothing on screen: `[net] connect_timeout` (10s)
bounds the connection and `[net] timeout` (30s) bounds waiting for data
on a live one, and both say which host and what they were doing.
Both take `:set connect_timeout=...` / `:set net_timeout=...` at
runtime, and `--import-muttrc` brings mutt's `$connect_timeout` over.
TLS trusts the built-in Mozilla roots and, by default, the operating
system's certificate store on top (`[net] system_cas`, mutt's
`$ssl_usesystemcerts`); `[net] certificate_file` names a PEM of extra
roots for a private CA or a self-signed server. These only ever add
trust anchors, never replace the defaults. Imported from mutt's
`$certificate_file` / `$ssl_ca_certificates_file`; rmut has no
interactive accept-once, client certificates or `$tunnel` yet, so
those import as skipped.
For Gmail/O365-style **OAuth2**, set `auth = "xoauth2"` (or
`"oauthbearer"`, RFC 7628) and a `token_command` whose first output
line is a fresh access token; acquiring and refreshing tokens is the
external tool's business (oauth2ms, mutt_oauth2.py, ...). The command
runs for every connection, since tokens expire; both IMAP
(AUTHENTICATE) and SMTP (AUTH) then use the token instead of a
password.
## mbox
`rmut /var/mail/$USER` opens an mbox file: it is mirrored into a cache
maildir (like IMAP folders), so the index, pager, flags, and patterns
all work unchanged, and messages are keyed by content so your flags
survive when the spool grows. `$` sync writes changes back into the
file: deleted messages dropped, `Status:`/`X-Status:` headers
rewritten (`RO`/`AF`), mboxrd `>From` quoting preserved, all under an
exclusive flock, with a crash backup kept in the cache until the
rewrite lands, and refuses (rather than clobbers) when the spool
changed since the last look; check for new mail (`G`) and sync again.
New deliveries are picked up by the regular poll.
## PGP
PGP messages are handled on view by shelling out to gpg(1):
PGP/MIME (RFC 3156) and inline/clearsigned messages are decrypted
and/or verified, with a `[-- PGP: ... --]` verdict line at the top of
the pager (good/BAD/unverified signature). Outgoing mail is treated
per message: in the compose menu, `p` opens the security menu with
(e)ncrypt, (s)ign, (b)oth, (c)lear, and the chosen state shows in the
menu's Security line. Signing uses `sign_key` (or gpg's default key); encryption
looks keys up by recipient address and always encrypts to the sender
too, so the Fcc copy stays readable. A reply can inherit the
original's protection: `reply_sign` signs replies to signed mail,
`reply_encrypt` encrypts replies to encrypted mail, `reply_sign_encrypted`
signs replies to encrypted mail too (mutt's `$crypt_replysign` /
`$crypt_replyencrypt` / `$crypt_replysignencrypted`, all off by
default). Detection reads the original's MIME type only; it never
decrypts to decide. Passphrases are gpg-agent's
business; rmut never sees them.
## Sending mail
Drafts open in `$VISUAL`/`$EDITOR` (default `vi`). If an account with
`smtp_host` applies (the open mailbox's account, or the first one
configured), the message goes out via SMTP submission (STARTTLS on
587, implicit TLS on 465, AUTH PLAIN/LOGIN, Bcc stripped from the wire
copy) and the Fcc lands in the account's `sent_folder` by IMAP
APPEND. Otherwise it is handed to `sendmail -t -oi`; setting
`$RMUT_SENDMAIL` or `mail.sendmail` forces the sendmail path. `From:`
defaults to the identity in effect (`[identity]` overlaid by the open
account's `identity` and any matching `[[identities]]` rules, with
`reverse_name` picking the address a replied-to message came to), or
falls back to `$EMAIL` / `user@hostname`; the draft's own From line
always wins, and rmut prefills it whenever an identity applies. In
the compose menu, `P` postpones the draft into a nearby Drafts maildir
(or `.rmut-postponed`); the next `m` offers to recall it. Sent mail is
copied to a nearby Sent maildir when one exists (local mailboxes).
Aliases are read from `[mail] alias_file` (mutt's own setting),
`$RMUT_ALIASES`, or `~/.config/rmut/aliases`, one mutt-style
`alias nick address...` per line; `a` in the index appends to the same
file. At the To prompt (compose
and bounce), **Tab** completes the word under the cursor: alias nicks
by prefix, plus hits from `query_command` when one is configured
(mutt's protocol: `%s` is the search word, the first output line is a
message, then `address<TAB>name` lines). Repeated Tab cycles through
multiple matches.
A reply quotes the original under mutt's `$attribution` line, one
`$indent_string` per line, and a forward takes its subject from
`$forward_format`. All three are format strings over the message being
answered (`%a` address, `%n` name, `%f` the whole From header, `%s`
subject, `%i` message-id, `%d` date, `%{...}` strftime), and the
defaults are mutt's:
```toml
[mail]
attribution = "On %d, %n wrote:" # the quoted reply's opening line
indent_string = "> " # what each quoted line starts with
reply_regexp = "^(re)(\\[[0-9]+\\])*:[ \\t]*" # what a reply subject
# may already start with
forward_format = "[%a: %s]" # the subject a forward carries
include = "ask-yes" # quote the original: yes/no/ask-*
ask_cc = false # mutt's $askcc, between To and
ask_bcc = false # Subject; $askbcc follows it
forward_quote = false # true indents the forwarded text
```
Every draft rmut starts can end with a signature (mutt's `$signature`
and `$sig_dashes`), and the two questions mutt asks on the way in are
settable (`$abort_nosubject`, `$abort_unmodified`):
```toml
[mail]
signature = "~/.signature" # a file, or a command when it ends in |
sig_dashes = true # the "-- " line above it (the default)
sig_on_top = false # true puts the signature above the quote
hostname = "mail.example.net" # the Message-ID host (mutt's $hostname)
user_agent = false # true adds a User-Agent: rmut/... header
abort_nosubject = "ask-yes" # empty subject: yes/no/ask-yes/ask-no
abort_unmodified = true # a first edit that changed nothing is
# not a message: the draft is dropped
```
The signature is read afresh for every draft, so `signature =
headers come from the prompts, and attachments are added with `a` at
the compose menu. The menu has mutt's heavier functions too: `A`
attaches the tagged messages of the open mailbox (the one under the
cursor when none is tagged) as message/rfc822, `n` makes a new file
of a given Content-Type and opens it in the editor, Ctrl+O renames a
file for sending, `u` marks it to be unlinked after the send, Ctrl+D
toggles inline/attachment, `K`/`J` move it up and down, `w` writes
the message as it stands to a mailbox without sending, `i` runs
`$ispell` over the draft, `V` views a file through its mailcap entry
and Esc+v as text. With `edit_headers = true` the draft's header block
is part of the editor buffer, where you can adjust To/Cc/Subject
directly and attach files with `Attach:` pseudo-headers:
```
To: jane@example.com
Subject: the report
Attach: ~/report.pdf the Q2 numbers
Attach: "/tmp/two words.png"
Attach: /tmp/build.log text/plain @name="log.txt" @inline @unlink notes
see attached
```
Each one becomes a base64 part of a multipart/mixed message (content
type guessed from the extension, the rest of the line an optional
description). After the editor you land in mutt's **compose menu**:
the draft's From/To/Cc/Bcc/Subject/Fcc/Security above the attachment
table (body, forwarded original, every `Attach:` file with size and
type). `y` sends, `e` reopens the editor, Enter views the selected
entry (text directly, other types via `[filters]`), `t`/`c`/`b`/`s`
edit the headers, `F`/`r` edit the From and Reply-To,
`a` attaches without a trip through the editor, `D` detaches
the selected file, `p` opens the security menu, `P` postpones, and
`q` asks "Postpone this message?" (no discards). `$postpone` sets that
question (yes/no skip it, ask-yes/ask-no pick the default). PGP signing and encryption wrap the whole multipart, attachments
included; this also works for forwards with `forward = "attach"`.
With several postponed drafts, recalling (`m`, then `r`) opens a
picker instead of silently taking the newest. `$recall` controls the
offer: `no` never asks (always a new message), `yes` recalls the
newest outright, the default asks.
## Enter-command
`:` opens mutt's command prompt (the same line editor as the other
prompts, with its own history) and applies one config line to the
running session. Nothing is written back to the config file, so it is
a place to try a setting before keeping it.
```
:set index_format="%4C %Z %{%b %d} %-15.15L (%?l?%4l&%4c?) %s"
:set nobeep # also: set beep, unset beep, toggle beep
:reset pager_context # back to the default (unset goes to nothing)
:set invtilde # mutt's inv prefix toggles
:set pager_index_lines=6 pager_context=2
:set sort? # report a value instead of setting it
:bind index \Cd delete-message # mutt keys and function names
:macro pager S "s=archive<enter>"
:color index brightyellow default ~F
:uncolor index ~F # and uncolor index *, uncolor status, ...
:mono index bold ~N # an attribute instead of a colour pair;
# unmono takes it back
:unhook folder-hook # drop every hook of a type, or unhook *
:unmailboxes ~/Mail/old # stop watching one, or unmailboxes *
:unalias bob # out of the alias file, or unalias *
:ignore x-spam-score # and unignore, to bring one back
:alternates 'jane@old\.example\.com' # and unalternates (* clears)
:my_hdr Organization: Acme # and unmy_hdr Organization (* clears)
:alias jane Jane Doe <jane@example.com>
:push "<enter>" # keys into the input queue
:exec sync # run one function now
```
Settable at runtime: `index_format`, `date_format`, `sort`,
`sort_aux`, `pager_format`, `pager_index_lines`, `pager_context`,
`quote_regexp`, `wrap`, `tilde`, `status_format`, `theme`, `beep`,
`from`, `realname`, `reverse_name`, `edit_headers`, `fast_reply`,
`autoedit`, `copy`, `forward`/`mime_forward`, `sendmail`, `editor`,
`print_command`, `query_command`, `trash`, `record`, `postponed`,
`new_mail_command`, `mail_check`, `undo_send`, `metoo`, `text_flowed`,
`attribution`, `indent_string`, `forward_format`, `include`, `askcc`,
`askbcc`, `connect_timeout`, `net_timeout`, `pager_stop`, `markers`,
`smart_wrap`, `collapse_unread`, `uncollapse_jump`, `quit`,
`confirmappend`, `save_name`, `force_name`, `forward_quote`,
`signature`, `sig_dashes`, `abort_nosubject`, `abort_unmodified`,
`mark_old`, `print`, `beep_new`, `wait_key`, `reverse_realname`,
`delete_untag`, `flag_safe`, `maildir_trash`, `mail_check_recent`,
`check_new`, `uncollapse_new`, `menu_scroll`, `menu_context`,
`menu_move_off`, `help`, `error_history`, `sort_browser`,
`sort_alias`, `shell`, `tmpdir`, `ispell`,
`reflow_text`, `notmuch`, `sidebar_visible`,
`sidebar_width`, `pgp_sign_as`, `crypt_autosign`, `crypt_autoencrypt`.
An unknown option, a bad number, an unbindable key, or an unknown
function reports on the bottom line in the error color and stops the
rest of the line. Changed settings recompile in place: colors, key
tables, the quote regexp, header rules, and a resort when the sort
order moved.
## Command line
```
rmut [-R] [-e CMD]... [-p|-y] [-z|-Z] [-f MAILBOX | MAILBOX | mailto:URL]
rmut -s SUBJECT [-c CC] [-b BCC] [-a FILE]... [-i FILE] -- ADDRESS...
rmut --import-muttrc [-w] [MUTTRC]
```
`-R` read-only, `-f` the mailbox to open, `-p` the postponed picker,
`-y` the mailbox list, `-z`/`-Z` exit 1 instead of starting when the
mailbox is empty or has no new mail (for a prompt or a cron job), `-e`
runs an enter-command line before the first draw (repeatable).
A `mailto:` URL opens a prefilled draft (to, cc, bcc, subject, body,
percent-decoded) with the mailbox still open behind it, which is what
a desktop mail handler passes:
```
rmut 'mailto:jane@example.com?subject=Lunch&body=Friday%3F'
```
**Sending without the TUI**, for scripts and one-shot mail: `-s`,
`-c`, `-b`, `-a` (repeatable), or `-i`, or a bare `--`, puts rmut in
send mode. The body is stdin (or `-i FILE`), the recipients are the
remaining arguments, the identity and transport come from the config,
and the exit code says whether the message went out:
```
rmut -s "nightly build" -a build.log -- ops@example.com < report.txt
```
The sent copy goes to a local `mail.sent` maildir; a remote Sent
folder is left to the interactive send, since an IMAP APPEND needs the
account opened. A configured `signature` ends a batch message too, as
in mutt.
## Mailing lists
Tell rmut which addresses are lists and it stops guessing:
```toml
[mail]
lists = ["announce@lists.example.com"] # lists you read
subscribed = ["rmut-dev@lists.example.com"] # lists you are on
```
Entries are case-insensitive regexes matched against addresses, and
`subscribed` counts as a known list too. mutt's `lists`, `subscribe`,
`unlists` and `unsubscribe` are imported.
With that, `L` replies to the list alone: the List-Post address when
the list published one, otherwise the known list address from To/Cc.
On a message from no known list it refuses rather than quietly mailing
the author. `~l` limits to list mail, and `%L` in the index format
already shows "To <list>".
Mail going to a known list carries a `Mail-Followup-To` so replies
land on the list (mutt's $followup_to). On a list you are subscribed
to, your own address is left out, since the list copy is the one you
will get; on a list you only read, it stays in. A sender's own
`Mail-Followup-To` is honored by a group reply: it replaces the
recipient set rather than adding to it.
## Who counts as me, and my_hdr
`[identity] email`, the accounts, and the `[[identities]]` rules
already name your addresses. `alternates` adds the rest: an old
domain, a role address, whatever forwards to you.
```toml
[mail]
alternates = ['jane@old\.example\.com', '^(jane|jd)@example\.com$']
my_hdr = ["Organization: Acme", "Bcc: jane@example.com"]
metoo = false # true: a group reply copies you too
```
Entries are case-insensitive regexes over the bare address, mutt's
`alternates`, and one answer serves everywhere rmut asks whether an
address is yours: `~p` and `~P`, the third `%Z` character (`+` sole
recipient, `T` one of several, `C` on the Cc, `F` sent by you, `L` to
a subscribed list), `reverse_name` picking the address a message came
to, and the dedup a group reply does. That dedup is mutt's: replying
to all drops your own addresses and the person already in the To, so
you get no copy of your own mail and nobody gets two. `metoo = true`
keeps you on the list, like mutt's $metoo.
`my_hdr` lines ride on every draft the compose menu, a `mailto:` URL,
or a batch send produces; with `edit_headers` they show in the editor
like any other header. One naming a header rmut already wrote replaces
it, so `my_hdr From:` and `my_hdr Reply-To:` win; `To`, `Cc` and `Bcc`
gain the address instead, so a standing `my_hdr Bcc:` cannot erase a
reply's recipients. There is one entry per header name: a later
`my_hdr` for the same header replaces the earlier one, and `unmy_hdr`
takes it off. mutt's `alternates`, `unalternates`, `my_hdr`,
`unmy_hdr` and `set metoo` are imported, and all of them work at the
`:` prompt.
## format=flowed
A `text/plain; format=flowed` part (RFC 3676) arrives split at
whatever width the sender's terminal happened to be. rmut puts it back
into paragraphs and lets the pager wrap it at your width, so `[pager]
wrap` and the window govern as they do for everything else. Quote
depth bounds a paragraph and survives as `>` marks, so quoted text
still colours and folds; space-stuffing is undone, `DelSp=yes` is
honoured, and `-- ` stays a fixed line. mutt's `$reflow_text` turns it
off:
```toml
[pager]
reflow_text = false # keep the sender's line breaks
[mail]
text_flowed = true # send text/plain; format=flowed
```
`text_flowed` declares the outgoing text part `format=flowed` and
space-stuffs it, in the plain case, under attachments, and inside a
PGP signature or encryption alike. The paragraphs themselves are your
editor's doing, exactly as in mutt: a line that continues has to end
with a space, and rmut adds none of its own.
## Mailbox names: `=` and `+`
Set `[mail] folder` and a mailbox can be named under it, the way mutt
does it:
```toml
[mail]
folder = "~/Mail" # or an account: "imap:work"
mailboxes = ["=inbox", "=lists"]
trash = "=trash"
```
`=x` and `+x` mean `$folder/x`, and `=` alone is the folder itself.
It works wherever a mailbox is named: the change-folder prompt, save
and copy, the compose menu's Fcc, the configured mailboxes, trash,
sent, postponed and the fcc-hook targets. It also works in a macro,
which is what lets an imported mutt line like
`macro index S "<save-message>=archive<enter>"` land where it means
to. Tab completion still works on real paths only, so complete first
or type the shorthand whole.
With `folder = "imap:work"`, `=Archive` is that account's Archive
folder.
## Credentials
An account's password can sit in the config as `password = "..."`,
and `--import-muttrc` puts `set imap_pass` there because that is
where mutt had it. It is the weakest of the options: the config is a
plain file in `~/.config`, and anyone who can read it can read your
mail. rmut writes the files it creates at mode 600, and says so at
startup if a config holding a password is readable by anyone else:
```
chmod 600 /home/you/.config/rmut/config.toml (it holds a password and others can read it)
```
Better is to keep the secret somewhere else and name the command that
fetches it. `password_command` runs once per session, and its first
line of output is the password:
```toml
[[accounts]]
name = "work"
user = "jane"
# pick one:
password_command = "pass show mail/work" # pass(1), GPG-backed
password_command = "gpg -q -d ~/.config/rmut/imap.gpg" # a file you encrypted
password_command = "secret-tool lookup service imap user jane" # libsecret
password_command = "cat /media/crypt/mail-pass" # an encrypted volume
```
Anything that prints the password works. A GPG-backed one prompts
once per session and gpg-agent remembers it for a while; a command
that fails takes the connection down with its message, so a locked
store fails closed rather than silently.
For a provider that wants OAuth2 rather than a password, `auth =
"xoauth2"` (or `"oauthbearer"`) with `token_command` runs the token
helper for every connection, since tokens expire.
## Threads
`o t` sorts by threads, and then the thread is a unit you can act on:
Alt+d, Alt+u and Alt+t (mutt's `Esc d`, `Esc u`, `Esc t`) delete,
undelete and tag the whole thread the cursor sits in, Ctrl+D and
Ctrl+U do the same for the subthread (the message under the cursor
and its replies), and Alt+n / Alt+p step to the next and previous
thread. Alt+v folds one thread, Alt+V all of them.
Each of these is one undo step, however many messages hang off it, so
`z` brings a thread back whole. Deleting advances to the next
undeleted message, mutt's `$resolve`, which makes clearing thread
after thread one repeated key; undeleting and tagging stay put.
Tagging follows the cursor, so a second Alt+t untags the thread.
Without thread sort they refuse, as they do in mutt.
Ctrl+R and Alt+r (mutt's `Ctrl+R`, `Esc r`) mark the thread or the
subthread read, `P` jumps to the parent message (root-message is
there too, unbound), and tag-subthread is an action for `:bind`.
Some mail arrives with no In-Reply-To and no References at all: a
notification robot that mints a fresh Message-ID every time, a list
that strips the headers. Nothing but the subject holds those
together, so, like mutt, rmut hangs a thread root whose subject
repeats one already in the mailbox under the message that named it.
The oldest becomes the root, the rest a fan under it, and the tree
ends in a star instead of an arrow where the subject decided the
place. `[index] strict_threads = true` (mutt's `$strict_threads`)
turns it off; `sort_re = false` (mutt's `$sort_re`) widens it from
"Re:" subjects to any equal subject, which groups unrelated mail
sharing a subject like "hi".
Two keys edit the threading itself, since misconfigured mailers
leave replies dangling or bolt a new discussion onto an old one.
`#` (break-thread) takes the In-Reply-To and References off the
message under the cursor, so it and its replies become a thread of
their own; on a message the subject grouping placed, where there are
no headers to take off, it writes an `X-Rmut-Thread: broken` header
that the grouping honours from then on. That last part is rmut's
own: mutt has nowhere to record the break, so its subject pass hangs
the message straight back and the manual's answer is
`$strict_threads`. `&` (link-threads) makes the tagged messages replies to
the one under the cursor, as mutt does, by giving each an In-Reply-To
naming it (and untagging it). Both rewrite the message file in place
and are one undo step each: `z` writes the old headers back. They
work on local maildirs; on IMAP and mbox the real message lives
elsewhere, so they refuse rather than edit a copy.
The patterns know threads too: `~(P)` matches every message in a
thread where some message matches P (`~(~P)`: threads I took part
in), `~<(P)` the messages whose parent matches P (`~<(~P)`: replies
to my mail), `~>(P)` those with a child matching P, `~v` the head of
a folded thread, and `~$` a message with no parent and no children.
Outside thread sort `~(P)` reads as P and the rest are false.
A reply's subject is "Re: " over the original with whatever
`[mail] reply_regexp` matched at its start taken off (mutt's
`$reply_regexp`, default `^(re)(\[[0-9]+\])*:[ \t]*`), so "RE: x" and
"Re[2]: x" both answer as "Re: x"; a locale's prefixes go in as
`^(re|aw|sv):[ \t]*`. It is case-insensitive unless it holds an
uppercase letter, as mutt compiles it.
## Tagged operations
`t` tags a message and `;` hands the tagged set to the next function
(`Tag-` sits on the message line while it waits, as in mutt):
`d`/`u`/`F`/`N`/`t` mark them all, `s`/`C` save or copy them all (one
prompt, one undo step), `|` and `p` pipe or print them concatenated
into a single run of the command (mutt's `$pipe_split` unset, the
separator mutt's `$pipe_sep`; `pipe_split`/`print_split` = true run it
once per message instead), and
`b` bounces them all to the same addresses, with the confirmation
counting what it is about to do.
A function that cannot take a set says so ("resend does not take the
tagged set") rather than quietly acting on the one under the
cursor. `T` and Ctrl+T tag and untag by pattern; `;t` clears the tags,
the way it does in mutt.
## The attachment reminder
Set `[mail] abort_noattach` and a draft whose body mentions an
attachment when none is attached gets a question before it goes:
```toml
[mail]
abort_noattach = "ask" # "no" (default), "ask", or "yes" (refuse)
attach_keyword = '\b(attach|attached|attachment)\b' # the default
```
Answering `n` puts you back in the compose menu, where `a` attaches
the file you meant. Quoted lines and anything below a `-- ` signature
do not count, so a reply to someone else's "see attached" and a
signature advertising an attachment opener are not false alarms.
neomutt's `abort_noattach` and `abort_noattach_regex` import
(`ask-yes` and `ask-no` both become `ask`), and mutt's `\<` / `\>`
word edges are translated to `\b` on the way in. Batch sends do not
ask: there is no terminal to answer at.
## Undo
`z` walks back the last change to your messages: a delete or
undelete, a flag or read toggle, a tag, a `D`/`U`/`T`/Ctrl+T pattern
sweep, or a save or copy to another mailbox. One keystroke is one
step however many messages it touched, so a pattern delete over three
hundred messages comes back in one go, and a save's copy in the
target mailbox is removed again along with the original's delete
mark.
A step remembers the messages as they stood before it, and puts that
state back. Writing the mailbox ends what can be undone: `$` (and the
write on quit) drops the stack, because those changes are on disk and
the deleted ones are gone. Up to 32 steps are kept.
This one is rmut's own; mutt has nothing like it. It is cheap here
because rmut already defers every mark to the sync.
### Undo send
`[mail] undo_send` holds a sent message for that many seconds before
anything leaves the machine:
```toml
[mail]
undo_send = 10 # 0 (the default) sends at once, as mutt does
```
The status line counts the seconds down, and `z` takes the message
back: not just cancelled, but returned to its compose menu with the
draft as you left it, ready to edit and send again. A held message is
the most recent thing you did, so `z` reaches it before it reaches
the mark history.
The timer running out sends it, and so does leaving rmut: quitting is
not cancelling. Batch sends (`-s` and friends) never hold, since
there is no terminal to press `z` at.
### Rescuing one message from a run of deletions
Deleted messages stay in the index (they only go on `$`), so `j`/`k`
land on them and `u` puts one back. When there are many, three things
help: `U <pattern>` undeletes a whole set (`U ~f boss`), `l ~D` limits
the view to the marked ones, and a pattern search hops between them:
`/~D` then `n`, with Alt+/ to go the other way. The two macros above
put that hop on `.` and `,`.
In the pager, `j`/`k` step over deleted messages by design (mutt does
the same); `J`/`K` step to any message, deleted ones included, and
`u` puts one back without going out to the index.
## Which part shows, and mailcap
A `multipart/alternative` message carries the same text twice or more.
mutt's `alternative_order` decides which copy you read, most wanted
type first; `text/*` matches a whole main type, and anything not
listed falls back to rmut's ranking (a part with a filter, then
enriched over plain over html):
```toml
[pager]
alternative_order = ["text/plain", "text/html"]
```
`[filters]` is mutt's `auto_view`: a MIME type and the command that
turns it into text on stdout. Leave the command empty and rmut takes
it from your mailcap, exactly where mutt takes it from: the first
`copiousoutput` entry for the type, skipping one whose `test=` fails
or that wants the terminal, with `%s` given a temporary file. The
files are `$MAILCAPS`, or `~/.mailcap`, `/etc/mailcap`,
`/usr/etc/mailcap`, `/usr/local/etc/mailcap`. A type with no such
entry simply does not autoview, and the part stays an attachment.
`auto_view`, `unauto_view`, `alternative_order` and
`unalternative_order` all work at the `:` prompt as well, and import
from a muttrc.
## Hooks
Beyond `[[identities]]` (the from/realname half of folder-hook and
send-hook), four mutt hooks have tables of their own. The three that
carry a command line take exactly what the `:` prompt takes.
```toml
[[folder_hooks]] # on opening a matching mailbox
folder = "*work*" # glob on the mailbox, path or imap: spec
command = 'set index_format="%4C %Z %-6d %-20.20F %s"'
[[message_hooks]] # while that message is selected
pattern = "~f boss@example.com"
command = "set pager_context=5"
[[reply_hooks]] # while a reply to it is built
pattern = "~f boss@example.com"
command = "set from=jane@work.example.com"
[[fcc_hooks]] # where the sent copy goes
pattern = '~t @work\.example\.com'
mailbox = "~/Maildir/.WorkSent"
[[crypt_hooks]] # encrypt to this key for this recipient
address = "boss@example.com"
key = "0xDEADBEEF"
```
A **message-hook** is in force only while its message is the selected
one: the moment the match set changes, every setting it touched goes
back to what it was, so a display setting really is per-message. A
**reply-hook** applies while the reply's draft is built, which covers
`set from`, `edit_headers` and `my_hdr`. A **folder-hook** is not
undone when you leave, exactly like mutt, so a catch-all entry
(`folder = "*"`) is how you put a setting back.
**fcc-hook** patterns match the draft as it stands after the editor,
so the compose menu's Fcc line already shows where the copy is going;
an Fcc chosen by hand with `f` still wins, and batch sends honour the
hook too. Bcc addresses join the Cc ones for matching, so `~c` sees a
blind recipient. **crypt-hook** replaces a recipient's address with a
key id when gpg is asked to encrypt.
## Configuration
`$RMUT_CONFIG` or `~/.config/rmut/config.toml`:
```toml
[identity]
name = "Jane Doe" # From: Jane Doe <jane@example.com>
email = "jane@example.com"
reverse_name = false # true: a reply's From becomes whichever
# of your addresses the mail was sent to
reverse_realname = true # false: only the address comes over, the
# name above stays (mutt's setting)
[[identities]] # conditional identity (folder-/send-hook):
folder = "*work*" # glob on the open mailbox, and/or
recipient = "*@work.example.com" # glob on a draft recipient;
name = "Jane Work" # matching rules overlay [identity] in
email = "jane@work.example.com" # order, unset fields fall through
[mail]
folder = "~/Maildir" # mutt's $folder: "=x" and "+x" name a
# mailbox under it, at a prompt or in a
# macro ("imap:work" works too)
mailboxes = ["~/Maildir"] # default mailbox + folder browser entries;
# local ones are watched for new mail
# ("new mail in ..." in the status line)
sent = "~/Maildir/.Sent" # Fcc target (else a nearby Sent is used)
postponed = "~/Maildir/.Drafts"
sendmail = "/usr/sbin/sendmail"
editor = "vim"
poll_seconds = 5 # new-mail check interval
print = "lpr" # `p` pipes the message here
save = "~/Maildir/.Archive" # default target for `s`
forward = "inline" # or "attach" (original as message/rfc822)
query_command = "khard email --parsable %s" # Tab completion lookup
trash = "~/Maildir/.Trash" # purged mail moves here (mutt's $trash;
# imap:acct/Trash for IMAP mailboxes);
# purging inside it deletes for real
edit_headers = false # true: the header block is part of the
# editor buffer (To/Cc/Subject, Attach:)
alternates = ['jane@old\.example\.com'] # my other addresses
my_hdr = ["Organization: Acme"] # on every draft
metoo = false # true: a group reply copies me too
text_flowed = false # true: send text/plain; format=flowed
undo_send = 0 # seconds a sent message waits, so z can
# take it back (0 sends at once)
delete = "ask" # mutt's $delete: "yes" purges without
# asking, "no" keeps the marks
abort_noattach = "no" # "ask"/"yes": a body that mentions an
# attachment with none attached is
# questioned before it goes
signature = "~/.signature" # ends every draft; a name ending in |
# is a command whose output it is
sig_dashes = true # the "-- " line above the signature
forward_quote = false # true: the forwarded text comes in
# quoted with indent_string
abort_nosubject = "ask-yes" # empty subject: "yes" aborts without
# asking, "no" never asks
abort_unmodified = true # false: keep a draft the first editor
# pass left untouched
mark_old = true # unread mail ages to old (O) when you
# leave the mailbox, as in mutt
delete_untag = true # deleting a tagged message untags it
flag_safe = false # true: a flagged message cannot be
# deleted (mutt's $flag_safe)
maildir_trash = false # true: a purge writes the maildir T flag
# instead of unlinking (maildir only)
mail_check_recent = true # false announces any mailbox holding new
# mail, once, not only one that grew
check_new = true # false stops rescanning the open maildir
sort_alias = "address" # address completion order: address (the
# default), alias (by nick); reverse-
shell = "/bin/zsh" # what a bare ! runs ($SHELL, then sh)
tmpdir = "~/tmp" # where temporary files go ($TMPDIR, /tmp)
ispell = "aspell" # the compose menu's i: `ispell -x FILE`
print_confirm = "ask-no" # mutt's $print: "ask-yes" makes Enter
# print, "yes" never asks, "no" refuses
[index]
format = "%4C %Z %-6d %-15.15L (%?l?%4l&%4c?) %s" # mutt's default
# %Z status/flag/mark, where
# the mark is mutt's to_chars:
# + sole recipient, T one of
# several, C on the Cc, F sent
# by me, L to a subscribed list
# %F from (%L: "To <list>" for
# List-Id mail) %c size %l body
# lines %M collapsed count
# %s subject, and
# %?X?then&else? conditionals
sort = "threads" # initial sort; reverse-date, size, ...
sort_aux = "last-date-sent" # which thread comes first: last- by its
# newest message, reverse- newest thread
# first (mutt's spellings)
date_format = "%d.%m.%Y" # strftime for the date column
hide_thread_subject = false # true blanks a thread reply's subject
# when it repeats the parent's (mutt's)
uncollapse_new = true # a folded thread unfolds when it grows
[pager]
index_lines = 10 # keep a slice of the index above the pager
context = 3 # overlapping lines when paging
search_context = 0 # lines kept above a search hit (mutt's)
reflow_text = true # false: keep a format=flowed part's own
# line breaks instead of rewrapping it
alternative_order = ["text/plain", "text/html"]
# which part of a multipart/alternative
# shows, most wanted first ("text/*" ok)
[filters] # auto_view: render a part via a command
"text/html" = "w3m -dump -T text/html -O UTF-8"
"text/calendar" = "" # empty: take the command from mailcap
[net] # how long the network gets before rmut
connect_timeout = 10 # says so; 0 waits as long as the OS
timeout = 30 # does (about two minutes). timeout is
# data on a live connection, never off:
# IMAP IDLE ticks on it, minimum 5
system_cas = true # trust the OS cert store too (mutt's
# $ssl_usesystemcerts); adds to the roots
certificate_file = "~/.mutt/certs.pem" # a PEM of extra roots to
# trust (a private/self-signed CA)
[ui]
theme = "default" # or "mono"
status_format = "---rmut: %f [Msgs:%?M?%M/?%m New:%n%?d? Del:%d?] (sort:%s)%?V? (limit:%V)?"
# bottom line: %f mailbox %m msgs
# %M shown-when-limited %n new %u unread
# %d deleted %F flagged %t tagged %s sort
# %V limit %r pending-mark %v version,
# with %?X?then&else? conditionals
beep = true # ring the bell on an error (mutt's $beep)
beep_new = false # true: ring when mail arrives, too
wait_key = true # a shell escape (!) ends with "Press
# Enter", so its output can be read
set_title = false # true sets the terminal title (mutt's
# $ts_enabled)
title_format = "rmut: %f" # its format, the status specifiers again
history_file = "~/.rmut_history" # persist prompt history across
# sessions (mutt's $history_file); unset
# keeps it in-memory only
status_on_top = false # true puts the status bar near the top
arrow_cursor = false # true marks the selection with -> not
# reverse video (mutt's $arrow_cursor)
menu_scroll = true # false turns a page when the cursor
# leaves the screen (mutt's default)
menu_context = 0 # lines kept in view past the cursor
menu_move_off = true # false keeps the last message on the
# bottom row once the index fills
help = true # false drops the key-help top line
error_history = 30 # what error-history shows; 0 disables
sort_browser = "alpha" # folder browser order: alpha, count,
# unread, date, unsorted; reverse- flips
status_chars = "-*%" # mutt's $status_chars: %r marker for
# unchanged/changed/read-only (unset keeps
# rmut's nothing/*/%)
[[color_index]] # mutt's `color index FG BG PATTERN`:
pattern = "~f boss@example.com" # any limit/search pattern; first
fg = "yellow" # matching rule colors the index line,
# bg = "blue" # over the [colors] slots below
[sidebar] # left pane: mail.mailboxes with new-mail
visible = false # counts (B toggles at runtime; bold =
width = 24 # has new mail, > marks the open one)
[colors] # status_fg status_bg deleted flagged
deleted = "red" # tagged header. One colour each: for
# mutt's `color index black magenta ~D`
# (a painted bar) use a [[color_index]]
# rule, which takes fg and bg both
[keys.index] # remap: action = "key" (see ? for actions)
sync = "w"
[keys.pager]
[macros.index] # macro: key = "replayed key sequence",
L = "l~f jane<enter>" # literals + <enter>/<esc>/<ctrl+x>/...;
"." = "/~D<enter>" # hop to the next message marked deleted,
"," = "<alt+/>~D<enter>" # and back again
[macros.pager] # it feeds the input queue, so it can
# drive prompts; a macro shadows a
# binding on the same key (like mutt)
[[accounts]] # remote account: open with `rmut imap:work`
name = "work"
user = "jane@example.com"
password_command = "pass show mail/work" # first stdout line
# password = "..." # alternative, but see Credentials below
# auth = "xoauth2" # or "oauthbearer": OAuth2 with
# token_command = "oauth2ms" # a fresh access token per connection
imap_host = "imap.example.com" # imap_port = 993 (implicit TLS; 143 = STARTTLS)
smtp_host = "smtp.example.com" # smtp_port = 587 (STARTTLS; 465 = implicit TLS)
sent_folder = "Sent" # Fcc target via IMAP APPEND
identity = { name = "Jane W", email = "jane@work.example.com" } # From
# when composing from this account
[pgp] # optional; gpg from $PATH by default
command = "gpg"
sign_key = "jane@example.com" # --local-user; gpg's default key if unset
sign_by_default = false # preselect security for new drafts
encrypt_by_default = false
reply_sign = false # a reply to signed mail defaults signed
reply_encrypt = false # a reply to encrypted mail, encrypted
reply_sign_encrypted = false # a reply to encrypted mail, signed too
```
Key syntax: a character, `ctrl+x`, `alt+x`, or enter/esc/space/tab/
backspace/up/down/pgup/pgdn/home/end. `?` lists all actions with their
current keys. An `alt+x` binding also answers to mutt's spelling, Esc
then x: in the index, the pager and the compose menu an Esc waits for
the next key, so Esc / is search-reverse however slowly it is typed,
and Esc Esc is whatever Esc alone is bound to (back, in the pager).
### Coming from mutt
```sh
rmut --import-muttrc -w # reads ~/.muttrc, writes the config
rmut --import-muttrc # or print it, to look first
```
`-w` saves it to `~/.config/rmut/config.toml`, creating the directory
and refusing to overwrite a config that is already there. Any `alias`
lines it found, including ones in a `source`d file, are written to
`~/.config/rmut/aliases` at the same time: rmut keeps aliases in a
mutt-format file of their own rather than in the TOML, so your mutt
alias file works as it stands (point `$RMUT_ALIASES` at it if you
would rather keep it where it is), and `a` in the index appends to
it. Without `-w` the translation goes to stdout for review, aliases
as a comment block, so redirect it yourself if that is what you
want.
The translation covers identity, folder/mailboxes, record/postponed,
sendmail/editor/print_command/query_command/status_format, binds,
status/header colors and `color index FG BG PATTERN` rules, PGP
defaults, IMAP/SMTP URLs into an `[[accounts]]` skeleton, with
`auth`/`token_command` when `*_authenticators` names
oauthbearer/xoauth2, reverse_name, alternates/unalternates,
my_hdr/unmy_hdr, text_flowed/reflow_text,
auto_view/unauto_view (the command left to mailcap),
alternative_order/unalternative_order,
folder-hooks/send-hooks that only set from/realname into
`[[identities]]` rules, every other folder-hook plus message-hook,
reply-hook, fcc-hook/fcc-save-hook and crypt-hook into their own hook
tables, and macros whose sequence is plain keys and
prompt input) into rmut
TOML on stdout for review; it never writes any file itself.
Directives with no rmut equivalent are kept as `# not imported:`
comments, and ones that match rmut's built-in behavior (ssl_starttls,
UTF-8 charset, pgp_auto_decode, ...) are acknowledged under
`# satisfied by rmut's defaults`; `imap_pass`/`smtp_pass` become the
account's stored `password`. Alias files need no
translation: rmut reads mutt-format aliases, so point `$RMUT_ALIASES`
at your existing file or copy it to `~/.config/rmut/aliases`.
## Development
```sh
just test # cargo test --workspace
just lint # clippy -D warnings + fmt --check
just e2e # pty-driven end-to-end tests
just check # test + lint + e2e
```
Tests sit where what they test does. `rmut-core` tests parsing and
protocols, `rmut-session` tests the operations against a maildir in a
tempdir and asserts on values (`crates/rmut-session/src/tests.rs`),
and the pty suite in `tests/e2e` drives a real terminal for the keys,
the drawing, and the paths that go all the way out through sendmail,
IMAP and gpg.
Three crates. `rmut-core` is the mail itself: maildir, mbox, IMAP,
SMTP, compose, PGP, patterns, threading, the importer. `rmut-session`
is an open mailbox and everything that can be done to it, with no
screen attached: what is in it, what is selected, marks, sync, save
and copy, the undo stack, the outbox, the hooks, and the named
functions a keymap binds. `rmut` (the binary crate) owns the menus, the keys, the
theme and the drawing, and drives a session.
An operation reports what it did as a `notice::Notice` rather than
writing into a status field, and the front end installs the sink that
receives it: the terminal keeps the last notice for its message line,
a test reads the values. A session with no sink installed is silent.
When an operation needs an answer it hands back an `Ask` ("Save to
mailbox: ", "Purge 3 deleted message(s)? (y/n): ") rather than opening
a prompt, and the front end answers with the `AskKind` it came with;
answering can produce the next question. What only a front end can do
comes back as a `Request`: quit, run an editor or a shell command,
suspend, show the draft again. A front end that cannot do one of them
simply does not honour it.
Every index operation has the name a muttrc binds (`delete-message`,
`group-reply`, `tag-prefix`), and the names live with the operations
in `rmut-session`. A front end resolves whatever it has (a keystroke,
a menu item, `:exec`) to a `Function`, hands it to
`Session::run_function`, and reads the `Outcome`: done, a question to
put, or a `FrontOp` naming the one thing it has to do itself. The key
tables stay in the front end, since only it knows what a key is. See
[docs/PLAN.md](docs/PLAN.md) for where that line is headed.