Config File Location

All configuration lives in a single file: ~/.config/mailypoppins/config.toml. Create it interactively with mp config init, or write it by hand. View the resolved config at any time with mp config show. Print the file path with mp config path.

Full Example

A complete single-account configuration with comments explaining each section.

theme = "mailypoppins"  # TUI color theme (see Theme section)
notifications = false       # Desktop notifications for new mail (see Notifications section)

[email]
font_family = "Helvetica, Arial, sans-serif"  # HTML email font
font_size = "16px"                             # HTML email font size
include_signature = true                       # Append signature to outgoing mail
send_hold_secs = 20                            # Undo-send window before hand-off (0 = send now)

[smtp]
host = "postout.lrz.de"
port = 587
username = "your-id"
default_from = "your.name@example.com"
# Password stored in OS keyring, not here

[imap]
host = "imap.example.com"   # Falls back to smtp.host if omitted
port = 993
username = ""               # Falls back to smtp.username if omitted
fetch_concurrency = 4       # Mailboxes fetched in parallel per sync (1-8)
body_fetch_deadline_secs = 30  # Per-mailbox download budget on a TUI sync (0 = unbounded)
sync_interval_secs = 900    # Daemon syncs after this long without a sync (0 = never)
# Password falls back to SMTP password if not set separately

[mailboxes.inbox]
server = "INBOX"            # IMAP folder name; its messages are stored
                            # under the key "inbox" in the account database

[mailboxes.archive]
server = "Archive"

[mailboxes.sent]
server = "Sent Items"

[[mailboxes.extra]]         # Additional mailboxes to sync
server = "Projects"

The file has no signature keys: each signature is a Markdown file under ~/.config/mailypoppins/signatures/. See Signatures.

Section Breakdown

theme

Selects the TUI color theme by name (helix-style: themes are built in and picked in config, not defined in separate files). The key is top-level, so it must appear before the first [section] header. Available themes:

Names are case-insensitive. An unknown name does not fail: the TUI starts with the default theme and logs a warning to the activity log.

theme = "catppuccin-mocha"

notifications

Opt-in desktop notifications for new mail while the TUI is running (default: false). Like theme, the key is top-level, so it must appear before the first [section] header. When a background sync (including the automatic IDLE-triggered fetches) saves new emails to the inbox, one notification is shown per sync: sender: subject for a single new email, or N new emails when several arrive at once. Read-status updates, duplicate cleanup, and mailboxes other than the inbox never trigger a notification.

Notifications use the platform's native tooling — osascript on macOS and notify-send on Linux — and degrade silently if the tool is unavailable.

notifications = true

[email]

Controls the HTML rendering of sent emails. The font family and size are applied to the generated HTML when Markdown is converted for sending. include_signature determines whether the account signature is appended to a draft body on compose, reply, and forward. send_hold_secs is the undo-send window: in the TUI a send is held for that many seconds before it is handed to SMTP, and u cancels it in the meantime, leaving the draft untouched. It defaults to 20 seconds; set it to 0 to send immediately.

[email]
font_family = "Helvetica, Arial, sans-serif"
font_size = "16px"
include_signature = true
send_hold_secs = 20

[smtp]

SMTP settings for sending email. The password is stored in the OS keyring (macOS Keychain, Windows Credential Manager, or Linux Secret Service) under the key smtp-password (or smtp-password-<account> for named accounts). It is never written to the config file.

[smtp]
host = "postout.lrz.de"
port = 587
username = "your-id"
default_from = "your.name@example.com"

[imap]

IMAP settings for fetching, syncing, and watching mailboxes. host, port and username fall back to their SMTP counterparts if omitted, so you only need to specify them when the IMAP server differs from SMTP.

fetch_concurrency is how many mailboxes a single sync fetches in parallel, each on its own connection (default 4, clamped to the range 1-8). A sync opens one connection per mailbox and overlaps their latency, so more mailboxes finish in about the time of one. Lower it toward 1 if a server throttles concurrent connections.

body_fetch_deadline_secs is how long a single mailbox may spend downloading message bodies on a sync started by the TUI (default 30, maximum 600, 0 for no limit). A mailbox that runs out of budget stops between batches, keeps what it has downloaded, and resumes on the next sync, so a large backlog arrives over several syncs instead of holding one open; newest mail is downloaded first. Nothing is deleted and no flag delta is advanced on a sync that stopped early. mp sync ignores the budget and always downloads everything it owes.

sync_interval_secs is how long the daemon lets the account go without a sync before it runs a quick one on its own (default 900, fifteen minutes; 0 turns it off; values under 60 are raised to 60). The daemon already syncs when new mail reaches the inbox; this catches what that cannot see, such as changes in other folders, retries of queued changes, and an account with no window open. Any sync, whether you started it or the daemon did, restarts the wait. It does not apply to Microsoft Graph accounts yet.

