In an effort to move away from centralized services, I decided to stop using Gmail and instead set up a self-hosted email using Mail-in-a-box. A perk of using a custom domain is that you get unlimited email addresses without using +tag in addresses. I intended to use a unique email address for each service I sign up for and forward the incoming emails to a single mailbox. Thanks to Mail-in-a-box, setting up a catch-all address for this purpose was extremely simple. Since I happen to have multiple domains, I was able to forward all incoming emails from those domains to a single mailbox, which again, was simple using domain aliases.

For the mail client, I settled on mu4e, because I was using Emacs for a lot of other things anyway. Mu is a command line tool for indexing and dealing with email stored in the Maildir format. A simple mu find <query> searches through the mails in an instant, such as mu find flags:unread would bring up unread emails. Mu includes mu4e, the Emacs client for mu.

Fetching email Link to heading

Before we can do anything with emails in Emacs, we need to fetch them from the server. We can use tools such as OfflineIMAP or Isync to achieve this. I chose isync since I found it to be considerably faster than offlineimap at retrieving emails. The isync executable is named mbsync and its config file is stored in $HOME/.mbsyncrc. The basic config for a single mailbox, which mine is, is here:

IMAPAccount MyAccount
Host mail.domain.tld
User user@domain.tld
PassCmd "gpg --quiet --for-your-eyes-only --no-tty --decrypt ~/.authinfo.gpg"
SSLType IMAPS

IMAPStore MyAccount-remote
Account MyAccount

MaildirStore MyAccount-local
SubFolders Verbatim
Path ~/Maildir/MyAccount/
Inbox ~/Maildir/MyAccount/Inbox

Channel MyAccount
Far :MyAccount-remote:
Near :MyAccount-local:
Patterns *
Create Both
Expunge Both
SyncState *

After the configuration is done, you can fetch all the emails from the server with mbsync -a which will take some time depending on the size of your mailbox. Make sure to have your Maildir folders exist before fetching your emails.

One way to be up-to-date with incoming emails is to run mbsync periodically using a systemd timer as shown here. Another way would be to use IMAP IDLE to get push notifications to download new emails, as and when they arrive on the server. I used goimapnotify, which works better with frequent network interruptions. A basic configuration for our mailbox is here:

{
  "host": "mail.domain.tld",
  "port": 993,
  "tls": true,
  "tlsOptions": {
    "rejectUnauthorized": true
  },
  "username": "user@domain.tld",
  "passwordCmd": "gpg --quiet --for-your-eyes-only --no-tty --decrypt ~/.authinfo.gpg",
  "onNewMail": "mbsync -a",
  "onNewMailPost": "",
  "wait": 10,
  "boxes": [
    "INBOX",
  ]
}

You can store this config file in $HOME/.config/imapnotify/MyAccount/notify.conf and start/enable the goimapnotify@MyAccount.service user unit.

Indexing with mu Link to heading

Once all the mail is downloaded, you need to initialize mu using:

$ mu init --maildir=Maildir --my-address=user@domain.tld

And then index the emails with mu index.

Alert for new mail Link to heading

The next task is to be notified whenever new emails are received. I hacked together a bash script to display notifications using dunst:

ICON_PATH="PATH/TO/SOME/MAIL_ICON.svg"
UNREAD_EMAILS=$(mu find flag:unread --format=json 2>/dev/null | jq -c '.[] | {subject: .":subject", from: (.":from"[0].":name" + " <" + .":from"[0].":email" + ">")}')

[ "$UNREAD_EMAILS" ] || exit 1

echo "$UNREAD_EMAILS" | while read -r line; do
    from=$(echo "$line" | jq -r '.from')
    subject=$(echo "$line" | jq -r '.subject')
    dunstify -a "Unread Email" -u normal -i "$ICON_PATH" "$from" "$subject"
done

You can add the path of this script to the onNewMailPost section in goimapnotify to show a notification for every incoming email.

Sending emails Link to heading

Emails are sent with msmtp. The configuration is fairly straightforward

defaults
port 465
tls on
tls_starttls off

account MyAccount
host mail.domain.tld
from user@domain.tld
auth on
user user@domain.tld
passwordeval "gpg --quiet --for-your-eyes-only --no-tty --decrypt ~/.authinfo.gpg"

account default: MyAccount

Mu4e configuration Link to heading

There’s not much to do to get mu4e up and running if you’re using Doom Emacs.

(after! mu4e
  (setq sendmail-program (executable-find "msmtp")
        send-mail-function #'smtpmail-send-it
        message-sendmail-f-is-evil t
        message-send-mail-function #'message-send-mail-with-sendmail)

  (set-email-account! "MyAccount"
    '((mu4e-sent-folder       . "/MyAccount/Sent")
      (mu4e-drafts-folder     . "/MyAccount/Drafts")
      (mu4e-trash-folder      . "/MyAccount/Trash")
      (smtpmail-smtp-user     . "user@domain.tld"))
     t)

Since it’s a catch-all email address, the reply to an email should be from the same address it was sent to. To do this, here’s a little snippet that fills in the FROM: address based on the TO: address of the email that’s being replied to:

(defun my/mu4e-change-from-to (msg)
  "Set the FROM address based on the TO address of the original message"
  (setq user-mail-address
        (or (and msg (plist-get (mu4e-message-contact-field-matches msg :to "domain-a.tld\\|domain-b.tld\\|domain-c.tld") :email))
            (cdr-safe (assoc 'smtpmail-smtp-user (mu4e-context-vars (mu4e-context-current)))))))

We can add it to the mu4e-compose-pre-hook to change the address when composing an email.

(add-hook! 'mu4e-compose-pre-hook
  (my/mu4e-change-from-to mu4e-compose-parent-message))

I’ve deliberately omitted the --read-envelope-from argument from sendmail settings to keep the same FROM envelope for all outgoing emails. The DNS settings allow domain-a.tld to send messages on behalf of domain-b.tld and others. Otherwise, I’d have had to make mailboxes for each domain.

This is just the basic configuration to get things working. You can find a very comprehensive mail setup in tecosaur’s config.