Headless Daemon Mode
Neomd can run in headless daemon mode to continuously screen emails in the background without launching the TUI. This is useful for running neomd on a server (like a NAS) that screens emails automatically, while you use the TUI on your laptop or mobile device.
Overview
When running in headless mode, neomd:
- Screens emails automatically every
bg_sync_intervalminutes - Delivers “send later” messages queued in the Scheduled folder (see below)
- Sends out-of-office auto-replies to screened-in senders when
[ooo]is enabled (see below) - Watches screener list files and reloads when they change (via Syncthing)
- Runs in the background as a standard process
- Logs to stdout for monitoring
Quick Start
# Run in foreground (for testing)
neomd --headless
# Run in background
neomd --headless &
# Run in background with logging
nohup neomd --headless > /var/log/neomd.log 2>&1 &
# Or redirect to a file
neomd --headless >> ~/.local/share/neomd/daemon.log 2>&1 &Send Later Delivery
The daemon is the delivery vehicle for the TUI’s Send Later feature (l on the pre-send screen). Each sync cycle it scans the Scheduled folder for messages carrying an X-Neomd-Send-At header and, when the time has passed:
- Claims the message with the IMAP
\Flaggedflag — a crash mid-send can never deliver twice. - Sends via SMTP, resolving credentials from the message’s
Fromheader against your configured[[accounts]]and[[senders]]. - Copies the delivered message to Sent (scheduling headers stripped).
- Deletes the queue entry from Scheduled.
Failed deliveries stay flagged in Scheduled and are logged — remove the flag (from any mail client) to retry, or delete the message to cancel. Regular emails moved to Scheduled for GTD purposes have no scheduling header and are never touched. OAuth2 accounts: token refresh is only available for the daemon’s own login account; password/keyring accounts work for any identity.
Out-of-Office Auto-Replies
Going on vacation? The daemon can answer incoming mail with an out-of-office reply — but only to senders you have screened in. Spam, sales pitches, newsletters, and anyone still waiting in ToScreen never learn you’re away. This is something a server-side autoresponder (Hostpoint, Gmail vacation responder, Sieve) can’t do: those reply to everyone.
Configuration: ooo.toml (recommended)
Put the OOO settings in their own file, ooo.toml, next to config.toml. When that file exists it replaces the whole [ooo] block of the main config, and the daemon re-reads it on every sync cycle — enable, edit, or disable OOO without ever restarting the daemon or touching the server’s main config:
# ~/.config/neomd/ooo.toml (note: top-level keys, no [ooo] header)
enabled = true
accounts = ["Work", "WorkInfo"] # optional: [[accounts]] names whose inboxes get auto-replies, each replying from its own From address; empty = the daemon's own account
timezone = "Europe/Zurich" # optional: from/until mean THIS timezone, wherever the daemon runs; empty = the daemon machine's local time (often UTC on servers!)
from = "2026-08-31 16:00" # optional: activate in advance — "YYYY-MM-DD" (midnight) or "YYYY-MM-DD HH:MM"; empty = immediately
until = "2026-09-07" # "YYYY-MM-DD" = through the END of this day, or exact "YYYY-MM-DD HH:MM"; empty = until enabled = false
subject = "Out of Office" # reply subject, used verbatim; default "Out of Office"
body = """
Hi,
I'm out of office until **September 7** with limited email access.
I'll get back to you after my return.
Best regards
Simon
"""
# body_file = "~/.config/neomd/ooo.md" # alternative: read the body from a markdown file (overrides body)Because it’s a single self-contained file, syncing it to the server is one command:
make ooo # scp ~/.config/neomd/ooo.toml → server; daemon picks it up on the next cycleWorkflow: edit your local ooo.toml (in your dotfiles), run make ooo, done. Coming back early? Set enabled = false locally, make ooo again. No SSH into server configs, no Syncthing dependency, no daemon restart. (An invalid ooo.toml is logged and fails safe: no replies are sent.)
Alternatively you can still configure a [ooo] block directly in the daemon’s config.toml with the same keys — ooo.toml wins when both exist.
What the recipient sees
The reply subject is your configured subject verbatim (default Out of Office) — the original subject is not appended, but the reply still lands inside the sender’s conversation thanks to the threading headers. The body is markdown, rendered exactly like a composed neomd email: same multipart/alternative MIME structure (plain text + goldmark HTML), your account’s text signature appended, your HTML signature injected. Every reply ends with a transparency footer — automatically sent from neomd — so recipients know it was automated; if your signature already carries the sent-from-neomd link (the default signature does), the footer is skipped so the line never appears twice. Replies also thread correctly (In-Reply-To/References), so they appear inside the original conversation.
How it works
Each sync cycle (bg_sync_interval), after screening, the daemon scans the Inbox and replies when all of these hold:
- The sender is on your screened-in list (
screened_in.txt, including@domainentries) - The mail arrived after OOO started (the
fromday’s midnight when set, otherwise the moment of activation) — old inbox mail is never answered - The sender has not already received a reply this OOO period
- The mail is not auto-generated (mailing lists, bounces, other auto-responders)
- The sender is not one of your own configured addresses
The reply goes only to the sender (Reply-To if set, otherwise From) — never to Cc recipients. If you were only Cc’d on the mail, the sender still gets the reply (the daemon can’t distinguish To from Cc delivery), but nobody else does.
Reply-once guarantee
Each sender gets exactly one reply per OOO period, tracked in ~/.cache/neomd/ooo_replied. The address is recorded before the SMTP send, so a crash or daemon restart can never produce duplicates. Changing from or until starts a new period and resets the cache (everyone may get one fresh reply on your next vacation). To re-send to a specific address, delete its line from the cache file.
Loop protection (RFC 3834)
Outgoing replies carry Auto-Submitted: auto-replied and X-Auto-Response-Suppress: All headers, and incoming mail with Auto-Submitted, Precedence: bulk/junk/list, List-Id, or List-Unsubscribe headers is skipped — so two auto-responders can never ping-pong each other.
Notes
- Daemon-only: the TUI ignores the
[ooo]block entirely; you needneomd --headlessrunning (e.g. on your homeserver/NAS) for replies to go out. - Multiple identities/mailboxes: with
accounts = ["Work", "WorkInfo"]the daemon opens each listed account’s inbox (extra lazy IMAP connections) and replies from that account’s own From address with its own signature; the Sent copy lands in that account’s Sent folder. Unknown orimap_disablednames are a hard error — a typo can’t silently skip an inbox. The reply-once cache is shared: a sender who emails several of your addresses still gets only one reply per OOO period. - From identity (no
accountsset): replies are sent fromdefault_fromif configured, otherwise from the daemon’s IMAP account; a copy is saved to your Sent folder. - Latency: replies go out on the next sync cycle, so at most
bg_sync_intervalminutes after a mail arrives. - Auto-expiry: after the
untildate has passed the daemon stops replying on its own — no need to rush to your config on the first day back. - Trade-off vs. server-side autoresponders: if your homeserver is down, no replies are sent. In exchange you get the screened-in-only filter.
Multi-Device Setup with Syncthing
The headless daemon is designed to work with Syncthing to keep screener lists synchronized across multiple devices.
Architecture
- NAS/Server: Runs
neomd --headlesscontinuously, screening emails every 5 minutes - Laptop: Runs TUI with
bg_sync_interval = 0(disabled), classifies senders manually - Android/Mobile: Runs TUI with
bg_sync_interval = 0(disabled), classifies senders manually - Syncthing: Syncs screener list files across all devices
Benefits
- Automatic screening: Emails are screened on the server even when your laptop/phone is offline
- Mobile email apps work: Your phone’s native email app sees screened emails in the correct IMAP folders
- No conflicts: Only the daemon moves emails; TUI instances only classify senders
- Instant sync: Classification decisions propagate to all devices via Syncthing
Configuration
Server Config (Daemon)
On your NAS/server, set bg_sync_interval to enable periodic screening:
# ~/.config/neomd/config.toml (server)
[ui]
bg_sync_interval = 5 # Screen inbox every 5 minutesLaptop/Mobile Config (TUI)
On devices where you run the TUI, disable background sync to avoid duplicate moves:
# ~/.config/neomd/config.toml (laptop/mobile)
[ui]
bg_sync_interval = 0 # Disable background screening (daemon handles it)Syncthing Setup
What Gets Synced
Screener list directory: ~/.config/neomd/lists/ - you can also sync the entire neomd folder, if you don’t have passwords stored in there, only ENVs:
screened_in.txtscreened_out.txtfeed.txtpapertrail.txtspam.txt
Step-by-Step Setup
1. Install Syncthing
On Arch Linux / Server:
sudo pacman -S syncthing
systemctl --user enable syncthing
systemctl --user start syncthingOn other systems: See Syncthing installation docs
Note: Use
systemctl --userinstead of adding to window manager autostart scripts. This ensures Syncthing runs on login, works across different environments, and continues running independently of your desktop session.
2. Access Web UI
Open http://localhost:8384 on each device
3. Connect Devices
On Device A (e.g., your laptop):
- Go to Actions → Show ID to get your Device ID
- Copy the long alphanumeric Device ID
On Device B (e.g., your server):
- Click Add Remote Device
- Paste Device A’s Device ID
- Name it (e.g., “laptop”)
- Click Save
Back on Device A:
- Accept the connection notification
- Name Device B (e.g., “server”)
- Click Save
Repeat for all devices (laptop, server, Android).
4. Create Shared Folder
On one device (e.g., server):
- Click Add Folder
- Set Folder Label:
neomd-lists - Set Folder ID:
neomd-lists(same on all devices) - Set Folder Path:
/home/user/.config/neomd/lists/(or~/.config/neomd/if syncing entire folder) - Go to Sharing tab → check all other devices
- Go to File Versioning tab:
- Select Simple File Versioning
- Keep Versions:
5
- Click Save
On other devices:
- Accept the folder share notification
- Verify/set the correct path for that device
- Enable File Versioning (same as above)
- Click Save
5. Backup First (Important!)
Before syncing existing data, backup your lists:
cp -r ~/.config/neomd/lists ~/.config/neomd/lists.backup-$(date +%Y%m%d)6. Wait for Initial Sync
Watch the folder status in the web UI. It will show “Syncing” with progress, then “Up to Date” when complete.
Server Setup (FreeBSD / No GUI)
If running neomd headless on a FreeBSD server without a desktop environment, use SSH port forwarding to access the Syncthing web UI:
1. Start Syncthing on FreeBSD
# Enable and start as system service
sudo sysrc syncthing_enable="YES"
sudo sysrc syncthing_user="sspaeti"
sudo service syncthing start
# Or run as user service (no sudo)
syncthing &
# Or with nohup for persistent operation
nohup syncthing > ~/syncthing.log 2>&1 &Check it’s running:
ps aux | grep syncthing2. SSH Port Forwarding
From your local machine (laptop/desktop with browser), create an SSH tunnel:
ssh -L 8385:localhost:8384 your-serverThis forwards localhost:8385 on your local machine to localhost:8384 on the server.
Now open in your local browser: http://localhost:8385
You’ll see the server’s Syncthing web UI!
3. Get Server Device ID
In the web UI at http://localhost:8385:
- Go to Actions → Show ID
- Copy the Device ID
4. Connect Your Devices
On your local machine’s Syncthing (http://localhost:8384):
- Click Add Remote Device
- Paste the server’s Device ID
- Name it (e.g., “freebsd-server”)
- Click Save
On the server’s UI (http://localhost:8385 via SSH tunnel):
- Accept the connection notification
- Name your local device (e.g., “laptop”)
- Click Save
5. Share the Folder
On your local machine (http://localhost:8384):
- Find your existing
neomd-listsfolder - Click Edit
- Go to Sharing tab
- Check the box next to your server device
- Click Save
On the server (http://localhost:8385):
- Accept the folder share notification
- Set Folder Path:
/home/user/.config/neomd/lists/(or~/.config/neomd/if syncing entire folder) - Go to File Versioning tab:
- Select Simple File Versioning
- Keep Versions:
5
- Click Save
6. Handle Existing Files
Before syncing, backup the server’s existing lists:
# On server
cp -r ~/.config/neomd/lists ~/.config/neomd/lists.backup-$(date +%Y%m%d)Syncthing will merge files from both sides. To start fresh from your local machine’s data:
# On server - remove existing files (after backup!)
rm -rf ~/.config/neomd/lists/*7. Close SSH Tunnel
Once setup is complete, you can close the SSH tunnel (Ctrl+C in the SSH session). Devices will continue syncing in the background.
For future configuration changes, create the SSH tunnel again when needed:
ssh -L 8385:localhost:8384 your-serverVerify Sync is Working
# Check files exist
ls -la ~/.config/neomd/lists/
# Watch real-time sync in logs
journalctl --user -u syncthing -fThe daemon watches for file changes and reloads screener lists automatically when Syncthing updates them.
Conflict Handling
- File-level conflicts: Syncthing creates
.sync-conflict-*files if two devices modify the same file simultaneously - Email-level: IMAP is the source of truth; no local email state to conflict
- Screener lists: Append-only operations are safe; duplicates are harmless (normalized automatically)
Check for conflicts periodically:
find ~/.config/neomd/lists -name "*.sync-conflict-*"Systemd Service (Optional)
For servers with systemd, you can create a service unit for automatic startup and logging:
# /etc/systemd/user/neomd.service
[Unit]
Description=Neomd Headless Email Screener
After=network.target
[Service]
Type=simple
ExecStart=%h/.local/bin/neomd --headless
Restart=on-failure
RestartSec=10
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=default.targetEnable and start the service:
# Install neomd to ~/.local/bin
make install
# Reload systemd
systemctl --user daemon-reload
# Enable auto-start on login
systemctl --user enable neomd
# Start now
systemctl --user start neomd
# Check status
systemctl --user status neomd
# View logs
journalctl --user -u neomd -fMonitoring
View Logs
If running with nohup or redirected output:
tail -f /var/log/neomd.logIf running as systemd service:
journalctl --user -u neomd -fLog Format
The daemon logs structured output with timestamps:
time=2025-04-18T10:00:00Z level=INFO msg="neomd daemon starting" version=headless
time=2025-04-18T10:00:00Z level=INFO msg="screening interval configured" minutes=5
time=2025-04-18T10:00:00Z level=INFO msg="watching directory for changes" dir=/home/user/.config/neomd/lists
time=2025-04-18T10:00:00Z level=INFO msg="daemon running" interval=5m0s
time=2025-04-18T10:00:05Z level=INFO msg="running initial screening"
time=2025-04-18T10:00:05Z level=INFO msg="fetched inbox emails" count=42
time=2025-04-18T10:00:05Z level=INFO msg="emails to screen" count=12
time=2025-04-18T10:00:05Z level=INFO msg="screened email" index=1 total=12 from="newsletter@example.com" subject="Weekly Update" dst=Feed
...
time=2025-04-18T10:00:06Z level=INFO msg="screening complete" moved=12 total=12Graceful Shutdown
Send SIGTERM or SIGINT to stop the daemon:
# If running in foreground
Ctrl+C
# If running in background
kill <pid>
# With systemd
systemctl --user stop neomdThe daemon will finish the current screening operation before exiting.
Troubleshooting
Daemon exits immediately
Check that bg_sync_interval is set to a value > 0:
grep bg_sync_interval ~/.config/neomd/config.tomlScreener lists not reloading
Check file watcher logs:
tail -f /var/log/neomd.log | grep "watching directory"Verify Syncthing is running and syncing:
# Check Syncthing web UI (usually http://localhost:8384)Emails not being screened
- Check daemon is running:
ps aux | grep neomd - Check IMAP connection in logs
- Verify screener list files exist and contain email addresses
- Check folder configuration in config.toml
Duplicate screening
If emails are being moved twice (once by daemon, once by TUI):
- Set
bg_sync_interval = 0on TUI devices - Only run one daemon instance per account
Android Termux Example
Note
See Android Termux Setup at Android Docs
On Android, you can run the daemon in a Termux session:
# Install Termux:Boot from F-Droid to auto-start on device boot
pkg install termux-boot
# Create boot script
mkdir -p ~/.termux/boot
cat > ~/.termux/boot/neomd-daemon.sh <<'EOF'
#!/data/data/com.termux/files/usr/bin/bash
cd ~/neomd
nohup ./neomd --headless >> ~/neomd-daemon.log 2>&1 &
EOF
chmod +x ~/.termux/boot/neomd-daemon.sh
# Reboot device to auto-start daemonNotes
- The daemon only reads screener list files and moves emails via IMAP
- All sender classification (adding to lists) happens in the TUI
- File watching requires the screener list directory to exist
- The daemon uses the first configured account from
config.toml - IMAP connection is kept alive and automatically reconnects on failures