Skip to content
TildeMailv0.2.1

Commands

This page describes how tmail starts the terminal interface and each subcommand: what it does, what it prints and its exit codes. The interface itself is described in the interface.

tmail --help lists the commands with one line each, and tmail <command> --help lists the arguments and options of one command. This page has everything else.

tmail opens the terminal interface (help when input or output is not a terminal)
tmail --help help only
tmail --version prints "~mail <version> - tom (tom@orivega.io) - https://codeberg.org/squidd/tildemail"
tmail check validates config.toml
tmail completion <shell> prints the completion script of bash, zsh or fish
tmail man [config] prints the manual page of tmail, or of config.toml, in roff
tmail cache status what each cache holds
tmail cache clear [name] empties the cache of one account, or of all
tmail cache clear --orphans
tmail gpg fetch <email> [--fingerprint fpr]
tmail rules run <account> [folder] [--limit N] [--dry-run] [--config path]
tmail send <account> --to a,b --subject S [--cc a,b] [--from a] [--body-file F] [--reply UID --folder F] [--config path]
tmail themes list [--config path]
tmail themes show <name>
tmail bar dump
tmail watch [--config path]

Every subcommand except tmail themes show, tmail bar dump, tmail gpg fetch, tmail completion and tmail man takes --config <path> to read a file other than ~/.config/tildemail/config.toml.

Only tmail rules run, tmail send and tmail watch connect to a server, resolve username and password, or run a #{command}. The other commands never do. tmail gpg fetch is the one command that contacts other servers: it makes gpg ask a web key directory host and a key server (see below).

tmail without arguments opens the terminal interface, which shows the mail of the accounts of config.toml and reopens the last folder you used. It needs a terminal: when the input or the output is redirected, it prints the help and the line The terminal interface needs an interactive terminal., and exits with 0. tmail --help and tmail --version are never the interface.

The interface reads ~/.config/tildemail/config.toml; it takes no --config option yet. It takes the services lock (see One tmail at a time) and runs the Lua engine, the hooks, the rules and the jobs while it is open, as tmail watch does. If another tmail already runs them, the interface opens in reading mode and says so on the status line.

Exit codes: 0 after a normal close, including SIGTERM and SIGHUP; 1 when config.toml cannot be read or the interface fails. If the terminal goes away (a dropped connection, a closed window) and the screen does not close within five seconds, the process ends with 1.

If config.toml is missing or invalid the command prints the reason and exits with 1 before it draws anything.

tmail check loads config.toml, prints one line per account and exits with 0:

personal me@example.org imap imap.example.org:993 smtp smtp.example.org:587 gpg key yes
work me@corp.example imap mail.corp.example:993 smtp mail.corp.example:587 gpg key no
local me@home.example maildir /home/me/Mail/home smtp smtp.home.example:587 gpg key no

The name, address and hosts it prints are the expanded ones: ${VAR} is replaced in every string value except the credentials.

It prints these warnings on stderr; they do not change the exit code:

  • the file is accessible by group or others (chmod 600 fixes it);
  • warning: unknown key 'theme' in cache (...) or warning: unknown table '...', one line for each key or table tmail does not read;
  • for a Maildir account, the path is not a directory.

For a missing, unreadable or invalid file, or a ${VAR} that is not defined, it prints the error to stderr and exits with 1.

tmail cache status prints one line per account: its name, the number of headers (summaries), the number and size of the stored messages (raw), and the size of the database file.

personal summaries 0 raw messages 0 raw 0 B file 0 B
work summaries 0 raw messages 0 raw 0 B file 0 B
  • An account without a database shows zeros. An account that reads a Maildir shows no cache: the account reads a Maildir.
  • A database it cannot read, because it is corrupt or was written by another version of tmail, is reported on stderr and left as it is, and the exit code is 1. tmail recreates it the next time it uses the account.
  • A database in the cache directory that belongs to no cached account of the configuration file is listed as orphan: <file name> with its size, followed by a hint to remove the orphans with tmail cache clear --orphans. A database becomes an orphan when its account was renamed, removed or switched to a Maildir, or when it belongs to another configuration file.

