NeoMutt: mirrored local IMAP with mbsync

The goal of this post is set-up a NeoMutt mail configuration that locally mirrors an IMAP servers mailboxes. This will mean we'll be able to read and write email when offline. Any changes made to the local email, such as deleting an email, will also be made on the IMAP server.

The way this works is that the remote IMAP server's mailboxes are synchronised with the local system's mailboxes. New email that comes into the remote IMAP server is downloaded to the local email store (i.e the maildir directory). The user accesses the local mail store using NeoMutt as usual. Any changes, for example an email is deleted, are written to the local mailboxes. On the next sync the local mailbox's state is synchronised with the remote IMAP server, so in this example the email is deleted on the remote IMAP server as well.

An important element in this type of set-up is that it's two-way synchronisation, so changes made on either side will propagate to the other. For example, if you use Runbox's Webmail and delete an email on the IMAP server, the change will be synchronised to the local mailboxes. And, the same in reverse when using NeoMutt. That's why I'm calling it a mirrored IMAP set-up.

The two popular utilities for mirroring IMAP are offlineimap and Isync. Offlineimap was popular but it suffered a lack of maintenance. Consequently, isync is a bit more popular now and it's what this post covers. One thing to bear in mind with isync is that the actual binary is called mbsync so this is the name that's often used for it.

This is part 4 of the NeoMutt email series, you can find them all 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 mbsync

It's a pretty stable project and is widely available, to install it on Guix do:

guix shell isync

Isync is a little bit more out of date on Ubuntu, but it is available in the archive:

sudo apt install isync

Configuring mbsync

Mbsync is configured by creating a configuration file, the default location is $XDG_CONFIG_HOME/isyncrc which for me is ~/.config/isyncrc. The older configuration option is to have a ~/.mbsyncrc file.

The model for thinking about how this works is that you have a local store and remotely there's a remote store (on an IMAP server), we tell mbsync how to connect the two together using a channel:

[local store] <---> [channel] <----> [remote store] <--> [remote server]

If there are a lot of channels then these can be put into a group. This means there are 5 steps to configuration:

  1. Configure the remote server
  2. Create a remote store
  3. Create a local store
  4. Connect them with a channel
  5. Put then in a group (optional)

The remote server

A remote mail store defines the IMAP account where email is, this section contains the authentication and server details of the remote IMAP server.

IMAPAccount <account-name>-imap     # Short name for the account
Host <imap-server.somehost.com>     # Hostname FQDN of the IMAP server
User <userid@mailprovider.net>      # Login userid
Pass <somelongpassword>             # Account password - use for testing only
TLSType IMAPS                       # Type of SSL to use
AuthMechs Login                     # Authorisation mechanism is password
SystemCertificates yes              # Use the systems TLS certificates

The IMAPAccount tells mbsync the name of the account and that the next section contains parameters for it. The <account-name> chosen will be used in other places to tie everything together.

The Host is the mail server address, for example for Runbox that is mail.runbox.com. The User is the userid to use for login, as we did before when configuring IMAP this is likely to be the user%domain but check with your mail provider's documentation.

For testing I just add the password using the Pass section. After completing testing it can be changed to using GnuPG and an encrypted file.

In newer versions of mbsync to specify the TLS type use the TLSType option with either STARTTLS or IMAPS. Additionally, specify TLSVersions with TLSv1.2. If you don't set TLSVersions it will use version 1.2 and above.

Mbsync automatically uses the system's certificate file so this doesn't need to be specified.

The AuthMechs Login provides a variety of authorisation options: to use passwords this has to be specified so that password authentication works.

The remote store

The next step is to define the remote store which is the location on the IMAP server where the mail files reside. This is pretty short:

# Remote store
IMAPStore <account-name>-remote
Account <account-name>-imap

This says that we're defining an IMAPStore and we define a name for it. The general convention is to call it something with -remote on the end e.g. myprovider-remote. Then we tell mbsync that the location of this store is on the Account which we defined just above.

If you're wondering why we didn't have to specify a location for the mailboxes it's because mbsync "the reference point for relative Paths is the current working directory".

The local store

The local store is the location on the local file system where email will be synchronised to. In my case I'm using $HOME/.mail for all email storage. The first task is to create a subdirectory for all email for this account to be stored in:

# Create the local store dir
mkdir -p ~/.mail/<account-name>

Going back to our mbsync configuration file we define the local storage:

