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: twineintl
passwordCmd: |
if [[ -x /opt/homebrew/bin/pass ]]; then
/opt/homebrew/bin/pass Email/twineintl
elif [[ -x /usr/sbin/pass ]]; then
/usr/sbin/pass Email/twineintl
else
pass Email/twineintl
fi
boxes:
- mailbox: INBOX
interval: 10
onNewMail: |
# Detect OS and use appropriate script path
if [[ "$(uname)" == "Darwin" ]]; then
/Users/alowree/.local/bin/mail-sync-twine
else
/home/alowree/.local/bin/mail-sync-twine
fi
onChangedMail: |
if [[ "$(uname)" == "Darwin" ]]; then
/Users/alowree/.local/bin/mail-sync-twine
else
/home/alowree/.local/bin/mail-sync-twine
fi
onNewMailPost: |
if [[ "$(uname)" == "Darwin" ]]; then
/Users/alowree/.local/bin/notify-mail 'New email received in Twine' 'Twine Mail'
else
/home/alowree/.local/bin/notify-mail 'New email received in Twine' 'Twine Mail'
fiConfiguration 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>
<string>-log-level</string>
<string>info</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>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
<key>HOME</key>
<string>/Users/alowree</string>
</dict>
</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 |
Resources
On Arch Linux, the standard and most powerful way to achieve the same "start on login and run continuously in the background" is by creating a user-level systemd service. Just like a LaunchAgent on macOS, a systemd user service will automatically start the daemon when you log into your desktop and keep it alive in the background.
⚙️ The Solution: A systemd User Service
Because goimapnotify needs to access your user's credentials and GPG keys, it must run as a user service. This method is the native, modern equivalent of LaunchAgents on Arch Linux.
Here is the step-by-step guide to creating the service:
1. Create the Service File
First, you need to create a service unit file in your local user configuration directory.
Open a terminal and run this command to create and edit the file:
systemctl --user edit --force --full goimapnotify-twine.serviceThis will open a blank text editor (likely vim or nano). Paste the following configuration into it:
[Unit]
Description=goimapnotify for Twine account
After=network.target
[Service]
Type=simple
ExecStart=/usr/sbin/goimapnotify -conf /home/alowree/.config/goimapnotify/goimapnotify.yaml -log-level info
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=default.targetExecStart: Specifies the command to run.%his a systemd shortcut that automatically expands to your home directory (/home/alowree), making the path portable.Restart=always: This is the key to the "continuous monitoring" you had on macOS. It ensures that ifgoimapnotifyever crashes or is killed, systemd will automatically restart it after 10 seconds.WantedBy=default.target: This tells systemd to start this service automatically when your user session begins, just like a LaunchAgent'sRunAtLoadkey.
2. Start and Enable the Service
After saving the file, you need to reload systemd so it recognizes your new service, then start it immediately, and enable it so it launches automatically on future logins.
Run the following commands:
# Reload systemd to register the new service file
systemctl --user daemon-reload
# (Optional) Start the service right now
systemctl --user start goimapnotify-twine.service
# (Critical) Enable the service to auto-start on login
systemctl --user enable goimapnotify-twine.service3. Verifying and Managing the Service
You can use the same systemctl --user commands to manage your service just like you did with launchctl on macOS.
Check if it's running:
bashsystemctl --user status goimapnotify-twine.serviceView live logs (like
tail -f):bashjournalctl --user -u goimapnotify-twine.service -fStop the service:
bashsystemctl --user stop goimapnotify-twine.service
📢 A Note on Desktop Notifications
For the notify-mail part of your script to work on Arch Linux, you must have a notification daemon running in your desktop session.
If you are using a minimal window manager (like i3, sway, or bspwm) and pop-up notifications aren't appearing, you likely need to install and run one. A very popular and lightweight choice is dunst. It is simple to install and, in most cases, works immediately after being started.
# Install dunst
sudo pacman -S dunstYou would typically add dunst & to your window manager's startup script (e.g., ~/.xinitrc). Once a notification daemon is running, the notify-send command in your scripts will work perfectly.
I hope this provides a clear and equally robust setup for your Arch Linux system. Let me know if you run into any issues while creating the service.
How do I know if the notification daemon is currently running?