The command never creates, recreates or deletes a cache database. Its only write is one SQLite makes on its own: when it finds the write-ahead log of a tmail that stopped abruptly, it folds that log into the database and removes it.

tmail cache clear empties the cache of one account, or of every account of the configuration file when no name is given. It prints what it removed, for example personal cleared 120 summaries, 8 raw messages (1.2 MB), or nothing to clear. A Maildir account gets <name> nothing to clear: the account reads a Maildir.

  • It asks for no confirmation: the cache is only a copy and is rebuilt as you use tmail.
  • Account names are compared without regard to case. An unknown name is an error (exit 1).
  • While another tmail process has the cache open and is writing to it, clear waits up to 5 seconds, then says the cache is in use and exits with 1.
  • It never deletes an orphan database.

tmail cache clear --orphans deletes the orphan databases that status lists, with their -wal and -shm files, and nothing else. It takes no account name.

  • Orphans are decided against the configuration file passed with --config, or the default one. With another configuration file, the caches of the default one are orphans too, so check status first.
  • A database another tmail process has open is skipped with in use, skipped: <file>, and the exit code is 1.
  • Only regular files named like tmail’s cache databases are touched. Symbolic links, directories and other files stay.

tmail gpg fetch <email> [--fingerprint <fingerprint>] looks public keys up for an e-mail address and imports the one you choose. It does not read config.toml, so it works before you have set anything up, and it runs only when you run it: nothing in tmail calls it on its own.

It runs gpg, so your keyring and your gpg.conf and dirmngr.conf apply. For the address it asks, in this order, the web key directory (WKD) of the address’s domain and the key server named in dirmngr.conf (or dirmngr’s default, hkps://keys.openpgp.org; a key server set only in gpg.conf is not used). Both servers learn which address you asked for. The keys are first downloaded into a temporary keyring, so a lookup never changes yours. Only keys with a user id for exactly that address are listed; expired and revoked keys are listed too, flagged.

Keys found for bob@example.org:
1) A604 C9AB 4872 5BC3 2FF1 F350 000B 7F53 C2C1 EE24
User id: Bob <bob@example.org>
Created: 2024-03-05
Expires: never
Flags: none
Source: keyserver
2) 0123 4567 89AB CDEF 0123 4567 89AB CDEF 0123 4567
User id: Bob <bob@example.org>
Created: 2019-01-17
Expires: 2021-01-17
Flags: expired
Source: wkd
Import which key? [1-2, n=none] 1
Imported key A604 C9AB 4872 5BC3 2FF1 F350 000B 7F53 C2C1 EE24
This key is NOT trusted: check the fingerprint with its owner and certify it with gpg before encrypting to it.
  • Choosing. It asks Import which key? [1-N, n=none] and reads one line. A number imports that key. n, an empty line or the end of the input imports nothing and exits with 0. Anything else imports nothing and exits with 1; it does not ask again. A key is never imported without a choice.
  • --fingerprint. --fingerprint <fingerprint> imports that key without asking, if the search returned it. It takes the full fingerprint, 40 hex digits (or 64), in any case, with or without spaces between groups (ordinary spaces only: a tab, a line break or a no-break space is a usage error); a key id, a short fingerprint or a 0x prefix is a usage error. A fingerprint that the search did not return is a failure: nothing is imported.
  • Without a terminal. When standard input is not a terminal and --fingerprint is missing, it prints the keys, says why it did not ask and exits with 1, importing nothing. Use --fingerprint in scripts.
  • The import. The key is downloaded again into a temporary keyring and imported only if its fingerprint is exactly the one you chose, the data to import holds that one key and no other (another key can carry the same fingerprint as a subkey; such data is refused), it is not over 1 MiB, and then without other people’s signatures. A key found through WKD can be imported because the address is passed along with the fingerprint. Each import writes one log line with the fingerprint.
  • Trust. The imported key is not trusted and not certified. Check the fingerprint with its owner, by a channel other than the one the key came through, and certify it with gpg --sign-key before encrypting to it. If your gpg.conf sets trust-model always, gpg encrypts to any key in the keyring, so an imported key can be used for encryption at once: the import keeps your gpg options.
  • Key server results. A key server decides the order of its index and only the first 10 entries are looked at, so it can push the real key out of the results; keys.openpgp.org only returns user ids whose e-mail address was verified.
  • Output. Everything from gpg or the network, such as user ids and error messages, is printed without control characters.

