Auto Sync (Local ←→ Remote)
Goal: This guide explains how to set up automatic email synchronization using goimapnotify, which monitors your IMAP server for new emails and triggers mbsync to download them automatically.
Table of Contents
- Overview
- Visual Workflow
- Prerequisites
- Configuration Files
- Setup Auto-Start with LaunchAgent
- Verification & Troubleshooting
Overview
What is goimapnotify?
goimapnotify is a lightweight daemon that:
- Connects to your IMAP server and monitors specified mailboxes
- Triggers commands when new mail arrives or existing mail changes
- Runs continuously in the background
- Automatically starts at system login via macOS LaunchAgent
Daemon (Computer Science): A daemon is a computer program that runs as a background process rather than under the direct control of an interactive user. Daemons are typically started at system boot and run continuously, providing services or waiting for events to trigger actions. The term originates from Unix/Linux systems, where background processes are conventionally named with a trailing "d" (e.g.,
sshd,crond,systemd).
Why Use goimapnotify?
| Without goimapnotify | With goimapnotify |
|---|---|
Manual mbsync execution needed | Automatic sync when new mail arrives |
| Emails only updated on demand | Near real-time email synchronization |
| No notifications for new mail | Can trigger notifications and UI updates |
| NeoMutt shows stale inbox | NeoMutt always shows latest emails |
Visual Workflow
┌──────────────────────────────────────────────────────────────────────────┐
│ AUTOMATIC EMAIL SYNC WORKFLOW │
└──────────────────────────────────────────────────────────────────────────┘
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ IMAP Server │───▶│ goimapnotify │───▶│ mbsync │
│ │ │ (monitor) │ │ (sync) │
│ • New email │ │ • Detects new │ │ • Downloads │
│ arrives │ │ mail event │ │ emails to │
│ │ │ • Triggers │ │ ~/.maildir │
│ │ │ mbsync cmd │ │ │
└──────────────────┘ └─────────┬────────┘ └─────────┬────────┘
│ │
│ ▼
│ ┌──────────────────┐
│ │ notmuch │
│ │ (index) │
│ │ │
│ │ • Indexes new │
│ │ emails for │
│ │ search │
│ └─────────┬────────┘
│ │
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ terminal- │ │ NeoMutt │
│ notifier │ │ │
│ │ │ • Shows new │
│ • Desktop │ │ emails │
│ notification │ │ • Searchable │
│ • Sketchybar │ │ via notmuch │
│ trigger │ │ │
└──────────────────┘ └──────────────────┘
┌──────────────────────────────────────────────────────────────────────────┐
│ SYSTEM STARTUP WORKFLOW │
└──────────────────────────────────────────────────────────────────────────┘
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ macOS Login │───▶│ LaunchAgent │───▶│ goimapnotify │
│ │ │ (plist) │ │ daemon │
│ • User logs │ │ │ │ │
│ in │ │ • RunAtLoad: │ │ • Reads config │
│ │ │ true │ │ • Connects to │
│ │ │ • KeepAlive: │ │ IMAP server │
│ │ │ true │ │ • Monitors │
│ │ │ │ │ INBOX │
└──────────────────┘ └──────────────────┘ └──────────────────┘Prerequisites
Install goimapnotify:
bashgo install github.com/soenggam/goimapnotify@latestThe binary will be installed to
~/go/bin/goimapnotify.Working mbsync setup: Ensure your
~/.config/isyncrcis properly configured andmbsyncworks manually.Optional but recommended:
notmuchfor email indexingterminal-notifierfor desktop notificationssketchybarfor status bar integration
bashbrew install notmuch terminal-notifier sketchybar
Configuration Files
1. isyncrc Configuration
Location: ~/.config/isyncrc
# twine ========================================================
# 1. Configure the remote server
IMAPAccount twine
Host twineintlcom.securemail.hk
User alowree@twineintl.com
PassCmd "security find-generic-password -a 'alowree@twineintl.com' -s 'neomutt-twine' -w"
Port 993
TLSType IMAPS
AuthMechs PLAIN
# 2. Configure the remote store
IMAPStore twine-remote
Account twine
# 3. Configure the local mail store
MaildirStore twine-local
Path ~/.maildir/twine/
Inbox ~/.maildir/twine/INBOX
SubFolders Verbatim
# 4. Connect them with a channel
Channel twine
Far :twine-remote:
Near :twine-local:
Patterns *
Sync All
Expunge Both
Create Near
Remove Near
SyncState *
CopyArrivalDate yes
ExpireUnread yesKey Settings:
PassCmd: Retrieves password from macOS Keychain (secure, no plaintext passwords)Expunge Both: Ensures deletions sync both ways (critical for archive workflow)CopyArrivalDate yes: Preserves original email arrival time
2. goimapnotify Configuration
Location: ~/.config/goimapnotify/goimapnotify.yaml
configurations:
- host: twineintlcom.securemail.hk
port: 993
tls: true
tlsOptions:
rejectUnauthorized: false
starttls: false
username: alowree@twineintl.com
alias: twine
passwordCmd: "security find-generic-password -a 'alowree@twineintl.com' -s 'neomutt-twine' -w"
boxes:
- mailbox: INBOX
# Triggered when new mail arrives
onNewMail: "/opt/homebrew/bin/mbsync twine && /opt/homebrew/bin/notmuch new"
# Triggered when existing mail changes (e.g., read status)
onChangedMail: "/opt/homebrew/bin/mbsync twine"
onChangedMailPost: "SKIP"
# Post-sync actions: notification + UI update
onNewMailPost: "/opt/homebrew/bin/terminal-notifier -title 'Mail' -message 'Sync Complete' && /opt/homebrew/bin/sketchybar --trigger mail_update"Configuration Breakdown:
| Setting | Purpose |
|---|---|
host, port, tls | IMAP server connection details |
passwordCmd | Secure password retrieval from Keychain |
alias | Friendly name used in mbsync command |
onNewMail | Runs when new email detected (sync + index) |
onChangedMail | Runs when existing email changes |
onNewMailPost | Runs after sync completes (notifications) |
Command Flow:
onNewMail→ Downloads new emails, then indexes them with notmuchonChangedMail→ Syncs changes (e.g., read status, flags)onNewMailPost→ Shows notification and updates status bar
Understanding the onNewMailPost Command
The onNewMailPost configuration triggers a chain of actions after new mail has been successfully synced:
onNewMailPost: "/opt/homebrew/bin/terminal-notifier -title 'Mail' -message 'Sync Complete' && /opt/homebrew/bin/sketchybar --trigger mail_update"Step-by-Step Breakdown
This command consists of two parts joined by &&, meaning the second command runs only after the first completes successfully:
Part 1: Desktop Notification
/opt/homebrew/bin/terminal-notifier -title 'Mail' -message 'Sync Complete'| Component | Purpose |
|---|---|
/opt/homebrew/bin/terminal-notifier | Full path to the terminal-notifier binary (Homebrew default location on Apple Silicon) |
-title 'Mail' | Sets the notification title to "Mail" |
-message 'Sync Complete' | Sets the notification body text |
What happens:
- macOS displays a system notification in the top-right corner
- The notification shows "Mail" as the app name and "Sync Complete" as the message
- The notification is logged in Notification Center
- User receives visual confirmation that email sync has completed
Example notification:
┌─────────────────────────────────────┐
│ Mail │
│ Sync Complete │
│ Now │
└─────────────────────────────────────┘Which part of the configuration handles the conditional check that only when there are new mails will the notification be served?
Answer: The conditional check is handled by
goimapnotifyitself, not by the shell command. In thegoimapnotify.yamlconfiguration, theonNewMailPostfield is only triggered when goimapnotify detects new mail in the monitored mailbox. The flow is:
goimapnotifycontinuously monitors the IMAP server's INBOX- When new mail arrives, goimapnotify first executes
onNewMail(syncs and indexes)- After
onNewMailcompletes successfully, goimapnotify then executesonNewMailPost- If no new mail is detected,
onNewMailPostis never calledThis is why the notification only appears when there's actual new mail—the trigger is built into goimapnotify's event detection logic, not the command itself.
Part 2: Sketchybar Status Bar Update
Normal NeoMutt user can skip this section unless you also use Sketchybar. Or you don't need to do anything and your original status bar on the macOS will handle the rest automatically.
/opt/homebrew/bin/sketchybar --trigger mail_update| Component | Purpose |
|---|---|
/opt/homebrew/bin/sketchybar | Full path to the sketchybar binary (custom status bar for macOS) |
--trigger mail_update | Fires a custom event named mail_update |
What happens:
- sketchybar receives the
mail_updatetrigger event - Any sketchybar items configured to listen for this event will refresh
- Typically, this updates a mail counter or icon in the status bar
Example sketchybar config that responds to this trigger:
# ~/.config/sketchybar/plugins/mail.sh
#!/bin/bash
MAIL_COUNT=$(find ~/.maildir/twine/INBOX/new -type f | wc -l | tr -d ' ')
sketchybar --set mail.icon label="" \
--set mail.count label="$MAIL_COUNT"# ~/.config/sketchybar/items/mail.sh
sketchybar --add item mail.icon right \
--set mail.icon script="$PLUGIN_DIR/mail.sh" \
--subscribe mail.icon mail_updateResult in status bar:
┌─────────────────────────────────────────────────────────────────┐
│ [CPU] [Memory] [Wifi] [ 5] [Volume] [Battery] [Clock] │
└─────────────────────────────────────────────────────────────────┘
↑
Mail icon shows 5 unreadComplete Flow Diagram
New email arrives at IMAP server
│
▼
goimapnotify detects new mail
│
▼
Triggers: onNewMail command
└─▶ mbsync twine (downloads emails)
└─▶ notmuch new (indexes emails)
│
▼
Sync completes successfully
│
▼
Triggers: onNewMailPost command
│
├────────────────────────────────┐
│ │
▼ ▼
terminal-notifier sketchybar
│ │
▼ ▼
┌────────────────────┐ ┌───────────────────┐
│ macOS Notification │ │ Status Bar Update │
│ "Mail" │ │ Mail counter │
│ "Sync Complete" │ │ refreshes │
└────────────────────┘ └───────────────────┘Why Use Full Paths?
The configuration uses absolute paths (/opt/homebrew/bin/...) instead of just terminal-notifier because:
- LaunchAgent environment – When macOS starts the daemon, it runs with a minimal PATH
- Reliability – Absolute paths ensure the correct binary is always executed
- No shell dependency – Avoids relying on shell profile loading (
~/.zshrc,~/.bash_profile)
To find the full path of any Homebrew binary:
which terminal-notifier
# Output: /opt/homebrew/bin/terminal-notifier
which sketchybar
# Output: /opt/homebrew/bin/sketchybarSetup Auto-Start with LaunchAgent
Setting up goimapnotify to auto-start via LaunchAgent ensures continuous email monitoring without manual intervention. Without this configuration, you would need to manually start goimapnotify in a terminal every time you log in or restart your computer. By registering it as a LaunchAgent, macOS automatically launches the daemon at login and keeps it running in the background, guaranteeing that you never miss new email notifications regardless of whether you've opened a terminal session.
Step 1: Create the LaunchAgent Plist
Create ~/Library/LaunchAgents/com.user.goimapnotify.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.user.goimapnotify</string>
<key>ProgramArguments</key>
<array>
<string>/Users/alowree/go/bin/goimapnotify</string>
<string>-conf</string>
<string>/Users/alowree/.config/goimapnotify/goimapnotify.yaml</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>/Users/alowree/.config/goimapnotify/stdout.log</string>
<key>StandardErrorPath</key>
<string>/Users/alowree/.config/goimapnotify/stderr.log</string>
</dict>
</plist>Key Settings:
| Setting | Value | Purpose |
|---|---|---|
Label | com.user.goimapnotify | Unique identifier for the agent |
RunAtLoad | true | Starts goimapnotify at login |
KeepAlive | true | Restarts if the process crashes |
StandardOutPath | Log file path | Captures stdout for debugging |
StandardErrorPath | Log file path | Captures errors for debugging |
Step 2: Load the LaunchAgent
# Load the agent (starts immediately and at every login)
launchctl load ~/Library/LaunchAgents/com.user.goimapnotify.plist
# Verify it's loaded
launchctl list | grep goimapnotifyStep 3: Manage the LaunchAgent
# Check status
launchctl list | grep com.user.goimapnotify
# Unload (stop until next login)
launchctl unload ~/Library/LaunchAgents/com.user.goimapnotify.plist
# Load again
launchctl load ~/Library/LaunchAgents/com.user.goimapnotify.plistVerification & Troubleshooting
Check 1: Is goimapnotify Running?
# Check if process is running
pgrep -x goimapnotify && echo "✓ goimapnotify is running" || echo "✗ goimapnotify is NOT running"
# Get process details
ps aux | grep goimapnotify | grep -v grepExpected output:
954
✓ goimapnotify is runningCheck 2: Is LaunchAgent Loaded?
# List loaded agents
launchctl list | grep goimapnotifyExpected output:
954 0 com.user.goimapnotifyThe number in the middle column is the PID. If it shows -, the agent is loaded but not currently running.
Check 3: Review Logs
# Check stdout log (successful operations)
tail -f ~/.config/goimapnotify/stdout.log
# Check error log (problems)
tail -f ~/.config/goimapnotify/stderr.logNormal stdout log:
Connected to twineintlcom.securemail.hk
Monitoring mailbox: INBOX
New mail detected in INBOX
Triggering onNewMail command...
Command completed successfullyError log (if any):
Connection failed: timeout
Retrying in 30 seconds...Check 4: Test Manual Sync
# Test mbsync manually
mbsync twine
# Verify maildir has content
ls -la ~/.maildir/twine/INBOX/cur | head -5Check 5: Monitor Real-Time Activity
# Watch for new sync activity
tail -f ~/.config/goimapnotify/stdout.log &
# In another terminal, watch maildir for new files
watch -n 2 'ls -lt ~/.maildir/twine/INBOX/cur | head -5'Common Issues
| Issue | Solution |
|---|---|
| goimapnotify not starting | Check plist syntax: plutil -lint ~/Library/LaunchAgents/com.user.goimapnotify.plist |
| Password errors | Verify Keychain entry exists: security find-generic-password -a 'alowree@twineintl.com' -s 'neomutt-twine' |
| mbsync fails in onNewMail | Use full paths in config: /opt/homebrew/bin/mbsync not mbsync |
| No notifications | Test manually: /opt/homebrew/bin/terminal-notifier -title 'Test' -message 'Working' |
| High CPU usage | Check for connection loops in logs; may need to adjust tlsOptions |
Debugging Workflow
# 1. Stop the agent
launchctl unload ~/Library/LaunchAgents/com.user.goimapnotify.plist
# 2. Run goimapnotify manually in foreground (verbose)
/Users/alowree/go/bin/goimapnotify -conf /Users/alowree/.config/goimapnotify/goimapnotify.yaml
# 3. Watch for errors in real-time
# If manual run works, reload the agent
launchctl load ~/Library/LaunchAgents/com.user.goimapnotify.plistQuick Reference Card
┌─────────────────────────────────────────────────────────────────┐
│ GOIMAPNOTIFY QUICK REFERENCE │
├─────────────────────────────────────────────────────────────────┤
│ Config file: ~/.config/goimapnotify/goimapnotify.yaml │
│ LaunchAgent: ~/Library/LaunchAgents/com.user.goimapnotify...│
│ Start: launchctl load ~/Library/LaunchAgents/...plist │
│ Stop: launchctl unload ~/Library/LaunchAgents/... │
│ Check running: pgrep -x goimapnotify │
│ View logs: tail -f ~/.config/goimapnotify/stdout.log │
│ Test sync: mbsync twine │
│ Manual run: goimapnotify -conf ~/.config/goimapnotify/... │
└─────────────────────────────────────────────────────────────────┘Related Files
| File | Purpose |
|---|---|
~/.config/isyncrc | mbsync IMAP configuration |
~/.config/goimapnotify/goimapnotify.yaml | goimapnotify daemon config |
~/Library/LaunchAgents/com.user.goimapnotify.plist | macOS auto-start config |
~/.config/goimapnotify/stdout.log | Success logs |
~/.config/goimapnotify/stderr.log | Error logs |
~/.maildir/twine/ | Local mail storage |