E-mail

This document no longer reflects how I access my e-mail, but I also expect I'll be changing my current system in the near future. Leaving it as it was on my gemsite for now.

I use mblaze, isync/mbsync, msmtp, and some custom scripts for handling e-mail.

mblaze is a suite of utilities for managing maildir mailboxes, in the Unix spirit of simple, combinable tools.

mbsync synchronizes IMAP4 and maildir mailboxes.

msmtp is a simplified sendmail alternative.

I also secure the passwords for my e-mail using two hardware devices: a physical password manager (the Mooltipass) and a PGP-capable crypto fob (the Ledger Nano S), which I also use for managing my SSH keys.

In short, I:

Securing passwords

My e-mail account passwords are auto-generated and stored on a hardware device that can act as a keyboard. This is about as secure as passwords get, as far as I can tell. This is not practical for checking e-mail periodically in the background, however.

The Ledger allows me to generate cryptographic keys deterministically on the device itself based on a randomly-generated string of 24 words. The algorithm is open-source, so as long as I can keep track of my 24 words, all the keys I've ever generated with it should be recoverable. I use trezor-agent to integrate the device with GPG and SSH. This allows me to keep my e-mail passwords encrypted on disk, decrypting them only to put them into a dedicated user account's kernel keyring once per boot.

Setting up trezor-agent for use with the Ledger and GPG is outside the scope of this document, but the process is described in that project's source code repository under the docs folder. Once set up, I used the following AGPL-3 licensed script (with the -p flag set) to create a store of e-mail address keys to password values at ~/.mbsync-passwords:

#!/usr/bin/env bash

# <AGPL-3 license omitted here for brevity>

# Has no way to handle tabs in keys or values, so don't try it!

set -e
set -o pipefail # This is the one line that keeps this a bash script.

[ ! -e "$HOME/.gpgkvrc" ] || . "$HOME/.gpgkvrc"

export GPGBIN="${GPGBIN:-gpg2 -q}"
export GPGKV_IDENT="${GPGKV_IDENT:-$USER}"
export GPGKV_STORE="${GPGKV_STORE:-$HOME/.gpgkv-store}"

case "$1" in
        add)
                if [ "$2" = "-p" ]; then
                        valopt="-s"
                        valprompt="Password"
                        shift
                else
                        valprompt="Value"
                fi

                key="$2"
                val="$3"

                [ "$key" ] || { echo -n "Key: " && read key; }
                [ "$val" ] || { echo -n "$valprompt: " && read $valopt val; }

                if [ ! -e "$GPGKV_STORE" ]; then
                        printf "%st%sn" "$key" "$val" 
                        | $GPGBIN --armour --encrypt -r "$GPGKV_IDENT" 
                        | tee "$GPGKV_STORE" >/dev/null
                else
                        echo "$($GPGBIN --decrypt "$GPGKV_STORE" 
                                                | xargs -0 printf "%st%sn%s" "$key" "$val" 
                                                | $GPGBIN --armour --encrypt -r "$GPGKV_IDENT")" 
                        > "$GPGKV_STORE"
                fi
                echo # in case it used a password prompt
                ;;

        get)
                $GPGBIN --decrypt "$GPGKV_STORE" 
                | grep -o "^$2t" 
                | cut -f2
                ;;

        del)
                $GPGBIN --decrypt "$GPGKV_STORE" 
                | grep -v "^$2t" 
                | $GPGBIN --armour --encrypt -r "$GPGKV_IDENT" 
                | tee "$GPGKV_STORE" >/dev/null
                ;;

        dump)
                $GPGBIN --decrypt "$GPGKV_STORE"
                ;;

        *)
                echo "Usage: $0 <command> [args]"
                echo "Commands:"
                echo "  add [-p] [<key> <value>]"
                echo "  get <key>"
                echo "  del <key>"
                echo "  dump"
esac

To write to ~/.mbsync-passwords instead of the default ~/.gpgkv-store, set the GPGKV_STORE variable when invoking gpgkv. E.g., "GPGKV_STORE='~/.mbsync-passwords' gpgkv ..."