Exit codes: 0 when a key was imported, or when you answered n. 1 for an address that is not a plain e-mail address (bob@example.org, without a name or angle brackets; the search reports why), a bad fingerprint or answer, no key found, a fingerprint the search did not return, no terminal to ask on, a lookup or import that failed (gpg missing, network or key server error, timeout), or when it is cancelled with Ctrl+C. Cancelling imports nothing; the command line framework may end the command itself with 130 instead, for example while it waits at the prompt, which can take a few seconds.

tmail rules run applies the rules that init.lua defines (see Rules) to the newest messages of a folder. Try a dry run first:

Terminal window
tmail rules run personal INBOX --dry-run
[dry-run] 5 rules-e2e hello -> t: tag vista, mark read, create folder RulesE2E, move to RulesE2E
1 of 4 messages matched

In this example the folder RulesE2E does not exist yet. Each matched message gets one line, uid subject -> rules: steps, and the summary counts the matched messages and the messages scanned (1 of 1 message matched when only one was scanned). The subject has control, line-separator and invisible format characters (such as the bidirectional overrides and zero-width characters) replaced by spaces, and is cut to 80 characters, ending in ....

Its help text is:

Applies the rules that init.lua defines (rules.on) to the newest messages of a folder, or with --dry-run only
lists what they would do. It prints one line per matched message, `uid subject -> rules: steps`, and ends
with `n of m messages matched`. It needs a reachable account; with no rules defined it says so and does not
connect. The exit code is 1 when the account, the script or the server fails, or when a step fails.
Arguments:
[0] <string> Name of the account to run the rules on.
[1] <string?> Folder to scan. INBOX when omitted.
Options:
--limit <int> How many of the newest messages to look at, from 1 to 5000. [Default: 200]
--dry-run Change nothing on the server: print what the rules would do.
--config <string?> Path of config.toml. Defaults to ~/.config/tildemail/config.toml. [Default: null]

Arguments and options:

  • The folder defaults to INBOX.
  • --limit defaults to 200 and must be from 1 to 5000; otherwise the command prints --limit must be between 1 and 5000 and exits with 1.
  • The account name is compared without regard to case. An unknown name prints unknown account '<name>' and exits with 1.
  • With --dry-run nothing changes on the server: no keyword, flag, folder or move is set, and every line starts with [dry-run] .

Behaviour:

  • The account must be reachable, unless no rule is defined. With no rule defined, the command prints no rules defined in <scripts dir>, exits with 0 and does not connect.
  • Headers are fetched from the server only when a rule uses to or header; from and subject come from the message list. To read the headers tmail downloads the whole message, in a dry run too. With the cache enabled this fills the local cache with those messages, a dry run included; a dry run still never changes the server.
  • If the server cannot be reached and the cache answers, the results come from the cache. The command prints them, then server unreachable, results come from the local cache; nothing was changed on stderr, and exits with 1.
  • A step that fails is printed on stderr as uid: step: error, the run goes on, and the exit code is 1. A real run prints the same line shape as a dry run, with the steps that were done.

Exit codes: 0 when the run finishes and every step worked. 1 for an unreadable configuration, an unknown account, a script error, a server failure, an invalid --limit or a failed step.

tmail send sends one message from the command line, with no editor and no confirmation, so scripts and cron jobs can use it. The body comes from standard input, or from a file:

Terminal window
echo "Lunch at noon?" | tmail send personal --to bob@example.org --subject "Lunch"
tmail send personal --to bob@example.org,ann@example.org --cc carol@example.org --subject "Notes" --body-file notes.txt