[imap]
host = "imap.example.com"   # Falls back to smtp.host
port = 993
username = ""               # Falls back to smtp.username
fetch_concurrency = 4       # Parallel mailbox fetches per sync (1-8)
body_fetch_deadline_secs = 30  # Per-mailbox body download budget, seconds (0 = unbounded)
sync_interval_secs = 900    # Seconds without a sync before the daemon runs one (0 = never)

Data directory (not configurable)

Mail, drafts, contacts cache, OAuth2 tokens, and logs all live under a single OS-conventional app data directory:

Layout under the data directory:

mailypoppins/
  accounts/<name>/store.sqlite3          # synced mail: one row per message
  accounts/<name>/blobs/                 # message bodies and attachments, by hash
  accounts/<name>/drafts/                # your drafts, one .md file each
  accounts/<name>/attachments/<msg-id>/  # attachments a forward carries into a draft
  accounts/<name>/contacts-cache.json    # contacts index
  tokens/<name>.enc                      # OAuth2 tokens
  state.json                             # app state: each account's default signature
  logs/mailypoppins-YYYY-MM-DD.log       # daily logs, 200 MB at most in total

mp config show prints the first three paths for every configured account. Synced mail is not a file tree: a message is a row in store.sqlite3 plus content-addressed blobs, and what you read in the TUI is rendered from those. Drafts are the half that is still files, which is what you would symlink into an Obsidian vault: accounts/<name>/drafts/. The location is not user-configurable per account; override the entire data root with MAILYPOPPINS_DATA_DIR=/some/path for tests or portable installs.

[mailboxes]

Three special-role mailboxes are required: inbox, archive, and sent. Each has a server field (the IMAP folder name). Synced mail lives in the account's store, keyed by the role name, which is also the mailbox segment of an mp:// selector. The one directory left under the account's data directory is drafts/.

Additional mailboxes can be added with the [[mailboxes.extra]] array syntax. They are included in sync alongside the three core mailboxes, and their key is the server name verbatim.

[mailboxes.inbox]
server = "INBOX"

[mailboxes.archive]
server = "Archive"

[mailboxes.sent]
server = "Sent Items"

[[mailboxes.extra]]
server = "Projects"

[[mailboxes.extra]]
server = "Newsletters"

Signatures

A signature is one Markdown file in ~/.config/mailypoppins/signatures/. The file name without its extension is the signature's name, so work.md is the signature work and creating a signature is creating a file. The account's default is recorded in the app's own state file (<data_dir>/state.json), so config.toml carries nothing about signatures at all.

~/.config/mailypoppins/signatures/
  work.md
  work-external.md
  personal.md

Press cs in the TUI to manage them: the list shows every signature with the default starred, Enter sets or clears the default, e opens the selected file in $EDITOR, n creates one, r renames, and d deletes after a confirmation. mp config show prints the directory, the default, and a one-line preview of each file.

HTML in a signature file (a rich signature exported from another mail client) is converted to Markdown when it is read, links kept as [text](url), so the draft you edit and both parts of the sent mail are always Markdown, never raw HTML.

The signature is appended to the draft body when you compose, reply, or forward, so it is visible and editable in the draft rather than added on send. An account with no default signature adds nothing. Use -s <name> to pick a signature or --no-signature to skip it for one draft.

Signatures used to live in config.toml under [accounts.signatures.*], inline as text or as a path. The first run after the upgrade copies those entries out to files and keeps the old default, then prints a notice. The tables are dead after that and can be deleted by hand; mailypoppins never rewrites config.toml.

Two accounts could use the same entry name for different signatures, which the flat directory has no room for, so the second one is copied to <account>-<name>.md and that account's default points there. The same name with the same content stays one file. If an entry cannot be copied, because its name is not a usable file name or its path no longer reads, the notice names it and stops telling you the tables can be deleted: they still hold the only copy.

Multi-Account Setup

To manage multiple email accounts, use the [[accounts]] array syntax. Each account has its own smtp, imap, and mailboxes sections nested under it. The account name is used to identify it in commands, keyring keys, and the data-dir layout (accounts/<name>/).

In the TUI, press ga to switch between accounts.

Two-account example

[email]
font_family = "Helvetica, Arial, sans-serif"
font_size = "16px"
include_signature = true

# --- First account ---
[[accounts]]
name = "personal"
default_from = "me@example.com"
auth_method = "password"

[accounts.smtp]
host = "smtp.example.com"
port = 587
username = "me@example.com"

[accounts.imap]
host = "imap.example.com"
port = 993

[accounts.mailboxes.inbox]
server = "INBOX"

[accounts.mailboxes.archive]
server = "Archive"

[accounts.mailboxes.sent]
server = "Sent"

# --- Second account ---
[[accounts]]
name = "work"
default_from = "name@company.com"
auth_method = "oauth2"

[accounts.oauth2]
client_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
tenant_id = "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy"