# Local mail store
MaildirStore <account-name>-local
Path ~/.mail/<account-name>/
Inbox ~/.mail/<account-name>/INBOX
Subfolders Verbatim

The MaildirStore is the name of a local store, again the convention is to use an account name with -local on the end this time. The Path is the path to the local mailstore, which is the account specific sub-directory: note that this must have a trailing forward slash to specify an entire directory. Inbox is the path to the inbox location (which is generally going to be in the MaildirStore path).

The Subfolders option specifies how subfolders are created for the local store: Verbatim converts them from the IMAP system which uses dots to using forward slashes so they're standard sub-directories.

Connecting with channels

Channels are the relationship between the local and remote mail stores, this is where everything is tied together.

# Channel connecting <account-name>-remote to <account-name>-local
Channel <account-name>
Far :<account-name>-remote: <mailbox>
Near :<account-name>-local: <mailbox>
Patterns *              # Sync all folders
Sync Pull               # NOTE: Change to 'All' when testing is finished
Expunge Near            # NOTE: Change to 'Both' when testing is finished
Create Near             # NOTE: Change to 'Both' when testing is finished
Remove Near             # NOTE:
SyncState *             # not setting this defaults to writing state files in ~
CopyArrivalDate yes
MaxMessages 50          # NOTE: useful for testing
ExpireUnread yes        # Remove old unread, to let up to 50 more in

The Channel parameter gives the channel a name, this will be used to run commands so should be a memorable name.

The Far and Near are the two mail-stores that are being connected together, which will be the remote and local ones from earlier, they are specified as :mailstore-name: using colons at the start and end. For example:

  • if the remote store was IMAPStore gmail-remote then the channel's Far would be Far :gmail-remote:.
  • if the configuration set the local mail store to be MaildirStore runbox-local then the channel's near would be Near :runbox-local:.

There are different ways to define which mailboxes to synchronise, which we'll discuss in more detail later. For now consider it in three steps:

  1. Set a specific <mailbox> (e.g Drafts) on the Far and Near line to sync a mailbox pair.
  2. Alternatively, specify a Patterns which defines folders to sync: a Pattern * means synchronise all folders.
  3. If no Pattern or mailbox is specified then mbsync will only sync the INBOX file.

The next option is the type of synchronisation to use, using Sync Pull for testing is a good way of checking everything is working as it only pulls down changes. When testing is complete it can be changed to Sync All which will bi-directionally sync changes including flag changes and deletions.

The Expunge option defines how email will be deleted from the mail stores, for testing this should be set to Near so that email deleted on the local side isn't deleted on the remote side. When testing is complete this can be changed to Expunge Both so that email deleted either remotely by another email client (e.g. Webmail) will also cause a deletion in the local store, and if email is deleted on the local side it's also deleted in the remote store.

The next two options Create and Remove define what happens when there are new mailboxes. By setting both to Near we're setting it so that the remote server is the master copy, and any mailboxes that appear there are also created in the local store. After testing this could be changed to Create Both so that if a new mailbox is created on the local system then it will appear on the remote one as well. Setting Remove Both might be a bit more of a worry if you're messing around with the configuration.

The SyncState option specifies where this channels synchronisation state files will be stored. For a Maildir local store using * means it stores the state file (.mbsyncstate) in the local directory.

The IMAP server records when the file was created, this is generally close to when the email actually arrived. The setting CopyArrivalDate yes records this time-stamp for you.

For testing set MaxMessages 50 which means that only 50 messages will be synchronised to the local store, along with ExpireUnread yes which means it will only sync 50 total messages even if the user has 100 new ones in a mailbox - it deletes the oldest messages to make way for newer ones. These can be removed after testing is complete.

Matching Mailboxes

There are a few options for choosing mail boxes to synchronise:

  • Define Far and Near line to match INBOX
  • Specify a mailbox pair on the Far and Near lines
  • Specify Patterns regexps to match a general set of mailboxes
  • Match Far/Near mailboxes and Patterns

