Configuration
tmail reads its settings from ~/.config/tildemail/config.toml. This page lists every key, how credentials are written, and how the local mail cache behaves. Read it when you set up an account or change a setting.
A minimal file
Section titled “A minimal file”One account needs a name, an address, the login and the two servers:
[[account]]name = "personal"address = "me@example.org"username = "me@example.org"password = "${PERSONAL_MAIL_PASSWORD}"
[account.imap]host = "imap.example.org"
[account.smtp]host = "smtp.example.org"Run tmail check after every change: it validates the file and lists the accounts (see Commands). Every subcommand except tmail themes show, tmail gpg fetch, tmail completion and tmail man takes --config <path> to read another file.
Files and folders
Section titled “Files and folders”Everything lives under ~/.config/tildemail/:
| Path | Content | Mode |
|---|---|---|
config.toml |
accounts and settings; you create it | tmail check warns if group or others have any access |
cache/ |
one SQLite database per account, drafts/ for the compose file |
0700, databases 0600 |
logs/ |
tmail-YYYYMMDD.log, one file per day, the last 14 files kept |
0700 |
scripts/ |
init.lua, your modules, the generated definitions.lua and .luarc.json |
default |
signatures/ |
the text signatures named by signature_file |
default |
maildir/ |
the UID indexes of the Maildir accounts; created when the first index opens | default |
themes/ |
the theme files; never created by tmail | default |
langs/ |
the language files, <code>.toml; created empty, you put a translation here; the languages that a package installs are read after it |
default |
state/ |
ui.toml, what the terminal interface remembers between runs: the last folder, the expanded accounts, whether the list shows conversations and the history of the : line; also services.lock, held by the tmail that runs the services (see tmail watch), and unsent/, the texts of messages that were not sent, kept here only when there is no $XDG_RUNTIME_DIR (see Closing) |
0700, files 0600 |
plugins/ |
the plugin bundles, one folder each; never created by tmail | default |
tmail creates the root, cache, logs, scripts, state, langs and signatures on first run, and restricts the root, cache, logs and state to the owner. This is best effort: on a filesystem without modes, a warning is logged. If ~/.config/tildemail cannot be created (a read-only home, for example), tmail prints the error to stderr and exits with 1.
config.toml
Section titled “config.toml”A complete example follows; every key that has a default is shown with it. Only name, address, username, password, smtp.host and one of imap.host or maildir.path are required.
theme = "nord" # optional, before any table; default: the built-in "default" themelog_level = "information" # optional, before any table; "verbose", "debug", "information", "warning" or "error"language = "en" # optional, before any table; the language of the interface, default "en"
[cache] # optionalenabled = true # default truemax_size_mb = 500 # default 500, from 16 to 100000
[jobs] # optionalworkers = 4 # default 0: one per CPU corestop_timeout_seconds = 10 # default 10, from 1 to 3600
[keys] # optionalpreset = "vim" # default "vim"; "vim" or "none"leader = "<Space>" # default "<Space>"; one key, a bare " " is accepted tootimeout_ms = 1000 # default 1000, from 100 to 10000
[[account]]name = "personal" # required, unique (case does not matter)address = "me@example.org" # required, a bare addressdisplay_name = "Me Myself" # default ""username = "me@example.org" # required; ${VAR} and #{command} allowedpassword = "${PERSONAL_MAIL_PASSWORD}"sent_folder = "Sent" # default "Sent"trash_folder = "Trash" # default "Trash"archive_folder = "Archive" # default "Archive"watch = ["INBOX"] # default ["INBOX"]signature_file = "me.txt" # optional, a plain file name in signatures/x_mailer = true # default true: add "X-Mailer: tmail <version>"
[account.imap]host = "imap.example.org" # requiredport = 993 # default 993security = "tls" # default "tls"; "tls", "start_tls" or "none"certificate = "~/imap.pem" # optional: trust only this certificate (self-signed server)verify_certificate = true # default true; false accepts any certificate (not recommended)
[account.smtp]host = "smtp.example.org" # requiredport = 587 # default 587security = "start_tls" # default "start_tls"certificate = "~/smtp.pem" # optional: trust only this certificate (self-signed server)verify_certificate = true # default true; false accepts any certificate (not recommended)
[account.gpg] # optionalkey = "0xABCD1234ABCD1234" # key id or fingerprint; required when sign is true or encrypt is truesign = false # default falseencrypt = false # default false; true, false or "auto"fetch_keys = false # default false; look up the key of a recipient who has none, before sending
[[account.identity]] # optional, repeatableaddress = "alias@example.org" # required, distinct from the account and the other identitiesdisplay_name = "Me, as alias" # default: the account'senvelope_from = "account" # default "account"; or "identity"signature_file = "alias.txt" # default: the account's; "" for none
[account.identity.gpg] # optional; a missing field is the account'skey = "0xABCD1234ABCD1234"sign = trueencrypt = "auto" # true, false or "auto"; default: the account'sfetch_keys = true # default: the account's
[[account]]name = "work"address = "me@corp.example"username = "me"password = "#{bw get password work-mail}"watch = ["INBOX", "Alerts"]
[account.imap]host = "mail.corp.example"
[account.smtp]host = "mail.corp.example"
[[account]] # an account read from a local Maildirname = "local"address = "me@home.example"username = "me" # for SMTPpassword = "#{pass show smtp/home}"sent_folder = "Sent"
[account.maildir] # instead of [account.imap]path = "~/Mail/home" # required, absolute (~ and ${VAR} allowed)layout = "verbatim" # default "verbatim"; or "maildir++"
[account.smtp]host = "smtp.home.example"Where each table is explained:
theme: Themes.language: Languages.[cache]: Mail cache below.[jobs]: Background jobs.[keys]: Key bindings.watch: New-mail watcher andtmail watch.signature_file,[account.gpg],[[account.identity]]: Composing and replying and Sender identities below.[account.maildir]: Maildir layout below.
Plugins
Section titled “Plugins”Plugins are disabled by default. To load a published plugin bundle, add a top-level [plugins] table outside the account tables and list the bundle directory names:
[plugins]enabled = ["TildeMail.Sample.Plugin"]Install the bundle under ~/.config/tildemail/plugins/ and restart tmail after installing it or changing enabled. See the plugin guide for publishing, authoring, lifecycle behavior, and limitations.
Validation
Section titled “Validation”The loader stops at the first error. An error in an account names the account and the key; an error in [cache], [jobs] or [keys] names the key; an error about the whole file (missing, not valid TOML, no account, a duplicate account name) names the file.
- Accounts. There is at least one
[[account]]. Every account has exactly one of[account.imap]and[account.maildir]. Names are required, unique without regard to case, and free of control characters. - Addresses.
addressand every identityaddressmust be a bare address such asme@example.org; the display name goes indisplay_name. An identity address must differ from the account’s and from the other identities, case aside. - Servers. Ports are 1 to 65535.
securityvalues are written in snake_case. - Limits.
cache.max_size_mbis 16 to 100000.jobs.stop_timeout_secondsis 1 to 3600. Ajobs.workersat or below 0 means one worker per CPU core. - Theme.
themeis a top-level string, written before any table. The loader only reads it: a blank value means no theme, and the name itself is checked later against the rules in Themes. A value that is not a string is an error. - Language.
languageis a top-level string, written before any table: a language code such asen,itorpt-br(two or three letters, optionally a dash and two to eight letters or digits), in any case. Anything else is an error. See Languages. - Log level.
log_levelis a top-level string, written before any table:verbose,debug,information(the default),warningorerror, in any case. Anything else is an error. See Logging. - Keys.
keys.presetisvimornone.keys.leaderis one key in the notation of Key bindings, so an empty value, more than one key or a digit from1to9is an error.keys.timeout_msis 100 to 10000. These are checked when the file is loaded, sotmail checkreports them; it does not start the scripts, so it does not check Lua bindings. - Signatures.
signature_fileis a plain file name insidesignatures/: no directory part, no/or\, not.or.., no control characters. An identity without the key uses the account’s signature;""turns it off for that identity. - Certificates. A server whose certificate no authority signed, such as a local mail bridge, needs one of two keys, in
[account.imap]and[account.smtp]. Without them the certificate is validated as usual.certificatepins it: the path of the server’s certificate file, PEM or DER (the first certificate of a PEM file).~/and${NAME}are expanded and the path must be absolute. The connection is accepted only if the server presents exactly that certificate, so a different one (or a man in the middle) is refused. The chain, the host name and the validity dates are not checked, because the pin replaces them: when the server’s certificate changes, put the new one in the file. The file is read when a connection is made, sotmail checkdoes not look for it. For Proton Mail Bridge:certificate = "~/.config/protonmail/bridge-v3/cert.pem".verify_certificate = falseaccepts any certificate, so nothing stops someone on the network path from reading the mail and the password. It is for a server you cannot pin;tmail checkwarns about every server that has it. Whencertificateis also set, the pin wins and there is no warning.- With
security = "none"nothing is encrypted, so either key is an error there.
- X-Mailer.
x_maileristrueorfalse. Whentrue(the default), every message the account sends carriesX-Mailer: tmail <version>; set it tofalseto send no such header. - GPG. An identity’s
[account.identity.gpg]inherits each field it leaves out from the account.encryptistrue,falseor"auto"(the string, in lower case; anything else is an error that names the account and the key)."auto"is resolved when composing, once the editor closes (see Composing). A key is required whensignis enabled orencryptistrue, but the loader does not check it, sotmail checkacceptssign = truewithout a key: composing is what refuses to sign or encrypt without one. Withencrypt = "auto"and no key the message goes out in clear and the confirmation says so.fetch_keys(defaultfalse) makes tmail look for the public key of a recipient who has none when the message is prepared; see Composing. It is off because a lookup tells the web key directory host of the recipient’s domain and the key server which address you write to.
tmail check also warns about every key or table that tmail does not read, such as a misspelled key. The warning does not change the result: the file still loads and the exit code stays 0. This catches a misplaced top-level key too: written after a [table] header, theme belongs to that table, and the warning says so.
Sender identities
Section titled “Sender identities”An identity is an extra From: address of an account (see Accounts and identities).
envelope_from chooses the SMTP envelope sender (MAIL FROM). The default, "account", uses the account address, because most servers refuse a foreign one; "identity" uses the identity address. Your provider must allow sending with the identity’s address in From: (often called “send as”); otherwise recipients’ servers may mark the mail as spam.
Maildir layout
Section titled “Maildir layout”layout says how the Maildir keeps its folders:
verbatim(the default;SubFolders Verbatiminmbsync) treats the path as a container. Every directory below it that holdscurandnewis a folder, named by its relative path:INBOX,Sent,Lists/rust.maildir++(Dovecot, andSubFolders Maildir++inmbsync) makes the path itself theINBOXand each.Name.Subdirectory in it the folderName.Sub.
Use these folder names in sent_folder, trash_folder, archive_folder and watch. A matching mbsync setup for the verbatim layout:
IMAPAccount homeHost imap.home.exampleUser mePassCmd "pass show imap/home"
IMAPStore home-remoteAccount home
MaildirStore home-localPath ~/Mail/home/Inbox ~/Mail/home/INBOXSubFolders Verbatim
Channel homeFar :home-remote:Near :home-local:Patterns *Create BothSyncState *How tmail reads a Maildir is described under Maildir accounts.
Logging
Section titled “Logging”tmail writes a log to logs/tmail-YYYYMMDD.log (see Files and folders). It logs at information by default: what started and stopped, and every warning or error. Set log_level = "debug" in config.toml, or run a command with the TMAIL_LOG_LEVEL environment variable, to follow what a command does step by step: which servers it connects to and how the certificate is checked, which identity, signature and GPG steps it applies, the size of the message it hands to the server, and where the sent copy goes.
TMAIL_LOG_LEVEL=debug tmail send personal --to bob@example.org --subject "Hi" < body.txtAt debug or verbose the same lines also go to standard error, so a subcommand shows them in the terminal as well as in the file; the interface keeps them out of the terminal. The variable wins over log_level, and a value that is not a level name is ignored. The level of config.toml applies once the file has been read, so the lines logged while it is being read use the level of the variable (or information).
tmail itself never writes a password, a resolved credential or the text of a message to the log; it does name the accounts, the servers and the sender address. What your Lua scripts print or log is written as it is.
Credentials
Section titled “Credentials”Never write a password in clear in the file. username and password take two placeholders, which can be mixed with literal text:
${NAME}is replaced by the environment variableNAME.#{command}runscommandwith/bin/sh -cand uses its trimmed standard output.
password = "${PERSONAL_MAIL_PASSWORD}"password = "#{pass show mail/${ACCOUNT}}"How they are resolved
Section titled “How they are resolved”- Both placeholders are resolved in one pass, only when the account needs the credential (when connecting), never when the file is loaded. The value is kept in memory only.
- Loading the file only checks that the placeholders are well formed and that every referenced variable is defined.
- A
${NAME}inside the command text reaches the command as data. tmail passes the value to the shell in a private environment variable and writes a quoted reference to it in place of${NAME}, so the shell never runs anything written in the value.#{pass show mail/${ACCOUNT}}and#{cat ${HOME}/.mail-token}work as expected, inside or outside quotes. - The value of a variable and the output of a command are used as they are: a
$, a$(...)or a#{inside them is never expanded again, by tmail or by the shell. - A bare
$wordinusernameorpasswordstays literal, so a password may contain$. - A command has 30 seconds to finish. One that fails, prints nothing or times out is an error for that account.
- Credentials are never logged: the log names the account, and at most the variable or the command’s exit code.
Known limitations
Section titled “Known limitations”- A
${NAME}that sits inside quotes nested in a$(...)is not substituted: the command receives the literal text${__tmail_value_0}. For example#{printf '%s' "$(printf '%s' '${X}')"}does not work; write#{printf '%s' '${X}'}, or move the nesting into a script and call that script. - A command’s standard error is discarded on purpose, so a command can never leak a secret into an error message. A command that fails, for example
bwwhen the vault is locked, is therefore reported only as “the credential command failed with exit code N”, never with the reason. Withbw, unlock the vault outside tmail and pass the session in theBW_SESSIONenvironment variable (export BW_SESSION=$(bw unlock --raw)), so the command can run without prompting.
Other string values
Section titled “Other string values”The other string values (hosts, folders, display_name, gpg.key, signature_file, …) take ${NAME}, replaced when the file is loaded. There a bare $NAME is expanded too, because that is how the shared expander works. #{...} is an error outside username and password. An undefined environment variable is always an error.
Error messages name the file, the account, the key and at most the variable; they never quote any other string value. A file that is not valid TOML is reported by line and column only, because the parser’s own message can quote the text it choked on, such as a password written without quotes.
Mail cache
Section titled “Mail cache”tmail keeps a local copy of each account’s mail in a SQLite database, so that listing a folder is fast and mail already read can be read without a network. The cache is only a copy: it never replaces an answer the server can give. Maildir accounts have no cache, because their files are already local.
[cache] turns it on or off (enabled) and sets its size (max_size_mb). tmail cache status and tmail cache clear inspect and empty it (see Commands).
What it stores
Section titled “What it stores”- the folder list;
- the headers of the messages (sender, subject, date, flags), which are what a folder listing shows;
- the whole text of every message tmail reads: the ones you open, and also those fetched in the background, such as each new message the new-mail watcher loads for notifications and scripts, and anything a script or a reply opens.
Each account has its own database in ~/.config/tildemail/cache/, created with mode 0600 in a directory with mode 0700.
When the cache is dropped
Section titled “When the cache is dropped”When anything about the copy looks wrong (a database that is corrupt or was written by another version of tmail, a folder whose UIDVALIDITY changed), tmail drops it and asks the server again.
The first time tmail reads a stored message of a folder it has not listed since it started, it checks the folder’s UIDVALIDITY with the server. It trusts the stored copy without that check only when the server cannot be reached.
The database also remembers which IMAP login filled it: a hash of the host and the user name (both without regard to case) and the port, never the password. A change of host, port or user name empties the cache of the account; a change of the address or the display name keeps it. A user name written as ${VAR} or #{command} is remembered by its text in the configuration file, so a change of the value it resolves to is not noticed.
Size limit
Section titled “Size limit”max_size_mb limits the stored messages of each account. When a new message does not fit, the stored messages used least recently are removed first. A message bigger than the limit is not stored. Headers are not evicted and do not count towards the limit, so the database file can be larger than the limit.
Fetching new mail
Section titled “Fetching new mail”After the first listing of a folder, tmail asks the server only for the messages that arrived since, refreshes the flags, and drops the messages that another program deleted or moved. That check runs on the first page of a folder. A later page shown within 10 seconds reuses its result, so a message moved away in those seconds can still appear there until the next first page.
Reading without a network
Section titled “Reading without a network”When the server cannot be reached, tmail serves what the cache holds: the folder list, the headers it already holds for the folders you listed, and the messages it already read. A message it never read and a folder you never listed fail with the connection error. About every 15 seconds tmail tries the server again, and it goes back to normal on its own once the server answers.
Searching the cache
Section titled “Searching the cache”A search goes to the server when the server can be reached. The server’s answer is returned and the cache is not consulted, so a message deleted elsewhere never shows up.
When the server cannot be reached, the search runs on a full-text index (SQLite FTS5) in the cache instead of failing:
- The index holds the sender and subject of every message whose header the cache holds, and the text of every message it holds in full. It therefore covers the folders you listed and the messages you read, so it can miss mail the server has, and it does not say which.
- A word matches a word that starts with it, ignoring case and accents:
invfinds “Invoice” andcafefinds “café”, butvoicedoes not find “Invoice”. The server matches any part of a word. In a"quoted phrase", only the last word matches as a prefix. - A message whose full copy was removed to respect
max_size_mbstill matches on its sender and subject, not on its text. - The decrypted text of an encrypted message is never indexed.
A library caller that wants quick local results first can call SearchCachedAsync, which never contacts the server.
What is not cached
Section titled “What is not cached”Writes (changing a flag, moving a message, saving a copy in a folder) and sending are not queued. While the server is unreachable they fail, and the cache is updated only after the server accepted a change.
What is on disk
Section titled “What is on disk”Every message tmail reads is stored in full, exactly as the server sent it. A message encrypted with PGP stays encrypted in the cache, and tmail never writes its decrypted text there. Everything else, including the headers and the search index (sender, subject and text of the messages), is stored unencrypted and protected only by the file permissions. Use full-disk encryption if that matters to you.
The words of a removed message can stay in the search index file until SQLite merges its segments; tmail cache clear forces that merge. Passwords and credential commands are never stored.
Lazy connection
Section titled “Lazy connection”With the cache on, an account connects at the first operation that needs the server. With enabled = false, tmail connects as soon as an account is used, and the files already in cache/ are left alone until you run tmail cache clear.