The message is built, signed and encrypted exactly as a message written in the editor would be (see Composing and replying): the gpg settings of the sender apply (the account’s, or the identity’s with --from), the sender’s signature is appended and a copy is stored in the sent folder. encrypt = "auto" encrypts only when every recipient has a key; when one lacks it the message goes out in clear, and the reason (not encrypted: no public key for ...) is printed to standard error, with exit code 0.

Option Meaning
<account> The sending account. Required.
--to, --cc Recipients, separated by commas (a JSON array also works). Give --to or --cc unless you use --reply. Write all the addresses in one option: if you repeat it, only the last one counts.
--subject The subject. Required unless you use --reply.
--from The sender: the account’s address or the address of one of its identities. Default: the account’s address.
--body-file Read the body from this file instead of standard input.
--reply <uid> Answer the message with this uid. The recipient and the subject default to the original’s (Re: ...), In-Reply-To and References are set, and the original is quoted under your text. --to, --cc and --subject replace the defaults.
--folder The folder of the message to answer. Default INBOX. Only valid with --reply.

When standard input is a terminal and there is no --body-file, the command stops with an error instead of waiting for you to type. A body that is empty or only whitespace is refused (so a cron job whose input is /dev/null sends nothing), except with --reply, where the quote is the message.

On success it prints sent to <recipients>.

  • 0 when the message was sent and a copy was stored.
  • 1 when nothing was sent, or when you cancelled it (Ctrl+C; if the server had the message already, it may have been sent): bad usage, an unknown account or sender, an invalid address, a missing body file, a failed GPG step, or the server refused the message. The reason goes to standard error.
  • 3 when the message was sent but its copy could not be stored in the sent folder. Do not send it again: the reason goes to standard error.

tmail themes list prints one line per theme: the built-in default first, then the files of the themes folders by name (your own folder and, if the package installed one, /usr/share/tildemail/themes/), then the skipped files.

default
broken (invalid: unknown key colors.acent.)
* nord
bad name.toml (skipped: invalid theme name)
default.toml (skipped: reserved name)
  • The active theme is marked with *. It is the one theme names in config.toml if its file is valid, and default otherwise.
  • If config.toml does not exist, default is marked. If it exists but cannot be loaded, the list is still printed with no theme marked.
  • A theme with a mistake shows the reason, and a file with an unusable name is listed as skipped.
  • The exit code is 0 even when some themes are invalid, and 1 only when the themes folder exists but cannot be read.

tmail themes show <name> prints the 20 colors of a theme as role #RRGGBB, in the order of the table in Themes. A color that the file does not set, and that comes from default, ends with (default); for top_bar_fg, top_bar_bg and conversation_bg the value shown is the one of default, while the screen draws them from the colors of the theme itself.

background #2E3440
foreground #D8DEE9
muted #7F849C (default)
border #45475A (default)
accent #88C0D0
...

It does not read config.toml and takes no --config. These cases exit with 1:

  • an unknown name prints Theme '<name>' not found in <themes folders>., with your folder and the one of the package, apart by a comma;
  • an invalid file prints its error;
  • an invalid name such as ../x or a/b is refused without reading anything outside the themes folder.

Neither command creates a themes folder, and every name and reason is printed without control characters.

tmail bar dump prints the Lua script that sets the default status line, exactly as it is built into the program. It is the script that runs before your init.lua, so it is the one your own bar.bottom(...) replaces. Redirect it into a file under scripts/, change it, and require it from init.lua, or paste the parts you want into init.lua.

tmail bar dump > ~/.config/tildemail/scripts/bars.lua

It reads no file and no config.toml, takes no --config, and always exits with 0. The default script sets the status line only; the top bar stays hidden until a script sets it.

tmail watch keeps tmail running in the foreground, without an interface, until you stop it. It runs init.lua, starts the job scheduler, and watches the folders that the accounts list under watch. Each new message goes to the Lua new_mail handlers, then to the rules, and is then printed.

