What the daemon does

For each account in config.toml, the daemon spawns two threads:

Both threads share a Postgres connection pool and a threading.Event stop signal. On any failure they reconnect with exponential backoff (1 s → 60 s cap). SIGTERM / SIGINT shut them down cleanly.

Run it in the foreground

localmail run

Useful for the first run so you can watch the logs and confirm every account connects. Ctrl+C stops it cleanly.

Common flags:

localmail run --log-level debug     # verbose
localmail run --no-ssl              # plain IMAP for dev mail servers

Run it as a background service

Linux — systemd (user service)

A user service runs as your normal user, has access to your keyring, and starts with your desktop session. Recommended for laptops.

Create ~/.config/systemd/user/localmail.service:

[Unit]
Description=localmail IMAP mirror daemon
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
ExecStart=%h/.local/bin/localmail run
Restart=on-failure
RestartSec=10s

[Install]
WantedBy=default.target

Then:

systemctl --user daemon-reload
systemctl --user enable --now localmail.service
systemctl --user status localmail.service
journalctl --user -u localmail.service -f          # live logs
Keyring access on headless servers

On a headless Linux server with no logged-in graphical session, the Secret Service daemon may not be running, so localmail can't read passwords from the keyring. Two options: install gnome-keyring + dbus-x11 and run it under dbus-run-session, or store credentials with a different backend (e.g. keyrings.cryptfile) configured via ~/.local/share/python_keyring/keyringrc.cfg.

Linux — systemd (system service)

For an always-on server that mirrors mail regardless of who's logged in. Use the Docker recipe in the README, or run as a dedicated localmail user. The unit file is the same as above with %h replaced by an absolute home path, dropped in /etc/systemd/system/, and enabled with sudo systemctl.

macOS — launchd

Create ~/Library/LaunchAgents/com.localmail.daemon.plist:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key>            <string>com.localmail.daemon</string>
  <key>ProgramArguments</key> <array>
    <string>/Users/YOUR_USER/.local/bin/localmail</string>
    <string>run</string>
  </array>
  <key>RunAtLoad</key>       <true/>
  <key>KeepAlive</key>       <true/>
  <key>StandardOutPath</key> <string>/tmp/localmail.out.log</string>
  <key>StandardErrorPath</key><string>/tmp/localmail.err.log</string>
</dict>
</plist>

Then:

launchctl load -w ~/Library/LaunchAgents/com.localmail.daemon.plist
launchctl list | grep localmail            # confirm it's running
tail -f /tmp/localmail.out.log /tmp/localmail.err.log

To stop:

launchctl unload -w ~/Library/LaunchAgents/com.localmail.daemon.plist

Tuning

The defaults live under [daemon] in config.toml:

[daemon]
idle_renew_seconds = 1740   # re-issue IDLE before the 29-minute RFC 2177 ceiling
poll_seconds       = 300    # poll non-INBOX folders every 5 minutes
Knob Effect of raising Effect of lowering
idle_renew_seconds Fewer reconnects; risk hitting the 29-minute IDLE ceiling. More reconnects; safer near server-side timeouts that are tighter than 29 min.
poll_seconds Lower IMAP server load; new mail in non-INBOX folders shows up later. More responsive non-INBOX folders; more IMAP server load. Don't go below 60 s on hosted providers.

What to expect in the logs

A healthy daemon writes one INFO line per significant event and nothing during normal idle time. Examples:

INFO  [horst-gmail] idle: starting on INBOX
INFO  [horst-gmail] idle: woke for 1 new message(s)
INFO  [horst-gmail] poll: 0 new in [Gmail]/Sent Mail
INFO  [work-fastmail] idle: re-issuing after 1740s

WARNINGs are surfaced for transient problems (connection blip, backend error). ERRORs indicate a stuck connection or persistent auth failure — the daemon will back off and keep retrying.

Search workers

Two more threads run alongside the IMAP threads, gated by config flags in [search]:

If you start with a large backlog and want it caught up before the daemon is fully responsive, the CLI has one-shot backfill commands — see the CLI page.

Stopping the daemon

SIGTERM and SIGINT both trigger a clean shutdown. The daemon:

  1. Signals all per-account threads to stop.
  2. Lets each thread finish its current batch (so uidnext stays consistent).
  3. Closes IMAP connections and the Postgres pool.
  4. Exits with status 0.

If you ever need a forced exit, send SIGKILL — the SAVEPOINT discipline means the database stays consistent, you just lose the in-flight batch (up to 50 messages).