This tutorial provides a complete, production-ready guide to integrating the Mark as Junk plugin in Roundcube Webmail with SpamAssassin (sa-learn) on a DirectAdmin server. Using the spool directory driver (dir_learn) combined with a background poller script and per-user sudoers permissions ensures seamless processing without web interface timeout or permission errors.
Step 1: Create the Spam and Ham Directory
Create the spool folders where Roundcube will temporarily drop messages reported as junk or ham:
mkdir -p /var/spool/markasjunk/spam
mkdir -p /var/spool/markasjunk/ham
chown webapps:webapps /var/spool/markasjunk/spam /var/spool/markasjunk/ham
chmod 770 /var/spool/markasjunk/spam /var/spool/markasjunk/ham
Step 2: Automate Sudoers Rules for New Users (DirectAdmin Hook)
Set up a DirectAdmin post-user-creation script to automatically grant the webapps user passwordless sudo permissions for running sa-learn under newly created accounts:
mkdir -p /usr/local/directadmin/scripts/custom
cat > /usr/local/directadmin/scripts/custom/user_create_post.sh << 'SCRIPT'
#!/bin/sh
DAUSER="${username}"
if [ -z "$DAUSER" ]; then
exit 0
fi
SUDOFILE="/etc/sudoers.d/sa-learn-${DAUSER}"
if [ ! -f "$SUDOFILE" ]; then
cat > "$SUDOFILE" << EOF
webapps ALL=(${DAUSER}) NOPASSWD: /usr/bin/sa-learn --spam --dbpath=/home/${DAUSER}/.spamassassin/bayes *
webapps ALL=(${DAUSER}) NOPASSWD: /usr/bin/sa-learn --ham --dbpath=/home/${DAUSER}/.spamassassin/bayes *
EOF
chmod 440 "$SUDOFILE"
visudo -cf "$SUDOFILE" >/dev/null 2>&1 || rm -f "$SUDOFILE"
fi
exit 0;
SCRIPT
chmod 700 /usr/local/directadmin/scripts/custom/user_create_post.sh
chown diradmin:diradmin /usr/local/directadmin/scripts/custom/user_create_post.sh
Step 3: Provision Sudoers Rules for Existing Users
Create and execute a bootstrap script to generate the appropriate sudoers entries for all existing DirectAdmin users:
cat > /root/bootstrap-sa-learn-sudoers.sh << 'SCRIPT'
#!/bin/bash
while IFS=': ' read -r DOMAIN DAUSER; do
DAUSER=$(echo "$DAUSER" | tr -d '[:space:]')
[ -z "$DAUSER" ] && continue
SUDOFILE="/etc/sudoers.d/sa-learn-${DAUSER}"
if [ ! -f "$SUDOFILE" ]; then
cat > "$SUDOFILE" << EOF
webapps ALL=(${DAUSER}) NOPASSWD: /usr/bin/sa-learn --spam --dbpath=/home/${DAUSER}/.spamassassin/bayes *
webapps ALL=(${DAUSER}) NOPASSWD: /usr/bin/sa-learn --ham --dbpath=/home/${DAUSER}/.spamassassin/bayes *
EOF
chmod 440 "$SUDOFILE"
visudo -cf "$SUDOFILE" >/dev/null 2>&1 && echo "Provisioned: $DAUSER" || { rm -f "$SUDOFILE"; echo "FAILED (invalid): $DAUSER"; }
fi
done < /etc/virtual/domainowners
SCRIPT
chmod +x /root/bootstrap-sa-learn-sudoers.sh
/root/bootstrap-sa-learn-sudoers.sh
Verify that the rules were created successfully:
ls /etc/sudoers.d/ | grep sa-learn
Step 4: Configure the Mark as Junk Plugin Source File
Locate the active Roundcube webmail path and edit the plugin configuration to use the spool directory driver:
RCSOURCEPATH=$(find /var/www -maxdepth 2 -type d -iname "roundcubemail-*" 2>/dev/null | head -1)
echo "Live path: $RCSOURCEPATH"
cp "$RCSOURCEPATH/plugins/markasjunk/config.inc.php.dist" "$RCSOURCEPATH/plugins/markasjunk/config.inc.php"
sed -i \
-e "s|\$config\['markasjunk_learning_driver'\] = null;|\$config['markasjunk_learning_driver'] = 'dir_learn';|" \
-e "s|\$config\['markasjunk_spam_dir'\] = null;|\$config['markasjunk_spam_dir'] = '/var/spool/markasjunk/spam';|" \
-e "s|\$config\['markasjunk_ham_dir'\] = null;|\$config['markasjunk_ham_dir'] = '/var/spool/markasjunk/ham';|" \
-e "s|\$config\['markasjunk_filename'\] = null;|\$config['markasjunk_filename'] = '%d_%u';|" \
"$RCSOURCEPATH/plugins/markasjunk/config.inc.php"
Confirm the plugin configuration parameters:
grep -E "learning_driver|spam_dir|ham_dir|filename" "$RCSOURCEPATH/plugins/markasjunk/config.inc.php"
Step 5: DirectAdmin CustomBuild Persistent Setup
Copy configuration and plugin files into the CustomBuild directory structure to ensure changes are retained across updates:
rm -rf /usr/local/directadmin/custombuild/custom/roundcube
mkdir -p /usr/local/directadmin/custombuild/custom/roundcube
cp "$RCSOURCEPATH/config/config.inc.php" /usr/local/directadmin/custombuild/custom/roundcube/config.inc.php
chmod 640 /usr/local/directadmin/custombuild/custom/roundcube/config.inc.php
RCCUSTOMCONFIG="/usr/local/directadmin/custombuild/custom/roundcube/config.inc.php"
sed -i -E \
-e "s/['\"]managesieve['\"],[[:space:]]*//g" \
-e "s/,[[:space:]]*['\"]managesieve['\"]//g" \
"$RCCUSTOMCONFIG"
if ! grep -qE "['\"]markasjunk['\"]" "$RCCUSTOMCONFIG"; then
sed -i -E "s|(\\\$config\['plugins'\]\s*=\s*\[[^]]*)\]|\1,'markasjunk']|" "$RCCUSTOMCONFIG"
fi
grep -E "plugins" "$RCCUSTOMCONFIG"
mkdir -p /usr/local/directadmin/custombuild/custom/roundcube/plugins
cp -r "$RCSOURCEPATH/plugins/markasjunk" /usr/local/directadmin/custombuild/custom/roundcube/plugins/
Step 6: Rebuild Roundcube and Restart Services
Recompile Roundcube via CustomBuild and restart Exim and SpamAssassin:
da build roundcube
systemctl restart spamd
da build exim_conf
Login to Roundcube, and set the INBOX.spam as Junk mailbox.
Settings > Preferences > Special Folders > "Junk = spam "
Step 7: Create the Background Poller Script and Cron Job
Set up a poller script that reads spooled messages, resolves the DirectAdmin user for each domain, trains SpamAssassin using sa-learn, and runs every 5 minutes via root cron:
cat > /usr/local/bin/process-markasjunk-spool.sh << 'SCRIPT'
#!/bin/bash
LOG=/var/log/rc-sa-learn.log
process_dir() {
local DIR="$1"
local TYPE="$2"
for f in "$DIR"/*; do
[ -f "$f" ] || continue
FNAME=$(basename "$f")
DOMAIN=$(echo "$FNAME" | cut -d_ -f1)
DAUSER=$(grep -i "^${DOMAIN}:" /etc/virtual/domainowners 2>/dev/null | awk -F': ' '{print $2}' | tr -d '[:space:]')
if [ -z "$DAUSER" ]; then
echo "$(date): ERROR - could not resolve DA user for domain $DOMAIN (file: $FNAME)" >> "$LOG"
mv "$f" "${DIR}/../failed_${FNAME}" 2>/dev/null
continue
fi
BAYESPATH="/home/${DAUSER}/.spamassassin/bayes"
sudo -u "$DAUSER" /usr/bin/sa-learn "--${TYPE}" --dbpath="$BAYESPATH" < "$f" >> "$LOG" 2>&1
if [ $? -eq 0 ]; then
rm -f "$f"
else
echo "$(date): WARNING - sa-learn failed for $FNAME" >> "$LOG"
fi
done
}
process_dir /var/spool/markasjunk/spam spam
process_dir /var/spool/markasjunk/ham ham
SCRIPT
chmod +x /usr/local/bin/process-markasjunk-spool.sh
(crontab -l 2>/dev/null; echo "*/5 * * * * /usr/local/bin/process-markasjunk-spool.sh") | crontab -
crontab -l
Step 8: Verification and Logs
- Log into the Roundcube Webmail interface.
- Select an email and click Mark as Junk (or As Not Junk).
- Verify that the message file lands in the spool folder and check the log output after the cron runs:
ls -la /var/spool/markasjunk/spam/ tail -20 /var/log/rc-sa-learn.log
Maintenance: Future Upgrade Workflow
When upgrading Roundcube via CustomBuild in the future, automatically resolve the active installation directory and compare your custom configuration files against the newly installed distribution files to detect any new configuration flags:
RCSOURCEPATH=$(find /var/www -maxdepth 2 -type d -iname "roundcubemail-*" 2>/dev/null | head -1)
# Check main configuration differences
diff /usr/local/directadmin/custombuild/custom/roundcube/config.inc.php "$RCSOURCEPATH/config/config.inc.php"
# Check plugin configuration differences
diff /usr/local/directadmin/custombuild/custom/roundcube/plugins/markasjunk/config.inc.php "$RCSOURCEPATH/plugins/markasjunk/config.inc.php.dist"
If any new $config[...] entries appear in the output, append or adapt them inside your custom configuration files under /usr/local/directadmin/custombuild/custom/roundcube/.
