Moving the email onto a single platform
i’d setup the yttrx mail platform years and years ago and just… let it run. i was always impressed with how little babysitting it took, and there were times when it had over a year of uptime! (which, imo is a really bad thing, because that meant that i wasn’t actually updating the system).
the thing is, i love email, and working with email. email was the first internet service that i ever configured – back in the mid/late nineties on a 486 running slackware linux, i installed sendmail and ran my own mini mailserver for myself and a handful of friends. my point being, i shouldn’t be afraid of managing the mail.
one of the biggest issues that i was having with how the mail was currently setup: it’s IP space and reputation.
the vm was sitting in digital ocean, and the entire ip space of that datacenter was getting on low ip reputation lists, causing mail to be marked as spam etc. this server is used for sending signup emails, and handling any admin stuff – i didn’t want its mail getting binned.
i was also in the process of migrating my personal email servers off of VMs and onto a phyiscal FreeBSD machine over in hetzner, so i thought that it was a good time to configure that box to double duty things.
FreeBSD #
FreeBSD is imo pretty great – and different enough from Linux that i had a bunch of fun doing things the FreeBSD way. one major difference between Linux and FreeBSD was how containers are used. i didn’t want to install all of the services together in the space space, so i made use of freebsd jails
. i had previously created a jail called mx for my personal mail’s inbound mta, so i created another jail to handle yttrx’s imap services.
root@bsd:~ # bastille list | grep -e mx -e yttrx
14 mx on 99 Up thin 192.168.1.125 tcp/25:25 15.1-RELEASE -
18 yttrximap on 99 Up thin 192.168.1.128 - 15.1-RELEASE -
this required me to teach mx how to route mail to the yttrximap jail.
Postfix #
i run postfix because i know it, not because i think it’s better than the alternatives. on mx there’s a /usr/local/etc/postfix/main.cf file that handles how to route my yttrx mail. it looks something like:
# yttrx.com: every address -> waffles@yttrx.com (catch-all, mirrors the DO box)
virtual_alias_maps = hash:/usr/local/etc/postfix/virtual
which aliases all of the inbound addresses to a single concrete account, and to disallow spamming to catchall accounts there’s:
smtpd_recipient_restrictions =
check_recipient_access hash:/usr/local/etc/postfix/yttrx_access,
where yttrx_access had an entry for each valid mailbox (or alias) and a catch all to reject everything else:
admin@yttrx.com FILTER lmtp:[192.168.1.128]:24
yttrx.com REJECT No such user
Note, this tells postfix to deliver to port 24, which is the LMTP protocol.
Dovecot imap #
once again, i don’t use this software because i love it, i use it because i (kinda) understand it).
the main dovcot config:
root@yttrximap:/usr/local/etc/dovecot # cat dovecot.conf
## yttrximap — permanent yttrx IMAP backend (Maildir on host ZFS /vmail, jail ephemeral)
## Private only (listen on the bastille0 bridge); fronted later by imapgw. NOT public.
protocols = imap lmtp
listen = 192.168.1.128
log_path = /var/log/maillog
info_log_path = /var/log/maillog
ssl = no
disable_plaintext_auth = no
auth_mechanisms = plain login
auth_username_format = %Lu
mail_location = maildir:/vmail/%d/%n/Maildir
mail_uid = vmail
mail_gid = vmail
first_valid_uid = 2000
last_valid_uid = 2000
passdb {
driver = passwd-file
args = /usr/local/etc/dovecot/users
}
userdb {
driver = static
args = uid=2000 gid=2000 home=/vmail/%d/%n
}
service lmtp {
inet_listener lmtp {
address = 192.168.1.128
port = 24
}
}
# match the DO (mbox) source hierarchy separator so dsync/imapc agrees
namespace inbox {
inbox = yes
prefix =
separator = /
}
## --- Sieve (Pigeonhole) added 2026-06-18 ---
## Filters inbound mail at LMTP delivery. Active script lives on persistent
## host ZFS (~ = /vmail/%d/%n), so it survives jail rebuilds.
protocol lmtp {
mail_plugins = $mail_plugins sieve
}
plugin {
sieve = file:~/sieve;active=~/.dovecot.sieve
}
effectively sets up the system to use /usr/local/etc/dovecot/users for user management, where the mail is delivered, and where the sieve rules are located.
Sieve #
sieve is kinda weird, but it gets the job done. mine looks like this:
root@yttrximap:/vmail/yttrx.com/waffles # cat .dovecot.sieve
require ["fileinto", "mailbox"];
# NOTE: every yttrx.com address is catch-all-rewritten to waffles@yttrx.com at
# the envelope, so role sorting matches the message To/Cc headers (untouched by
# the rewrite), NOT envelope "to".
# --- role addresses ---
if address :is ["to","cc"] "postmaster@yttrx.com" { fileinto :create "Postmaster"; stop; }
if address :is ["to","cc"] "abuse@yttrx.com" { fileinto :create "Abuse"; stop; }
# --- DMARC aggregate reports ---
if address :is "from" "noreply-dmarc-support@google.com" { fileinto :create "DMARC"; stop; }
# --- mailing lists / bulk ---
if exists "list-id" { fileinto :create "Lists"; stop; }
# --- Mastodon / server-generated mail ---
if address :is "from" "admin@yttrx.com" { fileinto :create "Mastodon"; stop; }
# everything else -> INBOX (implicit keep)
which basically allows me to use a single mailbox account but handle the different aliases getting sorted properly.
Sending mail #
there’s another flow that handles sending mail via postfix/submission, but i won’t go into that – it’s less interesting imo.
Final thoughts #
all of this is managed in ansible, so it’ll be pretty durable and i hopefully won’t forget how its setup in case i need to recreate the stack on another server in the next couple years. plus, collapsing it onto my personal mail’s hardware saves me a few bucks which is always nice.