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.
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 pathsmaster.cf— usually copy as-is- Any
mysql-*.cflookup files /etc/aliases— runnewaliasesafter 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). NotMailScanner. 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
RELEASEfield 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_VERSIONfinds nothing. - Z-Push GitHub tags have a
vprefix; the version file doesn’t. Strip it. dccproc -Vreturns non-zero. Add|| trueif running underset -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.phppreserved - SOGo database migrated
- Z-Push
config.phppreserved - LDAP/AD auth tested via SOGo web UI
- ActiveSync test from a mobile device
- CalDAV/CardDAV test
- ClamAV signatures updating (
freshclamand unofficial sigs) - Mail flow tested end-to-end
- TLS tested (
openssl s_client -connect <yourhost>:443) - Old server kept on standby for 48 hours