1. Posts/

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.