config: Email configuration

A very opiniated mail setup

This article presents my opinionated email setup, client side. By opinionated I mean that it requires quite some stuff (like nixpkgs) and is cli/emacs/… oriented.

I used to read my mails only through the web interface of my mail provider (GMail for the most part), or through my phone. As I’m trying to use my phone less, at least for work, and as I wanted to not have a gmail tab always opened on my browser, I decided to configure an email client on my laptops/desktops.

So far, I ended up using the following tools:

Something a bit special here is that I also use home-manager… and home-manager has modules for those tools, so we are going to use thoses.

This needs to be updated and rewritten.

Module

Let’s start by defining the module, the usual Nix way.

# Generated from an org file 💃
# See : https://sbr.pm/technical/configurations/mails.html
{ config, lib, pkgs, ... }:

with lib;
let
  cfg = config.profiles.mails;
in
{

Let’s now define options. As of now, except enable (to activate or not the module) I don’t have any options in mind.

options = {
  profiles.mails = {
    enable = mkEnableOption "Enable mails configuration";
    sync = mkEnableOption "Enable sync mail service";
    frequency = mkOption {
      default = "*:0/30";
      description = "Frequency at which the mail should be checked";
      type = types.str;
    };
  };
};

Finally, create the configuration.

config = mkIf cfg.enable (mkMerge [
  {

Base settings

Accounts

The next step is to actually define the accounts we want use and where we want to store email, amongst other need.

  • We want to store mails in desktop/mails/{account}.
  • We don’t want to input password each and every time so we’re using an encrypted file (symmetric encryption using GnuPG with a passphrase file).
  • We’re gonna enable diverse modules on each account
    • mbsync to sync the mail with some setupts (like specific rules for GMail specific folders)
    • notmuch for email indexing
    • msmtp to send a mail, using the account’s smtp server
    • astroid for a GUI
accounts.email = {
  maildirBasePath = "desktop/mails";
  accounts = {
    "redhat" = {
      address = "vdemeest@redhat.com";
      userName = "vdemeest@redhat.com";
      realName = "Vincent Demeester";
      passwordCommand = "${pkgs.gnupg}/bin/gpg -q --for-your-eyes-only --no-tty --exit-on-status-write-error --batch --passphrase-file ${config.home.homeDirectory}/sync/rh.pass -d ${config.home.homeDirectory}/desktop/documents/rh.pass.gpg";
      imap.host = "imap.gmail.com";
      smtp.host = "smtp.gmail.com";
      mbsync = {
        enable = true;
        create = "both";
        expunge = "both";
        patterns = ["*" "![Gmail]*" "[Gmail]/Sent Mail" "[Gmail]/Starred" "[Gmail]/All Mail"];
        extraConfig = {
          channel = {
            Sync = "All";
          };
          account = {
            Timeout = 120;
            PipelineDepth = 1;
          };
        };
      };
      notmuch.enable = cfg.sync;
      astroid.enable = cfg.sync;
      msmtp.enable = true;
    };
    "perso" = {
      address = "vinc.demeester@gmail.com";
      userName = "vinc.demeester@gmail.com";
      realName = "Vincent Demeester";
      passwordCommand = "${pkgs.gnupg}/bin/gpg -q --for-your-eyes-only --no-tty --exit-on-status-write-error --batch --passphrase-file ${config.home.homeDirectory}/sync/perso.pass -d ${config.home.homeDirectory}/desktop/documents/perso.pass.gpg";
      imap.host = "imap.gmail.com";
      smtp.host = "smtp.gmail.com";
      mbsync = {
        enable = true;
        create = "both";
        expunge = "both";
        patterns = ["*" "![Gmail]*" "[Gmail]/Sent Mail" "[Gmail]/Starred" "[Gmail]/All Mail"];
        extraConfig = {
          channel = {
            Sync = "All";
          };
          account = {
            Timeout = 120;
            PipelineDepth = 1;
          };
        };
      };
      notmuch.enable = cfg.sync;
      astroid.enable = cfg.sync;
      msmtp.enable = true;
    };
    "prv" = {
      primary = true;
      address = "vincent@demeester.fr";
      userName = "vincent@demeester.fr";
      realName = "Vincent Demeester";
      passwordCommand = "${pkgs.gnupg}/bin/gpg -q --for-your-eyes-only --no-tty --exit-on-status-write-error --batch --passphrase-file ${config.home.homeDirectory}/sync/prv.pass -d ${config.home.homeDirectory}/desktop/documents/prv.pass.gpg";
      imap.host = "mail.gandi.net";
      smtp.host = "mail.gandi.net";
      mbsync = {
        enable = true;
        create = "both";
        expunge = "both";
        patterns = ["*"];
        extraConfig = {
          channel = {
            Sync = "All";
          };
          account = {
            Timeout = 120;
            PipelineDepth = 1;
          };
        };
      };
      notmuch.enable = cfg.sync;
      astroid.enable = cfg.sync;
      msmtp.enable = true;
    };
  };
};

To create the pasword files:

  • create ~/desktop/documents/{account}.pass.gpg file, you need to create a ~/desktop/documents/prv.pass file with the actual password.
  • create ~/sync/{account}.pass with a passphrase (long, complex, whatever…)
  • encrypt ~/desktop/documents/{account}.pass.gpg with the following command

    gpg --batch --yes --symmetric --passphrase-file ~/sync/{account}.pass --encrypt {account.pass}
    
  • remove ~/desktop/documents/{account}.pass

msmtp wrapper

As I have multiple accounts, I need to be able to send mails from those multiple accounts too. For this we will use msmtp. We will $HOME/.nix-profile/bin/msmtp to make sure it uses --read-envolep-from. This means it will look at what FROM header is set in the e-mail and use the correct account accordingly.

home.file."bin/msmtp" = {
  text = ''
  #!${pkgs.stdenv.shell}
  ${pkgs.libnotify}/bin/notify-send "Sending mail ✉️"
  ${pkgs.msmtp}/bin/msmtp --read-envelope-from $@
  '';
  executable = true;
};

We also want to make sure we enable msmtp.

programs.msmtp.enable = true;

And that should be all for the base settings, so let’s close that part

}

Syncing

I may not want to sync and index mails on all computers. In practice, I only do that on one computer and I sync these mails with the others.

(mkIf cfg.sync {

Service

Now that all the configuration are defined (and generated once we run home-manager), we’re going to enable the mbsync service to synchronize email at the given frequency.

services.mbsync = {
  enable = true;
  preExec = "${config.xdg.configHome}/mbsync/preExec";
  postExec = "${config.xdg.configHome}/mbsync/postExec";
  frequency = cfg.frequency;
};

We also setup preExec and postExec hooks on the service to be able to run commands before and after actually running mbsync.

  • preExec has two main purpose :
    • Create the accounts mail folder — this is only useful for the first run ever, but it is required.
    • Move mails on the right folders
      • from Inbox to elsewhere (All mails, …)
      • (in the future) to the right folders (from the tags)
xdg.configFile."mbsync/preExec" = {
  text = ''
  #!${pkgs.stdenv.shell}

  export NOTMUCH_CONFIG=${config.xdg.configHome}/notmuch/notmuchrc
  export NMBGIT=${config.xdg.dataHome}/notmuch/nmbug

  ${pkgs.coreutils}/bin/mkdir -p ${config.home.homeDirectory}/desktop/mails/redhat ${config.home.homeDirectory}/desktop/mails/perso
  ${pkgs.afew}/bin/afew -C  ${config.xdg.configHome}/notmuch/notmuchrc -m -v
  '';
  executable = true;
};
  • postExec will index the new emails in the notmuch database and tag mail accordingly (to their folders and other rules in place).
xdg.configFile."mbsync/postExec" = {
  text = ''
  #!${pkgs.stdenv.shell}

  export NOTMUCH_CONFIG=${config.xdg.configHome}/notmuch/notmuchrc
  export NMBGIT=${config.xdg.dataHome}/notmuch/nmbug

  ${pkgs.notmuch}/bin/notmuch new
  ${pkgs.afew}/bin/afew -C ${config.xdg.configHome}/notmuch/notmuchrc --tag --new -v
  # Remove inbox (lower-case)
  ${pkgs.notmuch}/bin/notmuch tag -inbox -- tag:inbox
  # Remove Inbox tagged message that are not in an Inbox
  ${pkgs.notmuch}/bin/notmuch tag -Inbox -- not folder:redhat/Inbox and not folder:perso/Inbox and tag:Inbox
  ${pkgs.libnotify}/bin/notify-send "Mails synced 📬"
  '';
  executable = true;
};

Finally, let’s define custom commands to simplify my mail usage. Those should be nix package in the near future — as of now, it is a bit ugly as I’m creating binaries inside $HOME/bin instead of relying of Nix.

  • msync is an helper to run quickly mbsync systemd service from anywhere
home.file."bin/msync" = {
  text = ''
  #!${pkgs.stdenv.shell}
  ${pkgs.libnotify}/bin/notify-send "Syncing mails 📫️"
  systemctl --user start mbsync
  '';
  executable = true;
};

Programs

Additionally we can enable some programs and customize their behavior. Let’s enable programs.mbsync, which has for effect to put mbsync binary in PATH so that the user (us) can call it. Same goes for programs.msmtp and programs.notmuch.

programs.mbsync.enable = true;
programs.notmuch.enable = true;

Afew

afew is “an initial tagging script for notmuch mail”. We’re going to define some extra configuration to enable some filters and MailMover rules.

Note: This should go away at some point as these rules are not dynamic enough for my usage.

programs.afew = {
  enable = true;
  extraConfig = ''
    [SpamFilter]
    [KillThreadsFilter]
    [ListMailsFilter]
    [ArchiveSentMailsFilter]
    [FolderNameFilter]
    maildir_separator = /

    [MailMover]
    folders = perso/Inbox redhat/Inbox
    rename = true

    perso/Inbox = 'NOT tag:Inbox':"perso/[Gmail]/All Mail"
    redhat/Inbox = 'NOT tag:Inbox':"redhat/[Gmail]/All Mail"
  '';
};

Astroid

astroid is a “graphical threads-with-tags style, lightweight and fast, e-mail client for Notmuch”. My main e-mail client is emacs with the notmuch mode, but sometimes I want a GUI, mainly to see wanky HTML mails that would not render correctly some times.

programs.astroid = {
  enable = true;
  externalEditor = "emacsclient -c";
  extraConfig = {
    startup.queries.inbox = "tag:Inbox";
    startup.queries.inbox_perso = "folder:perso/Inbox";
    startup.queries.inbox_redhat = "folder:redhat/Inbox";
  };
};

And that’s all for the sync part, so let’s close it

})

Close the module

]);
}

References