Skip to content
TildeMailv0.2.1

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.

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.

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.

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" theme
log_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] # optional
enabled = true # default true
max_size_mb = 500 # default 500, from 16 to 100000
[jobs] # optional
workers = 4 # default 0: one per CPU core
stop_timeout_seconds = 10 # default 10, from 1 to 3600
[keys] # optional
preset = "vim" # default "vim"; "vim" or "none"
leader = "<Space>" # default "<Space>"; one key, a bare " " is accepted too
timeout_ms = 1000 # default 1000, from 100 to 10000
[[account]]
name = "personal" # required, unique (case does not matter)
address = "me@example.org" # required, a bare address
display_name = "Me Myself" # default ""
username = "me@example.org" # required; ${VAR} and #{command} allowed
password = "${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" # required
port = 993 # default 993
security = "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" # required
port = 587 # default 587
security = "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] # optional
key = "0xABCD1234ABCD1234" # key id or fingerprint; required when sign is true or encrypt is true
sign = false # default false
encrypt = 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, repeatable
address = "alias@example.org" # required, distinct from the account and the other identities
display_name = "Me, as alias" # default: the account's
envelope_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's
key = "0xABCD1234ABCD1234"
sign = true
encrypt = "auto" # true, false or "auto"; default: the account's
fetch_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 Maildir
name = "local"
address = "me@home.example"
username = "me" # for SMTP
password = "#{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:

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.

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. address and every identity address must be a bare address such as me@example.org; the display name goes in display_name. An identity address must differ from the account’s and from the other identities, case aside.
  • Servers. Ports are 1 to 65535. security values are written in snake_case.
  • Limits. cache.max_size_mb is 16 to 100000. jobs.stop_timeout_seconds is 1 to 3600. A jobs.workers at or below 0 means one worker per CPU core.
  • Theme. theme is 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. language is a top-level string, written before any table: a language code such as en, it or pt-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_level is a top-level string, written before any table: verbose, debug, information (the default), warning or error, in any case. Anything else is an error. See Logging.
  • Keys. keys.preset is vim or none. keys.leader is one key in the notation of Key bindings, so an empty value, more than one key or a digit from 1 to 9 is an error. keys.timeout_ms is 100 to 10000. These are checked when the file is loaded, so tmail check reports them; it does not start the scripts, so it does not check Lua bindings.
  • Signatures. signature_file is a plain file name inside signatures/: 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.
    • certificate pins 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, so tmail check does not look for it. For Proton Mail Bridge: certificate = "~/.config/protonmail/bridge-v3/cert.pem".
    • verify_certificate = false accepts 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 check warns about every server that has it. When certificate is 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_mailer is true or false. When true (the default), every message the account sends carries X-Mailer: tmail <version>; set it to false to send no such header.
  • GPG. An identity’s [account.identity.gpg] inherits each field it leaves out from the account. encrypt is true, false or "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 when sign is enabled or encrypt is true, but the loader does not check it, so tmail check accepts sign = true without a key: composing is what refuses to sign or encrypt without one. With encrypt = "auto" and no key the message goes out in clear and the confirmation says so. fetch_keys (default false) 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.

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.

layout says how the Maildir keeps its folders:

  • verbatim (the default; SubFolders Verbatim in mbsync) treats the path as a container. Every directory below it that holds cur and new is a folder, named by its relative path: INBOX, Sent, Lists/rust.
  • maildir++ (Dovecot, and SubFolders Maildir++ in mbsync) makes the path itself the INBOX and each .Name.Sub directory in it the folder Name.Sub.

Use these folder names in sent_folder, trash_folder, archive_folder and watch. A matching mbsync setup for the verbatim layout:

IMAPAccount home
Host imap.home.example
User me
PassCmd "pass show imap/home"
IMAPStore home-remote
Account home
MaildirStore home-local
Path ~/Mail/home/
Inbox ~/Mail/home/INBOX
SubFolders Verbatim
Channel home
Far :home-remote:
Near :home-local:
Patterns *
Create Both
SyncState *

How tmail reads a Maildir is described under Maildir accounts.

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.

Terminal window
TMAIL_LOG_LEVEL=debug tmail send personal --to bob@example.org --subject "Hi" < body.txt

At 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.

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 variable NAME.
  • #{command} runs command with /bin/sh -c and uses its trimmed standard output.
password = "${PERSONAL_MAIL_PASSWORD}"
password = "#{pass show mail/${ACCOUNT}}"
  • 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 $word in username or password stays 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.
  • 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 bw when the vault is locked, is therefore reported only as “the credential command failed with exit code N”, never with the reason. With bw, unlock the vault outside tmail and pass the session in the BW_SESSION environment variable (export BW_SESSION=$(bw unlock --raw)), so the command can run without prompting.

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.

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).

  • 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 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.

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.

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.

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.

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: inv finds “Invoice” and cafe finds “café”, but voice does 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_mb still 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.

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.

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.

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.