Security notes
This page sums up what tmail protects and how. Each point links to the page with the details.
Files on disk
Section titled “Files on disk”config.tomlholds no secret in clear when you use the credential placeholders, but it names your servers, variables and commands. Keep itchmod 600;tmail checkwarns otherwise.- The cache holds your mail as the server sent it, unencrypted. PGP-encrypted messages stay encrypted, and their decrypted text is never written to it. See What is on disk.
- The compose file holds the plain text of a message that may be about to be encrypted. It is created with mode 0600 under
$XDG_RUNTIME_DIR(private to you, in memory on most systems) or undercache/drafts(mode 0700), never in the shared temporary directory, and deleted afterwards. - Logs are kept as the last 14 daily files, in a directory with mode 0700.
Credentials
Section titled “Credentials”username and password are resolved only when a connection is made and kept in memory only. They are never written to the cache or the log, and never printed: AccountConfig.ToString() hides them, error messages name at most the variable, and credential commands run with their standard error discarded. A ${NAME} inside a credential command reaches the shell as data, never as code.
- gpg runs with
--batch, with its status on a separate descriptor and its diagnostics in a private log file, so nothing a sender writes can pose as a gpg status line. - Decryption never writes to a file name the sender chose and never asks a keyserver.
- Looking up and importing a public key (
gpg.search_key,gpg.import_key,tmail gpg fetch) happens only when a script calls it or you run the command; nothing runs it on its own.tmail gpg fetchimports only after you pick a key or give its fingerprint. The lookup tells the web key directory host and the key server which address was asked for, and the key server is the one indirmngr.conf(or dirmngr’s default), not the one ingpg.conf. A lookup downloads into a temporary keyring and never writes yours. An import checks that the key has the requested fingerprint and that the data holds no other key (one that carries the same fingerprint as a subkey is refused), refuses a key over 1 MiB, leaves other people’s signatures out, and never trusts or certifies the key. If yourgpg.confsetstrust-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. 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. - With
fetch_keys = truefor an account, tmail looks up the key of a recipient who has none while you write to them. The lookup tells the web key directory host of the recipient’s domain, which the recipient controls, and the key server which address you write to, and your IP address unless dirmngr goes through Tor; that is why the setting is off by default. A key found is only offered: the confirmation shows the address, the full fingerprint and the source, and the key is imported after you answery. Check the fingerprint with its owner when it matters: a web key directory or a key server can return a key that is not the person’s. That one message is encrypted with the offered keys accepted without a trust check (gpg --trust-model always, run only after the other recipients were checked with your own trust model), so your trust database is never read or changed, the key stays untrusted in your keyring, and accepting it does not make any other key valid. The import asks the web key directory and the key server again before it writes the key, so both hosts see the address twice. It does not apply totmail sendor to a script, which never ask. Kon a signature that saysUnknown keylooks the signing key up (the web key directory of theFromaddress’s domain and the key server) only when you press it, and imports it only after you answery. The hosts learn which address and which fingerprint you asked about, and the sender controls the web key directory of their domain, so they learn that you read the message and when. The key is not trusted by the import: the signature becomesUntrusteduntil you certify the key. A key found for the address is accepted only if its fingerprint, or one of its subkeys’ (a signature made by a subkey names the subkey), is the signing key; a long key id alone is never sent to a key server. The sender chooses the key a signature names, so the window says that a key found only by its fingerprint was not published for theFromaddress.- A good signature is shown as
Goodonly when the signing key has a valid user id with the exactFromaddress; otherwise it isUntrusted. - Recipients are matched exactly, never by substring.
Details are under GPG.
Input from the network
Section titled “Input from the network”- A received subject or display name cannot start a header line of its own in the compose file: control characters and Unicode line separators become spaces.
- Attachment names are sanitised, and saving an attachment never overwrites a file.
- Notification bodies are escaped before they reach
notify-send. - Scripts see a
headerstable, not raw bytes. - An HTML-only message is shown as formatted text, never as a page: scripts, styles, hidden elements and embedded images are dropped, no remote image is fetched (so a tracking pixel never calls home), no link is opened, and only
http,httpsandmailtolinks are kept. The reader shows the real host of every web link next to its text unless the text is exactly that host, so a link that says one site and goes to another is visible, in a table cell too; a link it cannot name is drawn as plain text. Control characters, escape sequences and bidi or invisible characters are removed from what is drawn, entities included. HTML nested more than 100 levels deep is read as plain text, and Markdown nested hundreds of levels deep is drawn as plain text, so a hostile body cannot exhaust the stack or the time of the interface.
Server certificates
Section titled “Server certificates”tmail validates the certificate of an IMAP or SMTP server with the system’s trust store. For a server with a self-signed certificate, name its certificate file with certificate (see Configuration): the connection is then accepted only for exactly that certificate. verify_certificate = false turns the check off for a server; that leaves the connection open to anyone on the network path, so use it only where the path is trusted (such as 127.0.0.1), and expect tmail check to warn about it.
Scripts
Section titled “Scripts”Lua has no filesystem, network or process access of its own, cannot leave the scripts directory through require, and each unit of work stops after 10,000,000 instructions. The one thing a script can do to your keyring is gpg.import_key, which adds a public key without asking you and leaves it untrusted; scripts cannot sign, encrypt or decrypt. Looking keys up (gpg.search_key, gpg.import_key) makes gpg contact a key server and a web key directory host, so a script can cause that network traffic. A lookup of a sender’s address, for example from a new_mail handler, works as a read receipt: the web key directory host of the sender’s domain, which the sender controls, learns that the mail was processed and when, and your IP address unless dirmngr goes through Tor. There is no rate limit; each call blocks the single script thread for up to about 30 seconds (60 for an import with an address). See gpg.search_key and gpg.import_key. init.lua runs through a symbolic link only when the target is a regular file you own that nobody else can write. See The sandbox.
Maildir
Section titled “Maildir”tmail reads and renames the files of the Maildir you name and nothing else: a folder name cannot contain .. or leave the Maildir, and symbolic links are never followed. The UID index in ~/.config/tildemail/maildir/ holds file names only, never message text. Messages tmail writes there (the sent copy) are created with mode 0600.
Live tests
Section titled “Live tests”The live tests create and delete folders, append messages and restart the container. Point them only at the throw-away GreenMail stack (see Development).