There are quite a lot of options, which is why a lot of examples in blog posts are subtly different.

  • Match Inbox with Far and Near line

    The easiest rule is that if no mailbox is defined on the channels Far or Near lines then mbsync will look for INBOX. For example, something like this:

    Channel mymail-inbox
    Far :mymail-remote:
    Near :mymail-local:
    Sync Pull
    Expunge Both
    

    This will find the INBOX and will pull it down, but won't match any other mailbox. You don't need a Patterns line for this and it causes some odd issues if you set one.

  • Match a mailbox pair on the Far / Near line

    For total granularity of matching specify a mailbox on the Far line and it will be synchronised to the mailbox that's specified on the Near line. This is good for matching individual mailboxes, with the downside being that it makes the configuration file longer.

    This is what I use as it's very explicit: for each mailbox that I want to sync I have a channel, and then they're all grouped together in a group.

    For example a pair would look something like this:

    Channel mymail-drafts
    Far :mymail-remote:Drafts
    Near :mymail-local:Drafts
    

    Where we're calling the channel mymail-drafts and then defining the Far as the remote server mymail-remote and the local mail store as my-mail-local. Mbsync will login to the remote server and find the Drafts folder and synchronise it into the local mail-store path, again into the Drafts folder.

    Again, note that you don't need a Patterns option.

  • Match mailboxes with a pattern

    To match more than a single pair of mailboxes we can define a pattern to match. The easiest way to do this is to leave the mailbox section of the Far and Near lines empty. This means that mbsync will login to the root of the IMAP server and will then use the Patterns option to match:

    Channel mymail-inbox
    Master :mymail-remote:
    Slave :mymail-local:
    Patterns * INBOX !Spam !Junk !Archive
    Sync Pull
    Expunge Both
    

    This example matches the special folder inbox by specifying INBOX and everything else with *, it then excludes specific folders using ! for example the junk folder (!Junk). It's possible to write complex rules to match all email folders like this. Personally, I prefer to have simple rules for each folder and use a Group as it makes it easier to debug.

  • Match a mailbox pair and a pattern

    Finally, if there's a hierarchy of folders then it might be useful to use both mechanisms to match. For example, I have Archives with subfolders for each year (2015, 2016). To match this I use both:

    Channel mymail-archives
    Master :mymail-remote:Archives
    Slave :mymail-local:Archives
    Patterns *              # Match multiple levels e.g. Archives/<year>
    Sync All                # Sync all email changes, including flags: bidirectionally
    Expunge Both            # Deleted mail: bidirectional sync
    Create Both             # Mailbox creation: bi-directional sync
    Remove Both             # Mailbox removal: bi-directional sync
    Syncstate *             # Put state file in maildir
    CopyArrivalDate yes     # Use server date/time for email
    

    In this example mbsync logs into the server and goes to the Archives folder location, it then matches all folders underneath this using Patterns *.

Groups

Groups allow selective synchronisation by listing a set of channels. This is useful because it's then easy to specify mailbox pairs in individual channels which are easy to test. And, if you only want some email on a particular device (e.g. only INBOX on a laptop) it's very easy to configure:

Group mymail-group
Channel mymail-inbox
Channel mymail-drafts
Channel mymail-archives
Channel mymail-sent
Channel mymail-trash

A Group is assigned a name (e.g. mymail-group) which will be used on the command line. It can have one or more Channels in it.

Testing

Messing around with email is very scary with the risk that something will be deleted. Mbsync has some commands to show what's happening.

mbsync --list-stores --verbose <store-remote>

This command will check whether the server and remote store work. If they do it will list all the mailboxes on the remote store. It can also be used on the local-store.

To check that either a channel or group is configured correctly do:

mbsync --list --verbose <group-name>

This will show each channel, what it's matching and where it will synchronise to.

There's quite a few options to debug, for example I find --debug-net-all useful to see precisely what mbsync is seeing on the IMAP server.

Synchronising email

To synchronise email run it like this:

mbsync --verbose --dry-run <group-name / channel-name>

This means that we can synchronise entire groups, or channels. For example, using the group and channels from above we could do:

mbsync mymail-group                 # sync everything in group
mybsync mymail-inbox                # sync inbox channel only
mbsync mymail-inbox mymail-drafts   # sync two channels

There's a lot of flexibility in the channels and groups concept. If storage is limited on a particular machine one strategy is to create a group with a couple of channels and only sync them.

When mbsync runs the output will show the channel or group and what is being matched, something like this:

Channels: 5    Boxes: 10    Far: +0 *4 #0 -0    Near: +23 *0 #0 -0

This says there are 5 Channels in the group, a total of 10 Boxes (mailboxes) were synchronised. On the far side there were 0 new messages (the plus sign) synchronised to it, 4 had flags changed (the * sign) and there were 0 trashed messages (the hash symbol) and 0 expunged (the minus symbol). On the Near side local email received 23 new messages, 0 had flags changed, 0 were trashed messages and 0 were expunged.

