aboutsummaryrefslogtreecommitdiffstats

posta

Leé esto en castellano

posta is a tiny, generic IMAP/SMTP mail client for your terminal. One binary, JSON output by default, and named accounts in a single TOML file — so your mailbox is as scriptable as any other API.

The name comes from the postas: the relay stations of the colonial mail routes of South America. In Argentine slang, la posta also means "the real thing".

$ posta list --unread --max 3 --format table
   UID      DATE                    FROM                              SUBJECT
  7121  ●   Fri, 17 Jul 2026 17:0…  "Example ISP" <billing@isp.ex…    Your invoice is ready
  7120  ●   Fri, 17 Jul 2026 15:0…  Meetup <events@meetup.examp…      🚀 Registration open
  7119  ●   Fri, 17 Jul 2026 10:1…  "bank@example.com"                Card notice

$ posta search 'FROM bank UNSEEN' | jq .uids
[7119, 7118, 7117]

Features

  • Works with any provider — plain IMAP/SMTP with STARTTLS (imap://, smtp://) or implicit TLS (imaps://, smtps://). Your own server, Gmail, Fastmail, anything.
  • Multiple accounts — named accounts in one config file, switched with --account, $POSTA_ACCOUNT, or a default_account.
  • Script-friendly — every command prints pretty JSON (pipe it to jq), with --format table for humans.
  • Secrets your way — literal passwords (the file is checked for loose permissions and posta warns you) or password_cmd to fetch them from pass, a keyring, or any command.
  • Safe by default — reading never marks messages as seen (BODY.PEEK everywhere); opt in with --mark-read or posta mark read.
  • No config file needed — everything can come from POSTA_* environment variables, handy for CI and one-off scripts.

Install

git clone https://git.all.ar/pub/posta.git
cd posta
cargo install --path .

Configure

~/.config/posta/config.toml (or $POSTA_CONFIG, or --config FILE):

default_account = "personal"

[accounts.personal]
email = "alice@example.com"
display_name = "Alice Example"
imap_url = "imap://alice@example.com@mail.example.com:143"
smtp_url = "smtp://alice@example.com@mail.example.com:587"
password_cmd = "pass show mail/personal"   # or: password = "..." (chmod 600!)

[accounts.work]
email = "alice@work.example"
imap_url = "imaps://alice@imap.work.example"    # implicit TLS, port 993
smtp_url = "smtps://alice@smtp.work.example"    # implicit TLS, port 465
password_cmd = "secret-tool lookup mail work"
sent_folder = "Sent Items"
trash_folder = "Deleted"

The user part of a URL may itself contain @ (imap://alice@example.com@mail.example.com); the host starts after the last one. imap_password[_cmd] / smtp_password[_cmd] override the shared password per protocol when they differ.

Environment variables override any field of the selected account: POSTA_IMAP_URL, POSTA_SMTP_URL, POSTA_PASSWORD, POSTA_PASSWORD_CMD, POSTA_IMAP_PASSWORD, POSTA_SMTP_PASSWORD, POSTA_EMAIL, POSTA_DISPLAY_NAME, POSTA_SENT_FOLDER, POSTA_TRASH_FOLDER, plus POSTA_ACCOUNT and POSTA_CONFIG.

Use

posta accounts                       # configured accounts
posta status                         # message/unread counts
posta folders                        # list mailboxes
posta list --unread                  # newest first
posta list --query 'FROM bank SINCE 1-Jul-2026'
posta get 7119                       # headers + text body, as JSON
posta get 7119 --html                # prefer the HTML part
posta get 7119 --raw > msg.eml       # raw RFC 822 source
posta send --to bob@example.com --subject 'Hi' --body 'Hello!'
git log | posta send --to bob@example.com --subject 'Changelog'
posta mark read 7119 7120            # also: unread, flagged, unflagged
posta move 7119 --to Archives
posta delete 7119                    # moves to the trash folder
posta search 'FROM bank UNSEEN'      # raw IMAP SEARCH, prints UIDs

Global flags: --account/-a NAME, --config FILE, --format json|table, and --folder on folder-scoped commands.

Every command opens a fresh session and logs out; there is no daemon, cache, or local state.

Notes

  • send appends a copy to the sent folder over IMAP (skip with --no-record).
  • move/delete use UID MOVE when the server supports it, falling back to COPY + \Deleted + EXPUNGE.
  • TLS is always required; plain-text connections are not supported.

Development

cargo test          # unit + CLI integration tests, no server needed
cargo clippy --all-targets -- -D warnings
cargo fmt --check

License

MIT