A run where a rule tagged a message, marked it read and moved it looks like this:

watching live/INBOX
live/INBOX 36 watch-e2e hello
-> w: tag vista, mark read, create folder WatchE2E, move to WatchE2E

The rule was rules.on("w", { subject = "watch-e2e" }, { tag = "vista", mark_read = true, move = "WatchE2E" }), and an events.on("new_mail", ...) handler wrote to the log, so the handler does not appear on the terminal. The folder is created the first time a message is moved to it.

Its help text is:

Usage: watch [options...] [-h|--help] [--version]
Keeps tmail running in the foreground without the interface: it runs init.lua, starts the job scheduler and
watches the folders that the accounts list under `watch`. Each new message is printed, sent to the Lua
new_mail handlers and then to the rules. It prints `watching account/folder` once it is running and
runs until stopped. Stop it with Ctrl+C, SIGTERM or SIGHUP: it finishes in order and exits. A second
Ctrl+C while it is stopping ends it at once. The exit code is 0 after an orderly stop, and 1 when the
configuration cannot be read, no account lists a watch folder, init.lua fails or a service fails to start.
Options:
--config <string?> Path of config.toml. Defaults to ~/.config/tildemail/config.toml. [Default: null]

An account is watched when its watch key lists folders. Omitting watch means ["INBOX"]. watch = [] opts an account out: it is not watched and is named on the not watching line.

Only one tmail runs the Lua engine, the hooks, the rules, the jobs and the new-mail watcher: tmail watch and the terminal interface. tmail watch takes the lock ~/.config/tildemail/state/services.lock before it reads the configuration and keeps it until the services have stopped. If another tmail holds it, the command prints this to stderr and exits with 1, before it builds anything:

another tmail already runs the Lua engine, hooks, rules, jobs and watcher (lock: /home/you/.config/tildemail/state/services.lock)

The lock belongs to the process: it is released when the command stops, or when the process dies for any reason, so a crash never leaves it taken. The file keeps the pid of the last owner and is not deleted. If the state folder cannot be created or the lock file cannot be opened, the command prints the reason and exits with 1.

  1. The lock is taken before anything else (see above). The configuration is loaded next. If it cannot be read, or no account lists a watch folder, the command prints why and exits with 1 before anything else is built.
  2. The new-mail handler is subscribed.
  3. The services start through the service lifecycle, in priority order: the mail backend factory (which does nothing at start), the event hub, the script engine (which runs init.lua), the job scheduler, and the new-mail watcher for the accounts that have a watch folder.

An error raised by init.lua at start is printed on stderr, and the command exits with 1 before the job scheduler starts and before any server is contacted. A small start guard, which runs right after the script engine, stops the start.

The Lua new_mail handlers run first, then the rules, so a rule sees the message after the handlers have seen it. The message is then printed (see Output).

A failing handler does not stop the rules, and a failing rule does not stop the watcher: the next message is handled as usual. Other Lua events (for example account_error) reach their handlers while the command runs, and the Lua jobs run on the job scheduler.

On stdout:

  • watching live/INBOX once the watcher has started. The folders of several accounts share the line, separated by commas: watching live/INBOX, work/INBOX. The line does not mean that the server connection worked: the watches connect in the background, and a server that cannot be reached is reported through the account_error event and retried.
  • not watching: <names> on a second line, naming the accounts with watch = [].
  • One line per new message, <account>/<folder> <uid> <subject>, with two spaces between the fields. The subject has control, line-separator and invisible format characters replaced by spaces, and is cut to 80 characters, ending in ....
  • When a rule matched the message, -> <rule names>: <steps> directly under that line, with the same steps that tmail rules run lists.

The watcher handles the folders of an account at the same time, so a message line is not printed when the message arrives. It is printed together with its rule line, in one write, after the handlers and the rules have run. As a result:

  • A message always gets its line, even when a handler or a rule fails.
  • The two lines of one message are never split by another message.
  • A slow handler delays the line of its own message only.
  • The lines of different messages appear in the order in which their handling ends.

