Robert's Blog

Migrating Off eFa to a Self-Managed Rocky 8 Mail Stack

eFa is EOL. Here's how to rebuild its stack on Rocky Linux 8 and keep it maintained.

· Tech Stuff

Why Rocky 8 and not Rocky 9?

Z-Push’s IMAP backend depends on php-imap. That extension was dropped from PHP 8.x and doesn’t exist in EL9’s appstream. Until Z-Push ships a replacement backend (tracked at Z-Hub/Z-Push#169), Rocky 8 is where you live. Rocky 8 EOL is May 2029, which is probably longer than ActiveSync stays relevant anyway.

If you don’t need Z-Push — if all your mobile clients are happy with native CalDAV/CardDAV — Rocky 9 is fine.

SOGo ?

I added SOGo to my efa appliance rather than maintain a seperate groupware server. If you don’t have this just skip the SOGo steps


Base repos

dnf install -y epel-release
dnf install -y https://rpms.remirepo.net/enterprise/remi-release-8.rpm
dnf module enable -y php:remi-8.4
dnf install -y dnf-utils

Mail stack packages

# MTA and delivery
dnf install -y postfix postfix-mysql dovecot dovecot-mysql

# AV and spam
dnf install -y clamav clamav-update spamassassin

# MailScanner — pull latest from GitHub
MS_VER=$(curl -sf https://api.github.com/repos/MailScanner/v5/releases/latest \
  | grep tag_name | sed -E 's/.*"([^"]+)".*/\1/')
dnf install -y \
  "https://github.com/MailScanner/v5/releases/download/${MS_VER}/MailScanner-${MS_VER}.noarch.rpm"

# SOGo groupware
dnf install -y sogo sogo-activesync

# PHP modules for Z-Push and MailWatch
dnf install -y php php-imap php-mbstring php-soap php-mysqlnd

Postfix

Copy /etc/postfix/ from the old server. Things to review:

  • main.cf — myhostname, mydomain, mynetworks, TLS cert paths
  • master.cf — usually copy as-is
  • Any mysql-*.cf lookup files
  • /etc/aliases — run newaliases after copying
postfix check
systemctl enable --now postfix

Dovecot

Copy /etc/dovecot/ from the old server. Check conf.d/10-ssl.conf for cert paths — they’ll point at wherever the old server kept them.

systemctl enable --now dovecot

ClamAV and unofficial signatures

dnf install -y clamav-unofficial-sigs
freshclam
clamav-unofficial-sigs.sh --upgrade
systemctl enable --now clamav-unofficial-sigs clamav-freshclam

DCC

DCC isn’t in any repo. Copy /var/dcc from your old server if the architecture matches, or grab the installer from dcc.rhyolite.com. The binary ends up at /usr/bin/dccproc and the updater at /var/dcc/libexec/updatedcc.

One gotcha: dccproc -V returns a non-zero exit code. If you’re running anything under set -e, that will bite you. Use || true.


MailScanner

Copy /etc/MailScanner/ from the old server, then review MailScanner.conf for spool paths, ClamAV and SpamAssassin settings, and the Run As User value. Copy any custom rules from rules/ and custom Perl modules from /usr/share/MailScanner/perl/custom/.

systemctl enable --now mailscanner

The service name on Rocky 8 is mailscanner (lowercase). Not MailScanner. This will catch you if you’re scripting restarts.


MailWatch

MailWatch installs from source into /var/www/html/mailscanner/. Migrate the MySQL database from the old server first, then install the web files:

MW_VER=$(curl -sf https://api.github.com/repos/mailwatch/MailWatch/releases/latest \
  | grep tag_name | sed -E 's/.*"v?([^"]+)".*/\1/')
curl -sL "https://github.com/mailwatch/MailWatch/archive/refs/tags/v${MW_VER}.tar.gz" \
  | tar xz -C /tmp/

rsync -av --exclude='conf.php' \
  /tmp/MailWatch-${MW_VER}/mailscanner/ /var/www/html/mailscanner/

rsync -av --exclude='MailWatchConf.pm' \
  /tmp/MailWatch-${MW_VER}/MailScanner_perl_scripts/*.pm \
  /usr/share/MailScanner/perl/custom/

Do not overwrite conf.php — it contains your database credentials and MailWatch settings. Copy it from the old server separately.


SOGo

Copy /etc/sogo/sogo.conf from the old server and review database connection strings and the LDAP/AD source block.

systemctl enable --now sogod

LDAP/AD authentication over LDAPS

If you’re authenticating against an LDAP server or Samba AD using LDAPS, the OpenLDAP library underneath SOGo validates the certificate independently. Two settings are required if your cert is from an internal CA:

In /etc/sogo/sogo.conf, inside the SOGoUserSources block:

TLSVerifyServerCertificate = NO;

In /etc/openldap/ldap.conf:

TLS_REQCERT allow

One thing that will get you: SOGo caches LDAP connections. If it ran for years on the old server without a restart, it was holding a live connection that masked a lot of sins — including an expired LDAPS certificate. After migration (and any time you restart SOGo), it has to establish a fresh connection. Always do a full stop/start after migration and check /var/log/sogo/sogo.log for LDAP errors before declaring victory.

If your Samba AD domain controller’s LDAPS cert has expired (SOGo will log Can't contact LDAP server (0xFFFFFFFF)), renew it with easy-rsa if you manage your own CA:

cd /path/to/easy-rsa
./easyrsa --batch --req-cn="<dc-hostname>" gen-req <dc-hostname> nopass
./easyrsa --days=1825 sign-req server <dc-hostname>
systemctl restart samba

The cert symlinks in /var/lib/samba/private/tls/ pick up the new files automatically.


Z-Push

ZP_VER=$(curl -sf https://api.github.com/repos/Z-Hub/Z-Push/releases/latest \
  | grep tag_name | sed -E 's/.*"v?([^"]+)".*/\1/')
curl -sL "https://github.com/Z-Hub/Z-Push/archive/refs/tags/${ZP_VER}.tar.gz" \
  | tar xz -C /tmp/

ZP_DIR=$(find /tmp -maxdepth 1 -type d -name "Z-Push*" | head -1)
rsync -avp --ignore-times \
  --exclude='config.php' --exclude='*config.php' --delete \
  "${ZP_DIR}/src/" /usr/share/z-push/

Copy your existing config.php from the old server — don’t let the rsync overwrite it.

SOGo CalDAV/CardDAV backend patch

If you’re using the SOGo backend for calendar and contacts sync, you may need a patch to handle null getlastmodified responses:

patch -p1 -d /usr/share/z-push < /path/to/zpush-sogo.patch
php -r 'opcache_reset();'
systemctl restart httpd php-fpm

Z-Push’s version is stored as plain text in /usr/share/z-push/version — not a PHP constant.


CPAN modules

MailScanner and MailWatch depend on several CPAN modules. Install libdb-devel first or DB_File will fail to build:

dnf install -y perl-App-cpanminus libdb-devel
curl -L https://cpanmin.us | perl - App::cpanoutdated
cpan-outdated | cpanm

Automating post-update maintenance

The part that makes self-managing this stack sustainable is not having to remember what to check after each OS update. I wrote a script that handles all six maintenance tasks — ClamAV unofficial sigs, DCC, MailScanner, MailWatch, Z-Push, and CPAN modules — checking GitHub releases against installed versions and upgrading only what’s behind.

A few non-obvious things the script has to get right:

  • MailScanner’s RPM RELEASE field includes a dist tag (.rhel). Strip it before comparing to GitHub tags or you’ll get false upgrade detections every run.
  • MailWatch’s version is a string literal returned by a function in functions.php, not a constant. grep MAILWATCH_VERSION finds nothing.
  • Z-Push GitHub tags have a v prefix; the version file doesn’t. Strip it.
  • dccproc -V returns non-zero. Add || true if running under set -e.

The script is on GitHub.


Firewall

firewall-cmd --permanent --add-service={smtp,smtps,imap,imaps,http,https}
firewall-cmd --reload

Migration checklist

  • DNS updated / hostname matches
  • Postfix config copied and tested (postfix check)
  • Dovecot config copied, mail spool migrated
  • MailWatch database migrated and conf.php preserved
  • SOGo database migrated
  • Z-Push config.php preserved
  • LDAP/AD auth tested via SOGo web UI
  • ActiveSync test from a mobile device
  • CalDAV/CardDAV test
  • ClamAV signatures updating (freshclam and unofficial sigs)
  • Mail flow tested end-to-end
  • TLS tested (openssl s_client -connect <yourhost>:443)
  • Old server kept on standby for 48 hours