NeoMutt: Automating new email with Goimapnotify

Last time we completed the configuration of mirrored local IMAP so that all email is synchronised into a local email folders (maildir) using mbsync. This time the focus is on immediately synchronising new email that's on the server down to our local system by using Goimapnotify.

To check for new email regularly we could simply run mbsync every 5 minutes! But, IMAP IDLE is a better approach that keeps a connection open from a client to the IMAP server. When the IMAP server receives a new email it notifies the client which can then connect and pull down the email. Unfortunately, mbsync doesn't support running continuously, so I'm using Goimapnotify to be the client notifier. In this configuration I start goimapnotify which runs continuously as a daemon - when it sees new email on the IMAP server it runs mbsync to download the new email.

🙏 Goimapnotify's developed by Jorge Araya - they'd appreciate a donation if you use it in your email set-up!

This is part 5 of the NeoMutt email series, you can find the whole series on the NeoMutt resources page. As before I'm using Runbox as my email provider - highly recommended - but this will work with any standard provider (including Gmail).

Installing goimapnotify

Goimapnotify is available as a package on Guix, so to install do:

guix package --install goimapnotify

Eventually, we'll script installing it and running it through guix shell.

Configuring goimapnotify

The configuration file for Goimapnotify is ~/.config/goimapnotify/goimapnotify.yaml. It looks like this:

configurations:
 -
   host: mail.runbox.com
   port: 993
   tls: true
   tlsOptions:
     rejectUnauthorized: false
   username: steve%futurile.net
   passwordCmd: "gpg --quiet --for-your-eyes-only --no-tty --decrypt ~/.sec.gpg"
   alias: futurile
   boxes:
     -
       mailbox: INBOX
       onNewMail: 'guix shell isync mailutils bash-minimal -- bash -c "mbsync <mychannel>"'
       onNewMailPost: 'guix shell isync mailutils bash-minimal -- bash -c "sieve --mbox-url=~/.mail/<user>/INBOX/ --verbose ~/.config/neomutt/mydomain-inbox.sieve"'
       onDeletedMail: 'guix shell isync mailutils bash-minimal -- bash -c "mbsync <mychannel>"'
       onDeletedMailPost: 'guix shell isync mailutils bash-minimal -- bash -c "sieve --mbox-url=~/.mail/<user>/INBOX/ --verbose ~/.config/neomutt/mydomain-inbox.sieve"'

The configurations: block contains information about the IMAP server monitor - the host and port define the connection. If you have multiple accounts there will be multiple blocks in configurations.

Set the tls option to specify that TLS is being used. If you're using a self-signed certificate then the tlsOptions should have rejectUnauthorized. We've configured the IMAP username and passCmd using GnuPG previously, in other tools, so this should all be familiar.

The boxes stanza contains the mailboxes to check. Multiple mailboxes can be configured by repeating the stanza. If all new mail goes to INBOX then we only need to specify this line, if some email is filtered on the server (e.g mailing lists) then multiple lines will be required. The important part is that the onNewMail command is run when Goimapnotify detects new email. For this I want it to run guix shell and then use mbsync:

guix shell isync bash-minimal -- bash -c "mbsync <channel>"

This starts a guix shell and installs the packages that are required (isync and bash-minimal): by doing it within a Guix shell I'm guaranteed that the specific packages required are installed. Note that after the first installation the packages will be cached in Guix's Store so this is very fast. The guix shell command will run a command after the double hyphen, I'm being a bit more sophisticated by specifically starting Bash (bash -c) and then providing the command.

We often want to run a command after we've downloaded new email, that's the purpose of the onNewMailPost command. In this case I'm filtering my email using a utility called sieve which I'll cover in the next blog post.

If mail is changed through more than one email client then it's possible for Goimapnotify to see email being deleted. For example, if I delete an email from my mobile phone, then Goimapnotify needs to know what to do. The onDeletedMail and onDeletedMailPost commands specify that it should run mbsync as usual: since we configured mbsync to do two-way sync it will remove any deleted email from the local store.

To test the configuration you can tell Goimapnotify to log into the IMAP server and list all mailboxes with:

goimapnotify -list

Alright, this is great! Now when we run goimapnotify from the command line it will listen for new email and when it sees some, it will download it using mbsync.

Starting Goimapnotify

A nice way to run Goimapnotify is to make it network aware, so it starts when the network is available and stops when there isn't a network. This means creating a dispatch script for Network Manager.

