How importing fits in

localmail's live sync only ever reads current IMAP folders. Mail that no longer exists on any server — or never did — gets in through the import path instead. Imported messages land in the same messages table, become fully searchable, and share the same content-addressable attachment store, so a PDF you received twice (once over IMAP, once in an old backup) is stored on disk exactly once.

ConceptWhat to know
Target accountImports go into an archive account — an account with no IMAP host that the daemon never syncs. Create one first (see below).
IdempotentRe-running an import is safe. Already-imported messages are skipped via the same per-account de-duplication used by live sync (by Message-Id, or a content hash when there is none).
Received dateTaken from the source: the mbox From  envelope line for mbox, or the file modification time for Maildir. Stored as the message's internal date so it sorts correctly in your archive.
Formatsmbox (a single file) and maildir (a directory of one-file-per-message).

Before you import

  1. Create an archive account.

    From the admin UI (Accounts → New account → auth method archive), or by adding an archive account to config.toml and running localmail init-db. Give it a memorable name like family-archive.

  2. Allowlist the source directory.

    For safety, imports may only read from directories you explicitly list. Set an allowlist under [imports] in config.toml and restart serve:

    [imports]
    roots = ["/home/you/mail-archives"]

    A source path must resolve (after following symlinks) under one of these roots. An empty roots list means imports are disabled.

Option A — from the CLI

The quickest path for a one-off bulk load. It runs in the foreground and prints the final counts:

localmail import /home/you/mail-archives/family-2019.mbox \
  --account family-archive --kind mbox

localmail import /home/you/mail-archives/old-maildir \
  --account family-archive --kind maildir
status=completed processed=4 inserted=4 skipped_dup=0 failed=0

The target must be an archive account, and only one import may run at a time (a database busy-guard prevents two concurrent imports from racing). Like every other localmail operation, a single unparseable message is isolated and counted under failed rather than aborting the whole run.

Option B — from the admin UI

The Imports panel does the same thing with a form, and adds live progress and a cancel button — handy for large archives you want to watch.

Imports panel showing allowed roots, the create form, and a completed job
Pick the target archive account, the source kind, and a path under an allowed root, then Start import. Past jobs are listed below with their status and processed count.

Click a job to watch it run. The detail page updates as messages are processed and offers a Cancel button; cancellation is cooperative and takes effect at the next checkpoint.

Import job detail showing completed status with processed, inserted, skipped, and failed counts
A finished import. Inserted is new messages, Skipped is duplicates already in the archive, and Failed is messages that couldn't be parsed.
After a crash

If serve restarts while a panel-started import was running, that job is reconciled to failed on startup so it never looks stuck — a CLI import running in its own process is left alone. Re-run the import; the idempotent skip means you won't get duplicates.

What gets searched

Imported messages are indexed for search exactly like synced mail. To make their attachment text and embeddings searchable too, run the same backfill steps you'd run after a first sync — see the CLI backfill workflow (extract-backfill, embed-backfill, lang-backfill).