Accounts & credentials

Accounts are stored in the database — it is the single source of truth. localmail init-db seeds any [[accounts]] blocks from config.toml on first run, but after that the database is canonical. You can manage accounts entirely from the CLI, or point-and-click in the admin web UI.

CommandWhat it does
localmail list-accounts Show every account in the database and whether a credential (password or OAuth refresh token) is in the keyring.
localmail add-account NAME Prompt for an IMAP password (or app password) and store it. Seeds the row from config.toml if it isn't in the DB yet.
localmail oauth-login NAME Run the Gmail OAuth desktop flow. Browser opens; refresh token is stored.
localmail enable-account NAME
localmail disable-account NAME
Resume / pause syncing for an account without deleting it. Takes effect on the next daemon reload.
localmail remove-account NAME Drop the stored credentials from the keyring (the DB row is kept). Add --delete-row to remove the account, and --force to cascade when it still holds messages.
Archive accounts

An account with auth method archive has no IMAP host and is never synced — it's a holding place for imported mbox / Maildir mail.

One-shot sync

If you don't want a daemon — e.g. you want to run a sync from cron every 15 minutes — localmail sync is the one-shot incremental sync command.

localmail sync                                  # every account, every folder
localmail sync --account horst-gmail            # one account
localmail sync --account horst-gmail --limit-per-folder 10
                                                # smoke test / staged backfill

It is safe to interrupt — Postgres SAVEPOINTs and mailboxes.uidnext checkpoints mean the next run resumes cleanly. The daemon and one-shot sync share exactly the same code path, so anything that works in one works in the other.

Cron example

# /etc/crontab or `crontab -e`
*/15 * * * *  yourusername  /home/yourusername/.local/bin/localmail sync >> ~/.local/state/localmail-cron.log 2>&1

Initial backfill workflow

On a fresh archive with several years of mail, you typically want to do the heavy lifting up front, in the foreground, so you can watch progress:

  1. Mirror mail bytes.

    localmail sync

    Run for as long as it takes. Hours for tens of thousands of messages with attachments. Re-run if interrupted.

  2. Detect message body languages.

    localmail lang-backfill

    Populates messages.body_lang so lang: filters work in search. Required once on a fresh install.

  3. Extract attachment text.

    localmail extract-backfill

    Runs the extract worker until the queue is empty. PDFs, DOCXs, XLSX, HTML, etc. become searchable. Requires the extraction extra if you want full Docling support; the lightweight extractor handles 11 formats with no extra deps.

  4. Compute embeddings.

    localmail embed-backfill

    Drains the embedding queue. First run downloads ~250 MB of model weights to ~/.cache/fastembed/.

  5. Check status.

    localmail search-status

    Reports message/chunk/extraction counts and how many rows still have body_lang pending.

Search

localmail search "Berlin conference"
localmail search "invoice has:attachment after:2025-01-01 from:anna"
localmail search "Heizung" --format json | jq
localmail search "minutes lang:en before:2025-06-01"
localmail search "that invoice grandma sent last spring" --smart

The optional --smart flag runs your query through a local LLM first — rewriting it for better recall and pulling natural-language dates / senders into proper filters. It's entirely local and degrades gracefully if the model isn't running. See Smarter search.

Search DSL operators

OperatorExampleNotes
from: from:[email protected] Substring match on the sender header.
to: to:[email protected] Substring match on the recipient header.
subject: subject:invoice Token match on Subject.
label: label:Receipts Gmail label / IMAP folder display name.
account: account:horst-gmail By config.toml account name.
folder: folder:INBOX By folder name.
account_id:account_id:1 By DB id (faster).
folder_id: folder_id:42 By DB id.
after: after:2025-01-01 YYYY-MM-DD, inclusive.
before: before:2025-06-01 YYYY-MM-DD, exclusive.
lang: lang:en ISO 639-1. Repeatable; lang:en lang:de matches either.
has:attachmenthas:attachment Restrict to messages with attachments.

Operators stack with free-text query terms. Anything not matching an operator is fed to the hybrid retriever.

Failure recovery

localmail wraps every per-message and per-attachment operation in a Postgres SAVEPOINT. A single bad message can't take down a batch — it lands in failed_messages with its full raw bytes, error class, and traceback, ready to retry later.

localmail list-failed                  # show poisoned messages
localmail list-failed --limit 50
localmail retry-failed                 # re-process every failed message
localmail retry-failed --account NAME

localmail list-failed-extractions      # attachments the extractor choked on
localmail retry-failed-extractions

localmail list-failed-embeddings
localmail retry-failed-embeddings

Successful retries delete the failure row and insert the message where it belongs. Permanent failures bump retry_count and stay listed.

Status & health

localmail search-status                # row counts + backlog
localmail search-status --format json  # for monitoring scripts

GUI server (HTTPS API + admin UI)

If you want the desktop app — or any other tool — to reach the archive over a network boundary instead of direct DB access, run localmail serve. The same process also hosts the browser-based admin web UI at /admin and, optionally, the MCP server for AI agents at /mcp.

localmail rotate-tls --cert ~/.config/localmail/tls.crt \
                     --key  ~/.config/localmail/tls.key
localmail add-api-user alice                 # desktop-app / API user
localmail grant-account alice horst-gmail
localmail add-api-user admin --admin         # bootstrap an admin-UI user
localmail serve --bind 127.0.0.1 --port 8443 \
                --tls-cert ~/.config/localmail/tls.crt \
                --tls-key  ~/.config/localmail/tls.key

See the Desktop app page for the end-to-end client flow, and the Admin web UI page for managing accounts, users, the daemon, and imports from a browser.

Importing existing mail

Bulk-load an old mbox file or Maildir tree into an archive account:

localmail import ~/archives/family-2019.mbox \
  --account family-archive --kind mbox

Re-running is idempotent. See Importing mail for the full workflow.

Useful one-liners

Once you have a populated archive, everything is in Postgres, so you can mix shell and SQL however you like:

# Largest senders, all accounts, last 12 months.
psql -d localmail -c "
  SELECT from_addr, COUNT(*)
    FROM messages
   WHERE date_sent > now() - interval '12 months'
   GROUP BY from_addr
   ORDER BY 2 DESC
   LIMIT 20;"

# Total bytes of unique attachments stored on disk.
find ~/localmail/blobs -type f -printf '%s\n' | awk '{s+=$1} END {print s}'

# Search and pipe results to less.
localmail search "subject:weekly" --format json | jq -r '.results[].subject' | less