First, we create a script that runs mbsync to pull down any email which came in while we were offline, and then it will start goimapnotify which will then listen for new email. Here's the ~/bin/email-checker.sh script:

#!/usr/bin/env bash
exec guix shell guile isync goimapnotify guile-readline guile-colorized -- guile -e main \
    -L /home/<user>/.config/guix/current/share/guile/site/3.0/ -s "$0" "$@"

(use-modules (ice-9 exceptions)
             (srfi srfi-19) ;for 'date->string'
             (srfi srfi-34) ;for 'guard'
             (guix build utils)) ;for 'invoke'

(define* log-port (open-file "~/.msmtp/log/email-checker.log" "a"))

(define* (start-logger)
   (write-log "Starting logging")
   (set-current-output-port log-port)
   (set-current-error-port log-port))

(define* (write-log msg)
    (format log-port "~a: ~a \n" (date->string (current-date) "~5") msg)
    (force-output log-port))

(define* (end-logger)
    (write-log "Stopping logging")
    (close-port log-port))

(define (mbsync_f)
  (let* [ (cmd_mbsync "mbsync futurile")
          (exit_status (system cmd_mbsync)) ]
    (cond
        ( (= exit_status 0)
            (write-log
              (format #f "Successfully ran mbsync. Command: ~s returned: ~y~%"
                 cmd_mbsync exit_status)))
        ( (>= exit_status 1)
            (write-log
              (format #f "Error! Command: ~s. Result returned: ~y~%"
                 cmd_mbsync exit_status))
            (error "Error executing mbsync: " exit_status)) ;exit with error
     ) ;end of cond
   )) ;end of func


(define (main args)
 (start-logger)
 (write-log "Starting e-mail checker")

 (mbsync_f)

 (write-log "Starting Goimapnotify")
 (invoke "goimapnotify")
 (write-log "Successfully ran goimapnotify")
 (exit (EXIT_SUCCESS)
) ;end of main

The shebang (#!/usr/bin/env) starts Bash which then executes a guix shell which installs all the necessary Guix packages, guaranteeing the script has the external application it needs. In turn guix shell executes a Guile script (the -- guile part). The location of the guix installation is provided with the -L switch so that the Guix modules are accessible, and Guile is told the entry point is the main procedure.

The clever part is that the -s switch tells Guile to execute the script and this filenames name is provided (using Bash's "$0") - so effectively it's told to execute itself! This works because Guile's multi-line comment is #! ... !# which means that when it executes the file it ignores the whole shebang line and sees a standard Guile Scheme program! I know - I had to read it a couple of times the first time I saw it - it's just brilliant! The last bit of the Guile invocation is the $@ which provides the command-line options, though we don't use them.

The Guile script loads some modules for time/date and to invoke an external command (e.g. call mbsync). For logging it defines a log-port variable that contains the location to log to. The start-logger function redirects standard output (set-current-output-port) to the log file, and the same for stderr so that the output of mbsync and goimapnotify are in the log file. The end-logger function simply closes the log-port correctly. To write a log messages there is a write-log function which uses the date->string and current-date functions from srfi-19 to add date/time to the log message that's going to the file.

The main function calls the mbsync_f function. This uses Guile's system function from to call mbsync: this will catch new email that's come in while the user was offline. It captures the result of the call and if there's an error it exits the script. If there's not we got back to the main function.

Next we're ready to call goimapnotify using the invoke function from (guix build util). The way that invoke works is that if there's an error it will create an Exception, so we don't need to capture the result. Eventually, when this script returns it prints the final message to the log and closes the log.

If this is script is run from the command-line it will check for email continuously by calling goimapnotify.

The way I run this is to use Network Managers dispatcher capability which will run a script when the network comes up and stop running it when the network is deactivated. There's a script that can be altered and further details on my msmtp tutorial page.

Summary

We now have an automated email set-up that will check for email while the network is available and download any new email. Goimapnotify is really nice, so thanks to the Jorge Araya (GitHub) for putting their time into it!

Did this post cover everything you needed to know about automating new email with Goimapnotify? If you have questions, comments or thoughts please email me - details on the About page. I can also be reached on Mastodon, futurile@mastodon.social.

The last major element for the inbound email flow is to filter out email so that we can deal with everything efficiently. Most email providers have a method for doing that on the server side but for additional flexibility I'm going to do it using Mailutil's sieve - which is what we'll explore next time.


Posted in Tech Thursday 29 May 2025
Tagged with tech ubuntu guix email neomutt isync mbsync goimapnotify