On stderr:

  • the configuration and init.lua errors;
  • the errors of Lua handlers, as <source>: <message> (the source is the event name, such as new_mail);
  • a failed rule step, as <account>/<folder> <uid>: <step>: <error>;
  • a failing new_mail handler, as <account>/<folder> <uid>: hook failed: <message>;
  • a failure of the rule service itself, as <account>/<folder> <uid>: rules failed: <message>. This line appears only for unexpected failures: the rule service logs and absorbs ordinary backend errors and reports them as failed steps instead.

A write to the terminal that fails (a full disk behind a redirected output, for example) is logged and does not stop the handlers, the rules or the watcher. Passwords and credential commands are never printed.

Ctrl+C, SIGTERM and SIGHUP all start an orderly stop, and the command exits with 0 when it is done. The stop runs in this order:

  1. The new-mail handler is unsubscribed, so no new message is handled from then on.
  2. The service lifecycle stops the services in reverse order: the new-mail watcher (stopped, then disposed), the job scheduler, the script engine (the Lua shutdown handlers run for up to 5 seconds, then it is disposed), the event hub and the mail backend factory.
  3. The script error handler is unsubscribed. It stays subscribed until here, so errors raised by shutdown handlers are still printed on stderr while the services stop.
  4. The session is disposed: the mail backend factory again, as a safety net, and then the service container.

Each service has its own time limit:

Service Limit
any service, by default 10 seconds
new-mail watcher 12 seconds
script engine 11 seconds (5 for the Lua shutdown handlers and the wait of its dispose)
job scheduler jobs.stop_timeout_seconds plus 2 seconds

The lifecycle waits one second more for every service, so that one that honours its cancellation token can finish its clean-up. A service that fails or runs over its limit is written to the log, not to the terminal, and the next one still stops. Because the job scheduler stops before the script engine, a Lua shutdown handler cannot queue a job.

A second Ctrl+C while the stop is running ends the process at once with exit code 130, without running the rest of the stop sequence. The command line framework does not cut the stop short after a fixed time.

  • 0 after an orderly stop.
  • 1 when the configuration cannot be read, when no account lists a watch folder, when init.lua raises an error at start, or when a service fails to start.
  • 130 when a second Ctrl+C ends the stop.

tmail completion <shell> prints the tab completion script of bash, zsh or fish on standard output and exits with 0. Install and build says how to load it.

Terminal window
tmail completion zsh > ~/.zfunc/_tmail
  • The script completes the commands, the second word of cache, gpg, rules and themes, and the option names of each command. --config and --body-file complete file names, and tmail completion completes bash, zsh and fish. Account names, folders, themes and fingerprints are not completed.
  • An option that is already on the line is not offered again, except --to and --cc.
  • The script is built from the commands tmail registers, so it lists what the installed version has. Print it again after an upgrade.
  • It prints the script before anything else starts: it does not read config.toml, takes no --config, creates no folder (not even ~/.config/tildemail), opens no log, never connects to a server and never runs gpg. It works with a read-only home folder, so it is safe in a shell start-up file.
  • The shell name is lower case. A missing or unknown name, or more than one name, prints Usage: tmail completion bash|zsh|fish on stderr, prints nothing on stdout and exits with 1.

tmail man prints the manual page tmail(1) in roff on standard output and exits with 0; tmail man config prints tmail-config(5), the page for config.toml. The Debian package installs both, so man tmail and man tmail-config work after apt install. To read them without the package:

Terminal window
tmail man | man -l -
tmail man config | man -l -
  • tmail(1) is built from the commands tmail registers, with their arguments and options, so it lists what the installed version has. tmail-config(5) is written by hand and embedded in the program.
  • Like tmail completion, it prints before anything else starts: it does not read config.toml, takes no --config, creates no folder, opens no log, never connects to a server and never runs gpg.
  • The topic is lower case. Any other word prints Usage: tmail man [config] on stderr, prints nothing on stdout and exits with 1.