Skip to content
On this page

Setting up goimapnotify to start up with the system and get your offline mail as soon as it appears on the server.


Article
4 months ago

Instant offline emails on Mac with goimapnotify, mbsync and mu

Intro

I have spent a few hours today making sure that my emails get delivered to my offline inbox as soon as they appear on the server, and that this happens for each of my email accounts.

Tools

I assume that you already have your favorite offline email management tool (mu, notmuch etc.) set up and your emails are synced with something like mbsync (isync). So you've got the basics covered and are ready for the advanced stuff.

If you're not there yet, here are a couple of great articles to help you get started in different ways:

Mailboxes

For the sake of this tutorial let's assume that you have a .mbsyncrc file with two mailboxes set up: work and private. This means that you can get all mail from the server using mbsync -a or selectively sync just work or private with mbsync work and mbsync private.

At the moment you either start the sync manually, or more probably have something like a homebrew service for isync running and checking for emails every few minutes.

We're going to make this experience as seamless as it is on the smartphones with new emails appearing in your inbox as soon as they hit the server.

Setup

First let's install goimapnotify. It's not available on brew and will require golang to be installed so:

  1. brew install golang
  2. go install gitlab.com/shackra/goimapnotify@latest
  3. Add the target folder for go installs to PATH, normally its ~/go/bin, so add this to .zshrc:
sh
export PATH=$HOME/go/bin:$PATH

Ok, the easy part is over. Now let's make config files.

Configuration

For some unknown reason goimapnotify thinks that it's config files should have a .conf extension, even though the content is simple json. We will not follow this strange pattern, and will create two json config files work.json and private.json.

Let's assume that your work email is on Gmail, and your private is on Protonmail (as it should be, preferably with a custom domain name, so that you could leave for another provider if you wish).

work.json

The simplest version of work.json for gmail would look like this:

json
{
    "host": "imap.gmail.com",
    "port": 993,
    "tls": true,
    "username": "your.work.email@gmail.com`,
    "password": "YOUR_PASSWORD",
    "onNewMail": "mbsync work",
    "onNewMailPost": "mu index --lazy-check",
    "boxes": ["INBOX"],
    "wait": 1
}

In this file we instruct goimapnotify to connect to our work Gmail inbox and wait for new messages. As soon as a new message hits, we wait 1 second and then run the onNewMail command mbsync work. This does the normal run of downloading new messages.

Once this is done we run a onNewMailPost command to index new emails.

If you want this to be more secure, you can substitute getting your password with a command like pass or op (1Password CLI):

json
{
    "host": "imap.gmail.com",
    "port": 993,
    "tls": true,
    "username": "your.work.email@gmail.com`,
    "passwordCmd": "pass email/work", 
    "onNewMail": "mbsync work",
    "onNewMailPost": "mu index --lazy-check", 
    "boxes": ["INBOX"],
    "wait": 1
}

Instead of password we provide a passwordCmd to get the password from pass. We can also do the same for the username, substituting username with usernameCmd and even with the host to hostCmd. For the most paranoid.

private.json

With Protonmail you would need to set up a Protonmail Bridge, so the config would look like this:

json
{
    "host": "127.0.0.1",
    "port": 1143,
    "tls": false,
    "username": "your.private.email@proton.me",
    "passwordCmd": "pass email/private",
    "onNewMail": "mbsync private",
    "onNewMailPost": "mu index --lazy-check",
    "boxes": ["INBOX"],
    "wait": 10
}

So, now you're ready to test it out. Put these two config files anywhere, for example into ~/.config/goimapnotify/ and test out your Work email integration with:

sh
goimapnotify -conf ~/.config/goimapnotify/work.json -debug

The -debug would make sure you get enough log output to understand that everything works.

Now all you need to do is send an email to your work email and watch the logs. If everything works fine, after the email appears in your Gmail inbox you should get a log message, and then mbsync and the mu will kick in, and email should be readily available in your offline email client of choice.

Launch as Startup

If everything works you're ready for the last step - make all of this launch at startup. On MacOS we need to use launchd to run things at startup, and getting those pesky plist files is always a hassle. So in case you need to debug use something nice like LaunchControl, I used the free version. There is also a very neat online plist generator launched., I haven't used it but it can definitely come in handy.

For work create a file called com.example.workemail.plist and put it to ~/Library/LaunchAgents:

xml
<?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.example.workemail.plist</string>
    <key>ProgramArguments</key>
    <array>
         <string>/Users/USERNAME/go/bin/goimapnotify</string>
         <string>-conf</string>
         <string>/Users/USERNAME/.config/goimapnotify/work.json</string>
    </array>
    <key>KeepAlive</key>
    <true/>
    <key>RunAtLoad</key>
    <true />
    <key>StandardErrorPath</key>
    <string>/Users/USERNAME/Library/Logs/CheckMailBlaster.err</string>
    <key>StandardOutPath</key>
    <string>/Users/USERNAME/Library/Logs/CheckMailBlaster.log</string>
    <key>EnvironmentVariables</key>
        <dict>
           <key>PATH</key>
           <string>/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/homebrew/bin</string>
        </dict>
</dict>
</plist>

Replace USERNAME with your user name.

The most tricky part that took a good hour to debug was figuring out that you need to provide the PATH variable to the running process, as it doesn't have access to the system wide one. Until I did, goimapnotify would start and connect normally, and once the new email hit I got an error on the mbsync step.

So, as I said this simple file took two hours of debugging, so say thank you and create similar file for private email like com.example.privateemail.plist in the same directory.

Now all that's left is launch:

sh
launchctl load ~/Library/LaunchAgents/com.example.workemail.plist
launchctl load ~/Library/LaunchAgents/com.example.privateemail.plist

Make sure that they're running with

sh
launchctl list | grep com.example

Or better yet use LaunchControl to monitor their status. If everything worked you should get the services started as soon as you log in, and the mail will appear in your inbox instantly.

Please note, that the sync will start only when a new email appears, so if your computer was off for the night and you turn it on, the syncing will not begin until the first new email that day. To avoid that you can create a similar plist that launches once and runs

sh
mbsync -a

I leave that as an exercise for the reader.

Blaster.ai