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.