Password storage

During testing the IMAP password's been in the configuration file unencrypted. We can fix that by storing the credentials using GnuPG. We already set these up for using NeoMutt previously, so it's a simple change in the IMAPAccount section:

PassCmd "gpg --quiet --for-your-eyes-only --no-tty --decrypt ~/sec.gpg"

NeoMutt configuration

Last time we configuring NeoMutt for multiple accounts using folder-hooks, with one account on Runbox and one on Gmail. This time, we update the mailboxes section so that NeoMutt uses the local mirrored mailboxes:

set folder = ~/.mail/runbox    # set to the primary account's folder
set mbox_type = maildir

set spool_file = +INBOX        # 'spool' is now pointing at the main inbox
set move       = no            # don't move mail from spool as it's pointing
                               # at the main inbox

# initial mailboxes
set record     = +Sent
set trash      = +Trash
set postponed  = +Drafts

set mail_check = 120
set timeout    = 30
set mail_check_stats = yes
set mail_check_stats_interval = 120

# Account 1: mailboxes and folder hook
mailboxes -label R:Inbox -notify -poll ~/.mail/runbox/INBOX \
          -label R:Drafts -nonotify -poll ~/.mail/runbox/Drafts \
          -label R:Sent -nonotify -poll ~/.mail/runbox/Sent \
          -label R:Trash -nonotify -nopoll ~/.mail/futurile/Trash

folder-hook ~/.mail/runbox/ 'source ~/.config/neomutt/runbox-account-neomuttrc'

# Account 2: mailboxes and folder hook
mailboxes -label G:Inbox -notify -poll ~/.mail/gmail/INBOX \
          -label G:Drafts -nonotify -poll ~/.mail/gmail/Drafts \
          -label G:Sent -nonotify -poll ~/.mail/gmail/Sent \
          -label G:Trash -nonotify -nopoll ~/.mail/gmail/Trash

 folder-hook ~/.mail/gmail/ 'source ~/.config/neomutt/gmail-account-neomuttrc'

This should all be pretty straightforward at this point. Initially the $folder points to one of the accounts (e.g. your primary one) and sets up the initial mailboxes. One thing to note is that as we're not accessing the email store over IMAP all of those settings (e.g. $imap_idle) have been removed, and $mailcheck can be tuned to whatever is ideal.

The mailboxes command operates the same, this time it's set to find mailboxes in the local store (e.g. ~/.mail/runbox/). This explains the folder hook which matches against ~/.mail/runbox/ or ~/.mail/gmail/ and then runs the appropriate account-specific configuration.

Here's the shortened runbox-account-neomuttrc:

# vim: ft=muttrc fdm=marker foldcolumn=2

set folder     = ~/.mail/runbox
set spool_file = +INBOX
set record     = +Sent
set trash      = +Trash
set from       = user@email.domain
set real_name  = "Your Name"

# set status: black (color0) text, yellow (color11) foreground
color status color0 color39  # change status color for this account

Then the gmail-account-neomuttrc:

# vim: ft=muttrc fdm=marker foldcolumn=2
set folder     = ~/.mail/gmail
set spool_file = +INBOX
unset record                  # Gmail automatically records sent email
                              # if $record is set it will duplicate mail

set trash      = +[Gmail]/Trash
set posponed   = +[Gmail]/Drafts
set from       = user@gmail.com
set real_name  = "Your Name"

# set status: black (color0) text, DeepSkyBlue1 (color39) foreground
color status color0 color11

As the default is to use the primary account, there's no need for a separate local configuration. When writing email in an account, any postponed email will be saved to the correct Drafts folder.

Recap and next!

At this point we can locally mirror email by manually runnng mbsync. When testing is complete change the settings to bi-directional sync (Sync All in each Channel) and remove the limit on the number of messages to download (MaxMessages).

At the start of this series I wanted to cover three different ways to retrieve and interact with email using NeoMutt: local email on a Unix server, native IMAP and mirrored IMAP. Mirrored IMAP is a bit complex as we have a few moving parts, but it's the most flexibile.

To make our mbsync configuration sing we need to automate it so that new mail comes in automatically. We'll tackle that next time, using Goimapnotify and a Network Manager dispatcher script.


Posted in Tech Tuesday 20 May 2025
Tagged with tech ubuntu guix email neomutt isync mbsync