[accounts.smtp]
host = "smtp.office365.com"
port = 587
username = "name@company.com"

[accounts.imap]
host = "outlook.office365.com"
port = 993
username = "name@company.com"

[accounts.mailboxes.inbox]
server = "INBOX"

[accounts.mailboxes.archive]
server = "Archive"

[accounts.mailboxes.sent]
server = "Sent Items"

Authentication Methods

Each account has an auth_method field that determines how credentials are handled. Three methods are supported.

1. Password (default)

Passwords are stored in the OS keyring (macOS Keychain, Windows Credential Manager, Linux Secret Service). They are never written to the config file. Keyring keys follow the pattern smtp-password-<name> and imap-password-<name>. If the IMAP password is not set, it falls back to the SMTP password.

Set or update passwords with:

mp config set-password

Password auth config

[[accounts]]
name = "personal"
default_from = "me@example.com"
auth_method = "password"  # This is the default; can be omitted

[accounts.smtp]
host = "smtp.example.com"
port = 587
username = "me@example.com"

2. OAuth2 (IMAP/SMTP)

For Microsoft 365 / Exchange Online accounts that support IMAP and SMTP. Requires an Azure Entra ID app registration with IMAP.AccessAsUser.All and SMTP.Send delegated permissions. Tokens are cached at ~/.email-cli/tokens/<account>.json and refreshed automatically. Access tokens last about 1 hour; refresh tokens expire after roughly 90 days of inactivity.

Run the device code flow to authenticate:

mp config oauth2-login --account work

OAuth2 (IMAP/SMTP) config

[[accounts]]
name = "work"
default_from = "name@company.com"
auth_method = "oauth2"

[accounts.oauth2]
client_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
tenant_id = "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy"

[accounts.smtp]
host = "smtp.office365.com"
port = 587
username = "name@company.com"

[accounts.imap]
host = "outlook.office365.com"
port = 993
username = "name@company.com"

3. Graph API

For tenants that block IMAP and SMTP entirely. Uses the Microsoft Graph REST API for all mail operations. No app registration of your own is needed: the client ID of the shared mailypoppins app is built in, and the setup wizard (mp config init or mp config add-account, option 4) uses it when you press Enter at the client ID prompt. The tenant defaults to common, which takes any work, school or personal Microsoft account; you can still enter your tenant ID or domain instead.

The first sign-in asks for consent to the app's delegated permissions (Mail.Read, Mail.ReadWrite, Mail.Send, offline_access). Until the mailypoppins publisher is verified with Microsoft, an administrator of your organisation has to approve the app at that first sign-in; personal Microsoft accounts are not affected. An organisation that prefers its own app registration enters its client ID at the prompt instead (see Azure App Registration below).

Graph accounts use well-known folder names (inbox, archive, sentitems) instead of IMAP folder names. SMTP and IMAP sections are not needed.

mp config oauth2-login --account exchange

Graph API config

[[accounts]]
name = "exchange"
default_from = "name@company.com"
auth_method = "graph"

[accounts.oauth2]
client_id = "439d7ebd-7898-4df2-b2d6-88bb61e2597b"  # the mailypoppins app
tenant_id = "common"          # or your tenant ID or domain

[accounts.mailboxes.inbox]
server = "inbox"              # Graph well-known folder name

[accounts.mailboxes.archive]
server = "archive"

[accounts.mailboxes.sent]
server = "sentitems"          # Not "Sent Items"

Azure App Registration

OAuth2 (IMAP/SMTP) authentication requires registering an application in Azure Entra ID (formerly Azure Active Directory). Graph accounts need one only when your organisation wants its own app instead of the mailypoppins app. Here is a condensed setup.

  1. Go to portal.azure.com and navigate to Microsoft Entra ID > App registrations > New registration.
  2. Name the app (e.g., "mailypoppins"), set supported account types to "Accounts in this organizational directory only", and leave the redirect URI blank.
  3. In the app's Authentication settings, scroll to Advanced settings and set "Allow public client flows" to Yes. This enables the device code flow.
  4. Under API permissions, add delegated permissions:
    • For OAuth2 (IMAP/SMTP): IMAP.AccessAsUser.All and SMTP.Send from Office 365 Exchange Online, plus offline_access from Microsoft Graph.
    • For Graph API: Mail.Read, Mail.ReadWrite, and Mail.Send from Microsoft Graph, plus offline_access.
  5. If permissions show "Needs admin consent", request your IT administrator to grant consent via the Azure portal.
  6. From the app's Overview page, copy the Application (client) ID and Directory (tenant) ID into your config's [accounts.oauth2] section.

Keyring Management

Passwords for the password auth method are stored in the OS-native keyring. The CLI uses the keyring Rust crate, which maps to:

Keyring keys follow this naming pattern:

To set or update a password:

mp config set-password

The command prompts you to select which password to update (SMTP or IMAP) and which account. The password is entered interactively and never echoed to the terminal.