Configuration¶
Pony Express is configured through a single TOML file. Use pony account add
to print an annotated template you can paste into it, or pony config edit
to open the file in $EDITOR (it's created from the bundled sample if it
does not exist yet).
File location¶
| Platform | Default path |
|---|---|
| Linux | ~/.config/pony/config.toml |
| macOS | ~/.config/pony/config.toml |
| Windows | %APPDATA%\pony\config.toml |
Override the directory with PONY_CONFIG_DIR, or pass --config <path> to
any command to use a specific file.
Schema version¶
config_version is required at the top of the file. Pony refuses to
load a config that omits it or carries a different value, rather than
silently migrating. The current supported version is 2. The bump from v1
moved the SMTP keys out of the flat namespace and into a per-account
[accounts.<name>.smtp] subtable.
Annotated example¶
The following config defines two IMAP accounts and two local accounts (one read-only, one with SMTP).
config_version = 2
# -- Global options --------------------------------------------------------
# Show Unicode symbols in the TUI (attachment clip, flag star, etc.).
use_utf8 = true
# External editor launched with ctrl+x e in the composer. If omitted or
# not executable, the built-in inline editor is used.
editor = "/usr/bin/nvim"
# Global default for Markdown composition mode. Override per-account
# below; toggle per-message with ctrl+x m.
markdown_compose = false
# BBDB v3 file for Emacs interop. Read by `pony contacts import` when no
# path is given; never written to. Nothing happens automatically — import
# and export are commands you run. Supports ~, $VAR, and %VAR% expansion.
# bbdb_path = "~/.emacs.d/bbdb"
# Where the TUI's "open" / "save" attachment keys land. Created on demand.
# Default: ~/Downloads.
# downloads_path = "~/mail-attachments"
# Textual theme for the TUI colour scheme. Run `pony --list-themes` for
# names. The --theme CLI flag overrides this for a single session.
# theme = "nord"
# -- First account: personal Gmail ----------------------------------------
[[accounts]]
name = "Personal"
email_address = "jane@example.com"
imap_host = "imap.gmail.com"
username = "jane@example.com"
credentials_source = "encrypted"
archive_folder = "Archive"
markdown_compose = true
signature = """
Jane Smith
jane@example.com"""
[accounts.smtp]
host = "smtp.gmail.com"
# port and ssl default to 465 / true.
[accounts.mirror]
path = "mirrors/personal" # relative to Pony's data directory
format = "maildir"
trash_retention_days = 30
[accounts.folders]
exclude = ["\\[Gmail\\]/Spam", "\\[Gmail\\]/Trash"]
read_only = ["\\[Gmail\\]/Sent Mail", "\\[Gmail\\]/All Mail"]
# -- Second account: corporate IMAP, STARTTLS on 587 ----------------------
[[accounts]]
name = "Work"
email_address = "jane.smith@corp.example.com"
imap_host = "mail.corp.example.com"
imap_port = 993
imap_ssl = true
username = "jsmith"
credentials_source = "command"
password_command = ["pass", "show", "corp/imap"]
sent_folder = "Sent Items"
drafts_folder = "Drafts"
archive_folder = "Archive"
[accounts.smtp]
host = "smtp.corp.example.com"
port = 587
ssl = false # STARTTLS
[accounts.mirror]
path = "mirrors/work"
format = "mbox"
trash_retention_days = 14
[accounts.folders]
include = ["INBOX", "Projects", "Projects/.*"]
exclude = ["Junk E-mail", "Deleted Items"]
read_only = ["Sent Items"]
# -- Local read-only archive ---------------------------------------------
[[accounts]]
account_type = "local"
name = "Archive"
email_address = "jane@example.com"
[accounts.mirror]
path = "mirrors/archive"
format = "maildir"
trash_retention_days = 90
# -- Local account with SMTP (read mail from disk, send via relay) -------
[[accounts]]
account_type = "local"
name = "Spool"
email_address = "jane@example.com"
username = "jane@example.com"
credentials_source = "plaintext"
password = "change-me"
[accounts.smtp]
host = "smtp.example.com"
[accounts.mirror]
path = "mirrors/spool"
format = "mbox"
trash_retention_days = 30
Global options¶
| Key | Type | Default | Description |
|---|---|---|---|
config_version |
int | — | Required. Must be 2. |
use_utf8 |
bool | false |
Enable Unicode symbols in the TUI. |
editor |
string | (none) | External editor for the composer (ctrl+x e). |
markdown_compose |
bool | false |
Global default for Markdown composition mode. |
bbdb_path |
string | (none) | BBDB v3 file imported/exported on each sync. Supports ~, $VAR, %VAR%. |
downloads_path |
string | ~/Downloads |
Directory for the TUI's open/save attachment keys. |
theme |
string | (textual default) | Textual theme name. --theme overrides per-session. |
IMAP account fields¶
These apply when account_type = "imap" (the default if account_type is
omitted).
Identity¶
| Key | Type | Required | Description |
|---|---|---|---|
name |
string | yes | Unique account identifier shown in the TUI and CLI. |
email_address |
string | yes | Address used in the From: header. |
username |
string | yes | Login username for IMAP and SMTP (often the email address). |
IMAP connection¶
| Key | Type | Default | Description |
|---|---|---|---|
imap_host |
string | — | IMAP server hostname. |
imap_port |
int | 993 (SSL) / 143 |
IMAP port. |
imap_ssl |
bool | true |
Use TLS for IMAP; false enables STARTTLS. |
SMTP connection — [accounts.smtp] subtable¶
Required for IMAP accounts; optional for local accounts (omit to make a local account read-only).
| Key | Type | Default | Description |
|---|---|---|---|
host |
string | — | SMTP server hostname. |
port |
int | 465 (SSL) / 587 |
SMTP port. |
ssl |
bool | true |
Implicit TLS when true, STARTTLS when false. |
Credentials¶
| Key | Type | Default | Description |
|---|---|---|---|
credentials_source |
string | — | One of "plaintext", "env", "command", "encrypted". |
password |
string | (none) | Password — only with credentials_source = "plaintext". |
password_command |
list of strings | (none) | Command whose stdout is the password — only with credentials_source = "command". |
See Credential backends below.
Composer¶
| Key | Type | Default | Description |
|---|---|---|---|
sent_folder |
string | (auto) | Exact folder name where sent messages are stored; auto-discovered by fuzzy match if omitted. |
drafts_folder |
string | (auto) | Exact folder name for saved drafts. |
archive_folder |
string | (none) | Target for the A key in the TUI. Omit to disable archiving. Must not be excluded or read-only. Created on the server on first archive if it doesn't exist. |
markdown_compose |
bool | inherits global | Default Markdown mode for this account. |
signature |
string | (none) | Text appended below the cursor on replies and forwards. |
Local account fields¶
Local accounts point at a Maildir or mbox tree that already exists on disk — an archive, or mail delivered there once. Pony never connects to an IMAP server for them; sync is a no-op. Browsing, searching, harvesting contacts, and folder creation all work normally.
Pony owns the tree
Pony writes flags, moves and deletions into the mirror, so the tree must
not also be written to by another program while Pony is using it. To bring
in mail another tool manages, copy it in with pony folder mirror rather
than pointing both at the same files. This applies to a second Pony
instance as well for mbox mirrors, which have no file locking yet.
| Key | Type | Required | Description |
|---|---|---|---|
account_type |
string | yes | Must be "local". |
name |
string | yes | Unique account identifier. |
email_address |
string | yes | Address used in the From: header. |
sent_folder / drafts_folder |
string | no | Folder-name overrides. |
markdown_compose |
bool | no | Default Markdown mode. |
signature |
string | no | Signature text. |
A local account becomes send-capable by adding an [accounts.smtp]
subtable plus username / credentials_source / password /
password_command fields. Only send-capable accounts appear in the
composer's From: dropdown.
Mirror configuration — [accounts.mirror]¶
| Key | Type | Default | Description |
|---|---|---|---|
path |
string | — | Local mail directory. Env vars (~, $VAR, %VAR%) expanded. Relative paths resolve against Pony's data directory; use absolute for custom locations. |
format |
string | — | "maildir" or "mbox". |
trash_retention_days |
int | 30 |
Days to keep trashed messages before permanent deletion. |
Maildir stores one file per message in cur/, new/, and tmp/
subdirectories. Robust under concurrent access, plays well with
rsync-based backups.
mbox stores all messages in a single file per folder. Compact, but does not tolerate concurrent writers. Pony maintains a TOC sidecar so re-opens are fast. Prefer Maildir unless you have an existing mbox archive to point at.
Folder sync policy — [accounts.folders]¶
All three keys accept lists of Python re.fullmatch() patterns. A plain
name like "INBOX" matches exactly; "Archive/.*" matches any subfolder of
Archive; ".*" matches everything. In TOML, backslashes must be doubled
("\\[Gmail\\]/.*").
| Key | Default | Description |
|---|---|---|
include |
[] (all) |
If non-empty, only matching folders are synced. |
exclude |
[] |
Folders never synced, even if matched by include. |
read_only |
[] |
Synced server-to-local only; local flag changes and deletions are never pushed back. |
Precedence: exclude beats include beats read_only. A folder in
both include and read_only is synced read-only. A folder in both
read_only and exclude is not synced at all.
Auto-include of read_only: When include is non-empty, folders in
read_only are still synced unless also matched by exclude — so
include = ["INBOX"] will still pull Sent Mail if it's in read_only.
Common patterns¶
# Sync everything except Spam and Trash
[accounts.folders]
exclude = ["Spam", "Trash"]
# Gmail: sync INBOX and Archive; treat Sent/All Mail as read-only
[accounts.folders]
exclude = ["\\[Gmail\\]/Spam", "\\[Gmail\\]/Trash"]
read_only = ["\\[Gmail\\]/Sent Mail", "\\[Gmail\\]/All Mail"]
# Sync only a specific project folder tree
[accounts.folders]
include = ["INBOX", "Projects", "Projects/.*"]
Credential backends¶
plaintext¶
Password stored as a string in config.toml. Convenient for testing;
leaves the password readable on disk.
env¶
Password read from an environment variable at runtime. The variable name
is derived from the account name: uppercased, every character that is not
a letter or digit replaced with an underscore, prefixed with
PONY_PASSWORD_.
For an account named "Personal", set PONY_PASSWORD_PERSONAL. For
"Work Email" or "work-email", set PONY_PASSWORD_WORK_EMAIL — both map
to the same variable, since a hyphen cannot appear in a shell variable
name.
command¶
An external command is run and its stdout (trailing whitespace stripped) is used as the password. The command is executed directly — no shell interpolation.
Compatible with pass, the 1Password CLI
(op read), macOS Keychain via security find-generic-password, and
similar tools.
encrypted¶
Password is encrypted and stored as a blob in the SQLite index. The encryption key is derived from machine-specific information (OS username and machine ID), so the blob is usable only on the machine that created it.
- Windows: DPAPI (
CryptProtectData). - Linux/macOS: PBKDF2-HMAC-SHA256 key derivation with a SHAKE-256 keystream.
The first time Pony tries to connect it prompts interactively:
The result is stored and reused. To re-prompt (after a password change):