Editing sudoers

Creating a dedicated user for the purpose of running msmtp and mbsync and keeping the passwords in their kernel keyring seemed to me like the easiest, safest way to keep the passwords in memory. In order to avoid having to type my user password during the normal execution of this user's duties, I made these entries in my sudoers file:

lykso ALL=(mbsync) NOPASSWD: /bin/keyctl padd user *@* @u
lykso ALL=(mbsync) NOPASSWD: /bin/keyctl link @us @s
lykso ALL=(mbsync) NOPASSWD: /usr/bin/mbsync -a
lykso ALL=(mbsync) NOPASSWD: /usr/bin/msmtp
lykso ALL=(mbsync) NOPASSWD: /bin/chmod -R g=u /home/mbsync/mail/*@*

If you copy this, be sure to verify the locations of keyctl, mbsync, msmtp, and chmod. I moved this to another distribution of Linux recently and had some trouble when, e.g., mbsync moved from /bin/mbsync to /usr/bin/mbsync.

Loading passwords

To load the passwords, I wrote a simple script that iterates over every entry in ~/.mbsync-passwords and puts it in mbsync's keyring:

#!/usr/bin/env bash

# Opens the .mbsync-passwords file with gpgkv and loads each key/value pair into
# the kernel's keystore for the mbsync user.

set -e
set -o pipefail

export GPGKV_STORE="$HOME/.mbsync-passwords"

# Make sure the session and user session keyrings are linked.
# User keys cannot be found otherwise.
# Some distributions do this by default, but others don't.
sudo -u mbsync keyctl link @us @s

gpgkv dump | while read line; do
	# Doing it this way to keep the credentials out of the process list.
	IFS=$'t' read -r -a credentials <<<"$line"
	sudo -u mbsync keyctl padd user "${credentials[0]}" @u <<<"${credentials[1]}"
done

As noted before, this script has to be re-run at every boot. The integration with my Ledger is seamless, so this script can be used as-is regardless of whether or not you have such a thing.

Configuring msmtp, mbsync, and mblaze

.msmtprc and .mbsyncrc both are kept in /home/mbsync and belong to the mbsync user and group.

/home/mbsync/.msmtprc:

defaults
auth on
tls on
tls_trust_file /etc/ssl/certs/ca-certificates.crt
logfile ~/.config/msmtp/msmtp.log

account lykso@lyk.so
host mail.privateemail.com
port 465
tls_starttls off
from lykso@lyk.so
user lykso@lyk.so
passwordeval "keyctl request user lykso@lyk.so | xargs keyctl pipe"

/home/mbsync/.mbsyncrc:

IMAPAccount lykso@lyk.so
Host mail.privateemail.com
Port 993
User lykso@lyk.so
PassCmd "keyctl request user lykso@lyk.so | xargs keyctl pipe"
SSLType IMAPS
SSLVersions TLSv1.2

IMAPStore lykso@lyk.so-remote
Account lykso@lyk.so

MaildirStore lykso@lyk.so-local
Path   ~/mail/lykso@lyk.so/
Inbox  ~/mail/lykso@lyk.so/Inbox
Trash  ~/mail/lykso@lyk.so/Trash
SubFolders Verbatim

Channel lykso@lyk.so
Master :lykso@lyk.so-remote:
Slave :lykso@lyk.so-local:
Patterns *
Create Both
SyncState *

.mblaze/profile is kept in my home directory and belongs to my user and group:

Local-Mailbox: lykso@lyk.so
Sendmail: sudo -u mbsync msmtp
Sendmail-Args: --read-recipients --read-envelope-from

Custom scripts

After adding my user to the mbsync group, symlinking ~/mail to /home/mbsync/mail, and running chmod g+rw /home/mbsync/mail, I wrote a couple scripts to make using all these pieces togther easier. First was check-mail, for which I created a cronjob that runs every 10 minutes:

#!/usr/bin/env sh

# Dependencies:
# mblaze
# mbsync
# notify-send

set -e

sudo -u mbsync mbsync -a
sudo -u mbsync chmod -R g=u /home/mbsync/mail/*@*
new="$(find /home/mbsync/mail -path '**/new/**' | wc -l)"

[ "$new" != "0" ] || exit 0

# Void doesn't set this, but Debian does.
## notify-send needs this set.
#export XDG_RUNTIME_DIR="$HOME/.service/xdg"

[ "$new" = 1 ] || plural="s"
notify-send "You've got mail!" "$new new message${plural}!"

mdirs /home/mbsync/mail | while read mdir; do
  minc "$mdir" 2>&1 > /dev/null
done

echo "New messages: $new"

A script for listing all mail in my inboxes, and optionally searching via arguments to mpick:

#!/usr/bin/env sh

# Show all messages in the inboxes not tagged as trash.
mdirs ~/mail 
  | grep '/Inbox(/|$)' 
  | mlist 
  | mpick "$@" 
  | msort -d 
  | mseq -S 
  | mscan

Same idea, but trimmed down and focused on trash:

#!/usr/bin/env sh

# Show all messages in the trash
mdirs $1 ~/mail 
  | grep '/Trash(/|$)' 
  | mlist 
  | msort -d 
  | mscan

The shortest script, the one I use to read my mail:

#!/usr/bin/env sh

mless $1 && mflag -S $1

This only works for plaintext e-mail. For HTML e-mails, I haven't bothered writing a script yet. Any time I get an important e-mail that doesn't include a plaintext version, I just run:

mshow <e-mail number> | lynx -stdin

If it contains images that need to be seen, then I suppose I could just pipe mshow to a file and view it in Firefox or Netsurf, but that hasn't happened yet.

For archiving e-mail:

#!/usr/bin/env sh

set -e

message="$(mpick $1)"
mrefile $1 "$(echo "$message" | sed 's//Inbox/.+//Archives/')"

For trashing mail:

#!/usr/bin/env sh

set -e

[ "$1" ] || { >&2 echo "Need to specify at least one test!"; exit 1; }
mdirs ~/mail 
  | mlist 
  | mpick "$@" 
  | mflag -T 
  | while read message; do
    mrefile "$message" "$(echo "$message" | sed 's//Inbox/.+//Trash/')"
  done

For trashing things that get through my spam filter:

#!/usr/bin/env sh

# Spam
trash-mail '-t from =~ "@winsonsoloads.com" || from =~ "@welcometoterizin.org"'

Probably ought to be marking those as spam instead, but this is what I'm doing as of this writing, so there you are.

Finally, for moving mail between folders in the same mailbox:

#!/usr/bin/env sh

set -e

dest="$1"
shift

[ "$1" ] || { >&2 echo "Please specify at least one test or message!"; exit 1; }

mdirs ~/mail 
  | mlist 
  | mpick "$@" 
  | while read message; do
    account="$(echo "${message#*/mail/}" | cut -d/ -f1)"
    mrefile -v "$message" "$HOME/mail/$account/$dest"
  done

Hopefully that gives you an idea of how mblaze's utilities might be recombined to create whatever experience you're after. I very much appreciate the adherence to the Unix philosophy, as it allows me to craft whatever workflow I like. I also very much like that this keeps my e-mails in a greppable, human-friendly, flatfile format rather than in some database that needs special tools to be interacted with.

Debian addendum

After migrating from Void to Debian, I found that msmtp had stopped working. I was getting permission errors from keyctl and xargs. After some searching, I found someone else with the same problem:

Stack Overflow: awk permission denied when run through msmtp

To save you a click, the trouble was that Debian comes with a restrictive AppArmor profile for msmtp. Rather than learn how to use AppArmor and edit the profile, I followed the advice on the StackOverflow page and just disabled it for msmtp:

sudo ln -s /etc/apparmor.d/usr.bin.msmtp /etc/apparmor.d/disable/
sudo apparmor_parser -R /etc/apparmor.d/usr.bin.msmtp 

Relevant Links

Page Created:: 2021-09-13

Last Updated:: 2024-12-12

Last Reviewed:: 2024-12-12