Compare commits

..

No commits in common. "c8d9f03587d5d8c6ca601b5232daa12a2807c091" and "4f159716ef5fd324ed6a37fbc00d6dd42fc3634f" have entirely different histories.

10 changed files with 18 additions and 1380 deletions

View file

@ -1,991 +0,0 @@
#!/bin/bash -e
#
# Installer for ISPmail servers
# Copyright © 2025 Christoph Haas <ispmail@christoph-haas.de>
#
# License: Creative Commons BY-NC-SA license
#
# Version 0.1
# 2025-12-14
#
usage() {
echo "Usage: $0 -f fqdn"
exit 1
}
FQDN=""
while getopts "f:h" opt; do
case "$opt" in
f) FQDN="$OPTARG" ;;
h) usage ;;
*) usage ;;
esac
done
############ pre flight check ############
pre_flight() {
# Check for root
if [[ $EUIA -ne 0 ]]; then
echo "❌ You must run this script as root."
exit 1
fi
# Check for Trixie
DEBIAN_VERSION=$(cut -d'.' -f1 /etc/debian_version)
if [[ "$DEBIAN_VERSION" -ne 13 ]]; then
echo "❌ Sorry, this installer only works on Debian 13/Trixie."
exit 10
fi
# FQDN needs to be given as an option
if [[ $FQDN = "" ]]; then
echo "❌ You must specify your FQDN using the '-f' option."
exit 1
fi
}
############## Intro ##############
intro() {
cat <<EOF
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣤⢶⢞⣞⢟⢽⢫⢏⠗⡗⢖⠦⡢⡠⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⣮⢾⡷⣯⣿⣽⣗⢷⡹⣮⢦⣣⡢⣅⢓⣕⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⢻⡿⣟⣯⢿⢯⢷⡻⡽⢽⢾⢽⣳⠓⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⣺⡽⡾⣽⣽⣵⣇⣯⡺⣸⡹⣌⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⡟⢔⢕⡇⣏⢿⡿⡏⢀⡉⡉⠈⢳⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠠⣟⠌⡜⢜⣿⡳⣏⢊⠘⠪⡿⡕⠀⠈⢷⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢼⣷⡱⡨⡪⡮⡮⣎⠆⡐⡐⠄⠂⠀⣀⣟⡧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⣿⣿⣾⣮⣯⣞⡇⣆⣪⣬⣼⢷⣿⡽⣯⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣟⡿⠾⠟⠿⠻⠿⠿⠿⠿⠿⠞⠯⠿⠷⠟⢿⡅⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⢿⣽⡇⠂⢄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡀⠄⢐⣿⣤⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⣠⣾⣽⣿⢿⢅⠁⠄⠑⢄⠄⠀⠀⠄⠀⠐⠈⢀⠀⡀⠠⣿⣯⣷⣤⡀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⢀⣴⣾⣿⣿⣿⣾⢫⢂⠂⡐⠠⠀⠑⢔⢀⠀⠂⠁⡈⢀⠠⠀⠀⢳⣿⣻⣿⣿⣦⣄⠀⠀⠀⠀
⠀⠀⠀⠀⣴⣿⣿⢿⡿⣟⣿⢯⣪⡢⡁⠄⠂⢈⠀⠄⠱⣔⡈⡀⠄⠠⠀⠠⣀⡨⣾⢿⣻⢿⢿⡿⣷⡀⠀⠀
⠀⠀⠀⠨⣿⣷⡿⣿⣻⣿⣾⢿⣽⢯⡿⣶⡡⠀⡐⠨⢈⠠⠑⠦⣐⠰⣵⣟⢗⢽⢝⡽⣝⣟⣯⣿⣯⡧⠀⠀
⠀⠀⠀⠈⢻⢾⣿⣿⢿⣿⣟⣿⣟⣿⣟⡿⡪⠐⡀⠡⢀⠐⠠⠁⠕⢝⢿⣽⡿⣮⡷⣽⢵⣻⣽⡷⣟⠊⠀⠀
⠀⠀⠀⠀⠀⠙⠿⣾⡿⣷⣟⣷⣟⡷⣟⣷⡂⠡⠀⡂⠄⡈⠄⠡⢡⢱⣟⡷⣝⣵⢽⣺⣽⢷⡻⠝⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⡐⣿⣿⣯⢳⢹⢛⢛⠓⠅⠅⢂⠐⡀⡂⠌⠌⡂⠕⡛⠟⢛⢺⣿⢿⠕⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⢐⠵⣿⣯⠪⡊⢆⢂⢌⢄⢅⣐⢐⢠⢂⢡⢐⡀⡂⡄⡂⡀⠈⣾⡻⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⢭⢻⣯⢣⠱⡡⡃⣊⠢⡑⢄⢃⢑⠌⡐⡐⢈⠠⠐⠀⠀⠂⡸⠀⢀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠈⢎⢽⢪⢊⢆⢪⢐⡑⢌⠢⡂⢅⢂⠢⠐⠠⠀⠂⡀⢁⠀⢂⢈⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠁⠣⢣⡪⡢⡣⡪⡪⢌⢆⢕⢢⡑⡅⡅⡅⡅⡔⡠⠂⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⡀⢀⠀⡀⡀⡀⡀⣄⣦⡶⣟⣿⣻⣾⣾⡇⡃⠍⡊⡊⣾⢾⣷⣻⣞⠎⣔⢄⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⢁⠐⠠⢁⢂⢂⠂⢗⢽⢿⢍⢇⢭⣻⢯⡗⡍⡦⣣⢲⢜⣎⢣⡻⡯⡮⡨⢸⢷⢱⠀⡀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠈⠀⠂⠈⠈⠄⠁⠌⠈⠨⠈⠐⠈⠈⠐⠀⠡⠈⠨⠈⠈⠐⠐⠀⠂⠀⠁⠈⠀⠀⠀⠀
This installer does all the steps described in the ISPmail tutorial from
https://workaround.org to turn your bare Debian 13/Trixie installation into a
mail server.
It will install software packages and alter your system configuration. Do not
run it on a server that is currently used in production. This installer is
meant to help you set up multiple servers without going through the ISPmail
guide time and again. It will not read the ISPmail guide for you.
Enter to continue. CTRL-C to abort.
EOF
}
############# Packages ###############
apt_install() {
DEBIAN_FRONTEND=noninteractive apt-get install -qq \
postfix-mysql dovecot-mysql \
dovecot-imapd dovecot-lmtpd dovecot-managesieved \
apache2 python3-certbot-apache libapache2-mod-php \
php-intl php-mbstring php-xml unzip certbot \
roundcube-mysql roundcube roundcube-plugins swaks libnet-ssleay-perl \
mutt unattended-upgrades mariadb-server \
rspamd redis-server opendkim-tools bind9-dnsutils pwgen curl
}
############# MariaDB ###############
init_mariadb_schema() {
mariadb <<'EOT'
create database if not exists mailserver;
grant all privileges on mailserver.* to 'mailadmin'@'localhost' identified by 'MAILADMIN-PASSWORD-HERE';
grant select on mailserver.* to 'mailserver'@'127.0.0.1' identified by 'MAILSERVER-PASSWORD-HERE';
USE mailserver;
CREATE TABLE IF NOT EXISTS `virtual_domains` (
`id` int(11) NOT NULL auto_increment,
`name` varchar(50) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY (`name`)
);
CREATE TABLE IF NOT EXISTS `virtual_aliases` (
`id` int(11) NOT NULL auto_increment,
`domain_id` int(11) NOT NULL,
`source` varchar(100) NOT NULL,
`destination` varchar(100) NOT NULL,
PRIMARY KEY (`id`),
FOREIGN KEY (domain_id) REFERENCES virtual_domains(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS `virtual_users` (
`id` int(11) NOT NULL auto_increment,
`domain_id` int(11) NOT NULL,
`email` varchar(100) NOT NULL,
`password` varchar(150) NOT NULL,
`quota` bigint(11) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `email` (`email`),
FOREIGN KEY (domain_id) REFERENCES virtual_domains(id) ON DELETE CASCADE
);
REPLACE INTO virtual_domains
(id, name)
VALUES (10,'example.org');
REPLACE INTO virtual_users
(id, domain_id, password, email)
VALUES (
1,
10,
'{BLF-CRYPT}$2y$05$.WedBCNZiwxY1CG3aleIleu6lYjup2CIg0BP4M4YCZsO204Czz07W',
'john@example.org');
REPLACE INTO virtual_aliases
(id, domain_id, source, destination)
VALUES (
1,
10,
'jack@example.org',
'john@example.org');
EOT
echo "✅ Database ready. Schema is set up."
}
############# Certbot ###############
certbot_config() {
# Certbot renewal
cat > /etc/letsencrypt/cli.ini << 'EOF'
# Restart services after renewing a certificate
post-hook = systemctl reload postfix dovecot apache2
# Because we are using logrotate for greater flexibility, disable the
# internal certbot logrotation.
max-log-backups = 0
# Adjust interactive output regarding automated renewal
preconfigured-renewal = True
EOF
}
############# Postfix ###############
postfix_config() {
cat > /etc/postfix/mariadb-virtual-mailbox-domains.cf << EOF
user = mailserver
password = MAILSERVER-PASSWORD-HERE
hosts = 127.0.0.1
dbname = mailserver
query = SELECT 1 FROM virtual_domains WHERE name='%s'
EOF
cat > /etc/postfix/mariadb-virtual-alias-maps.cf << EOF
user = mailserver
password = MAILSERVER-PASSWORD-HERE
hosts = 127.0.0.1
dbname = mailserver
query = SELECT destination FROM virtual_aliases WHERE source='%s'
EOF
cat > /etc/postfix/mariadb-virtual-mailbox-maps.cf << EOF
user = mailserver
password = MAILSERVER-PASSWORD-HERE
hosts = 127.0.0.1
dbname = mailserver
query = SELECT 1 FROM virtual_users WHERE email='%s'
EOF
postconf virtual_mailbox_domains=mysql:/etc/postfix/mariadb-virtual-mailbox-domains.cf
postconf virtual_mailbox_maps=mysql:/etc/postfix/mariadb-virtual-mailbox-maps.cf
postconf virtual_alias_maps=mysql:/etc/postfix/mariadb-virtual-alias-maps.cf
chown root:postfix /etc/postfix/mariadb-*.cf
chmod o= /etc/postfix/mariadb-*.cf
# Postfix check
RES=$(postmap -q example.org mysql:/etc/postfix/mariadb-virtual-mailbox-domains.cf)
if [[ $RES -ne 1 ]]; then
echo "❌ The mariadb-virtual-mailbox-domains.cf mapping failed."
exit 10
fi
RES=$(postmap -q jack@example.org mysql:/etc/postfix/mariadb-virtual-alias-maps.cf)
if [[ $RES != 'john@example.org' ]]; then
echo "❌ The mariadb-virtual-alias-maps.cf mapping failed."
exit 10
fi
RES=$(postmap -q john@example.org mysql:/etc/postfix/mariadb-virtual-mailbox-maps.cf)
if [[ $RES -ne 1 ]]; then
echo "❌ The mariadb-virtual-mailbox-maps.cf mapping failed."
exit 10
fi
echo "✅ Mappings work."
}
############# Dovecot ###############
dovecot_config() {
groupadd -f --system vmail
id -a vmail 2>/dev/null >/dev/null || useradd --system --gid vmail vmail
mkdir -p /var/vmail
chown -R vmail:vmail /var/vmail
chmod u=rwx,g=rx,o= /var/vmail
sed -i '/!include/s/^/#/' /etc/dovecot/conf.d/10-auth.conf
cat > /etc/dovecot/conf.d/99-ispmail-mail.conf << EOF
mail_driver = maildir
mail_home = /var/vmail/%{user | domain}/%{user | username}
mail_path = ~/Maildir
mail_uid = vmail
mail_gid = vmail
mail_inbox_path = ~/Maildir/
EOF
cat > /etc/dovecot/conf.d/99-ispmail-master.conf << EOF
service auth {
unix_listener /var/spool/postfix/private/dovecot-auth {
mode = 0660
user = postfix
group = postfix
}
}
EOF
cat > /etc/dovecot/conf.d/99-ispmail-ssl.conf << EOF
ssl = required
ssl_server_cert_file = /etc/letsencrypt/live/$FQDN/fullchain.pem
ssl_server_key_file = /etc/letsencrypt/live/$FQDN/privkey.pem
EOF
cat > /etc/dovecot/conf.d/99-ispmail-lmtp-username-format.conf << EOF
protocol lmtp {
auth_username_format =
}
EOF
cat > /etc/dovecot/conf.d/99-ispmail-sql.conf << EOF
sql_driver = mysql
mysql /var/run/mysqld/mysqld.sock {
user = mailserver
password = 'MAILSERVER-PASSWORD-HERE'
dbname = mailserver
host = 127.0.0.1
}
userdb sql {
query = SELECT email as user FROM virtual_users WHERE email='%{user}'
iterate_query = SELECT email as user FROM virtual_users
}
passdb sql {
query = SELECT password FROM virtual_users where email='%{user}'
}
EOF
# fix permissions
chown root:root /etc/dovecot/conf.d/99-ispmail-sql.conf
chmod go= /etc/dovecot/conf.d/99-ispmail-sql.conf
cat > /etc/dovecot/conf.d/99-ispmail-managesieve.conf << EOF
service managesieve-login {
# Listen only on localhost
inet_listener sieve {
listen= 127.0.0.1
port = 4190
}
# Disable the deprecated listener
inet_listener sieve_deprecated {
port = 0
}
}
EOF
systemctl restart dovecot
# Check if running
systemctl is-active --quiet dovecot.service
if [[ $? -ne 0 ]]; then
echo "❌ Dovecot failed to start properly."
exit 10
fi
echo "✅ Dovecot is running fine."
# Check query
JOHNS_MAILDIR=$(doveadm user john@example.org | grep ^mail_path | cut -f2)
if [[ $JOHNS_MAILDIR != '/var/vmail/example.org/john/Maildir' ]]; then
echo "❌ Could not find John's mail_path."
exit 10
fi
echo "✅ Lookup works."
}
############# LMTP ###############
lmtp_config() {
# Add an LMTP listening socket in Dovecot
cat > /etc/dovecot/conf.d/99-ispmail-lmtp-listener.conf << EOF
service lmtp {
# Used internally by Dovecot
unix_listener lmtp {
}
# Listen to LMTP connections from Postfix
unix_listener /var/spool/postfix/private/dovecot-lmtp {
mode = 0600
user = postfix
group = postfix
}
}
EOF
# Restart Dovecot
systemctl restart dovecot
postconf virtual_transport=lmtp:unix:private/dovecot-lmtp
# Testing delivery
echo Testing email delivery internally.
if [[ -d /var/vmail/example.org/john/Maildir/new/ ]]; then
rm -f /var/vmail/example.org/john/Maildir/new/*
fi
swaks --server localhost --to john@example.org --tls --silent
DELIVERY=0
for i in $(seq 1 50); do
NUM_FILES=$(ls -1 /var/vmail/example.org/john/Maildir/new/ | wc -l)
if [[ $NUM_FILES -eq 1 ]]; then
DELIVERY=1
break
fi
sleep 0.2
done
if [[ $DELIVERY -eq 0 ]]; then
echo "❌ Mail delivery to john@example.org failed."
exit 10
fi
echo "✅ Test successful."
}
############ APACHE #############
apache_config() {
cat > /etc/apache2/sites-available/000-default-le-ssl.conf << EOF
<VirtualHost *:443>
ServerAdmin postmaster@$FQDN
ServerName $FQDN
DocumentRoot /var/lib/roundcube/public_html
ErrorLog \${APACHE_LOG_DIR}/error.log
CustomLog \${APACHE_LOG_DIR}/access.log combined
SSLCertificateFile /etc/letsencrypt/live/$FQDN/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/$FQDN/privkey.pem
Include /etc/letsencrypt/options-ssl-apache.conf
Include /etc/roundcube/apache.conf
<Location /rspamd>
Require all granted
</Location>
RewriteEngine On
RewriteRule ^/rspamd$ /rspamd/ [R,L]
RewriteRule ^/rspamd/(.*) http://localhost:11334/\$1 [P,L]
</VirtualHost>
EOF
# Enable the required Apache modules
a2enmod proxy_http -q
a2enmod rewrite -q
# Restart Apache
systemctl restart apache2
# Check if running
systemctl is-active --quiet apache2.service
if [[ $? -ne 0 ]]; then
echo "❌ Apache has not started properly."
exit 10
fi
echo "✅ Apache is running fine."
# Set imap_host and smtp_host in /etc/roundcube/config.inc.php
sed -i "s|^\s*\$config\['imap_host'\]\s*=.*|\$config['imap_host'] = 'tls://$FQDN:143';|" /etc/roundcube/config.inc.php
sed -i "s|^\s*\$config\['smtp_host'\]\s*=.*|\$config['smtp_host'] = 'tls://$FQDN:587';|" /etc/roundcube/config.inc.php
# Check if web interface is loading
curl -s https://$FQDN | grep title | grep -q "Roundcube Webmail"
if [[ $? -ne 0 ]]; then
echo "❌ Webmail interface is not reachable at https://$FQDN"
exit 10
fi
}
############ Relaying #############
relaying_setup() {
postconf -M submission/inet="submission inet n - y - - smtpd"
postconf -P "submission/inet/syslog_name=postfix/submission"
postconf -P "submission/inet/smtpd_tls_security_level=encrypt"
postconf -P "submission/inet/smtpd_sasl_auth_enable=yes"
postconf -P "submission/inet/smtpd_sasl_type=dovecot"
postconf -P "submission/inet/smtpd_sasl_path=private/dovecot-auth"
postconf -P "submission/inet/smtpd_recipient_restrictions=permit_sasl_authenticated,reject"
postconf -P "submission/inet/smtpd_sender_restrictions=reject_sender_login_mismatch,permit_sasl_authenticated,reject"
postconf -M submissions/inet="submissions inet n - y - - smtpd"
postconf -P "submissions/inet/syslog_name=postfix/submissions"
postconf -P "submissions/inet/smtpd_tls_wrappermode=yes"
postconf -P "submissions/inet/smtpd_sasl_auth_enable=yes"
postconf -P "submissions/inet/smtpd_sasl_type=dovecot"
postconf -P "submissions/inet/smtpd_sasl_path=private/dovecot-auth"
postconf -P "submissions/inet/smtpd_recipient_restrictions=permit_sasl_authenticated,reject"
postconf -P "submissions/inet/smtpd_sender_restrictions=reject_sender_login_mismatch,permit_sasl_authenticated,reject"
postfix reload 2>/dev/null
postconf smtp_tls_security_level=encrypt
postconf smtpd_tls_security_level=encrypt
postconf smtp_tls_mandatory_protocols=">=TLSv1.2"
postconf smtpd_tls_mandatory_protocols=">=TLSv1.2"
postconf smtp_tls_mandatory_ciphers=high
postconf smtpd_tls_mandatory_ciphers=high
postconf smtpd_tls_cert_file=/etc/letsencrypt/live/$FQDN/fullchain.pem
postconf smtpd_tls_key_file=/etc/letsencrypt/live/$FQDN/privkey.pem
# Create a mapping configuration from the user to themself
cat > /etc/postfix/mariadb-email2email.cf << EOF
user = mailserver
password = MAILSERVER-PASSWORD-HERE
hosts = 127.0.0.1
dbname = mailserver
query = SELECT email FROM virtual_users WHERE email='%s'
EOF
# Fix permissions
chown root:postfix /etc/postfix/mariadb-email2email.cf
chmod u=rw,g=r,o= /etc/postfix/mariadb-email2email.cf
# Tell Postfix to use this mapping
postconf smtpd_sender_login_maps=mysql:/etc/postfix/mariadb-email2email.cf
# Test spoofing
set +e
swaks --server localhost:587 \
--from brunhilde@example.org \
--to list@example.com \
--tls \
--auth-user john@example.org \
--auth-password summersun \
--silent 3
if [[ $? -ne 24 ]]; then
echo "❌ Forged sender spoofing test failed."
exit 10
fi
set -e
echo "✅ Forged sender spoofing successfully rejected."
# Test with proper email
set +e
swaks --server localhost:587 \
--from john@example.org \
--to list@example.com \
--tls \
--auth-user john@example.org \
--auth-password summersun \
--silent 3
if [[ $? -ne 0 ]]; then
echo "❌ Relaying of proper email failed."
exit 10
fi
set -e
echo "✅ Relay test okay."
# Submission plaintext test
set +e
swaks --server localhost:587 \
--from john@example.org \
--to lisa@example.com \
--silent 3
if [[ $? -ne 23 ]]; then
echo "❌ Relaying over submission without STARTTLS was accepted."
exit 10
fi
set -e
echo "✅ Plaintext auth refused via submission port."
# Submission encryption but no auth test
set +e
swaks --server localhost:587 \
--from john@example.org \
--to lisa@example.com \
-tls \
--silent 3
if [[ $? -ne 24 ]]; then
echo "❌ Relaying over submission with STARTTLS but without auth was accepted."
exit 10
fi
set -e
echo "✅ Encrypted non-auth refused via submission port."
# Test encryption and auth over submission
set +e
swaks --server localhost:587 \
--from john@example.org \
--to lisa@example.com \
-tls \
--auth-user john@example.org \
--auth-password summersun \
--silent 3
if [[ $? -ne 0 ]]; then
echo "❌ Relaying over submission with STARTTLS and auth failed."
exit 10
fi
set -e
echo "✅ Encrypted and authenticated relaying over submission works."
# Test encryption and auth over submissions
set +e
swaks --server localhost:465 \
--from john@example.org \
--to lisa@example.com \
--tls-on-connect \
--auth-user john@example.org \
--auth-password summersun \
--silent 3
if [[ $? -ne 0 ]]; then
echo "❌ Relaying over submissions with STARTTLS and auth failed."
exit 10
fi
set -e
echo "✅ Encrypted and authenticated relaying over submissions works."
}
############ IMAP ##############
imap_test() {
# Test IMAP access
apt-get -qq install fetchmail
cat > ~/.fetchmailrc <<EOF
poll "$FQDN" proto IMAP
user "john@example.org" there with password "summersun"
EOF
set +e
fetchmail --check -s 2>/dev/null
rm ~/.fetchmailrc
if [[ $? -ne 0 ]]; then
echo "❌ IMAP connection failed"
exit 10
fi
set -e
echo "✅ IMAP connection test successful."
}
############ rspamd #############
rspamd_config() {
postconf smtpd_milters=inet:127.0.0.1:11332
postconf non_smtpd_milters=inet:127.0.0.1:11332
postconf inet_protocols=all
systemctl restart postfix
cat > /tmp/gtube.txt <<EOF
This is the GTUBE, the
Generic
Test for
Unsolicited
Bulk
Email
If your spam filter supports it, the GTUBE provides a test by which you
can verify that the filter is installed correctly and is detecting incoming
spam. You can send yourself a test mail containing the following string of
characters (in upper case and with no white spaces and line breaks):
XJS*C4JDBQADN1.NSBN3*2IDNEN*GTUBE-STANDARD-ANTI-UBE-TEST-EMAIL*C.34X
EOF
set +e
swaks --server $FQDN:25 \
--from lisa@example.com \
--to john@example.org \
-tls \
--body @/tmp/gtube.txt \
--silent 3
if [[ $? -ne 26 ]]; then
echo "❌ GTUBE spam test pattern was not rejected"
exit 10
fi
set -e
echo "✅ GTUBE spam test was rejected successfully."
# Add learning scripts
cat > /etc/dovecot/conf.d/99-ispmail-sieve-movetojunk.conf << 'EOF'
sieve_script spam-to-junk-folder {
driver = file
type = after
path = /etc/dovecot/sieve/spam-to-junk-folder.sieve
}
# Enable the execution of Sieve rules when Postfix sends an email to Dovecot over LMTP
protocol lmtp {
mail_plugins {
sieve = yes
}
}
# Make sure that every user has a Junk folder and is subscribed to it
namespace inbox {
mailbox Junk {
special_use = \Junk
auto = subscribe
}
}
EOF
# Restart Dovecot
systemctl reload dovecot
# Create the directory for Sieve files
mkdir -p /etc/dovecot/sieve
# Create the Sieve script to move Spam mails to the user's Junk folder
cat > /etc/dovecot/sieve/spam-to-junk-folder.sieve << 'EOF'
require ["fileinto"];
if header :contains "X-Spam" "Yes" {
fileinto "Junk";
stop;
}
EOF
# Make the sieve script machine-readable
sievec /etc/dovecot/sieve/spam-to-junk-folder.sieve 2>/dev/null
# Create a config file to enable automatic spam training
cat > /etc/rspamd/local.d/classifier-bayes.conf << 'EOF'
# Store training data in the Redis database
servers = "127.0.0.1:6379";
backend = "redis";
# Enable automatic training
autolearn = true; # if rspamd is sure that an email is spam, it will be learned
min_learns = 200; # do not trust the data before at least 200 mails have been learned
EOF
# Restart rspamd
#systemctl restart rspamd
cat > /etc/dovecot/conf.d/99-ispmail-imapsieve.conf << 'EOF'
# Enable the imap_sieve plugin
protocol imap {
mail_plugins {
imap_sieve = yes
}
}
# Allow the use of the pipe plugin to send mails to shell scripts
sieve_plugins {
sieve_extprograms = yes
sieve_imapsieve = yes
}
sieve_global_extensions {
vnd.dovecot.pipe = yes
}
# Where to look for Sieve scripts that use the Pipe functionality
sieve_pipe_bin_dir = /etc/dovecot/sieve
# Moved into Junk? -> Learn as spam.
mailbox Junk {
sieve_script spam {
type = before
cause = copy
path = /etc/dovecot/sieve/learn-spam.sieve
}
}
# Moved out of Junk? -> Learn as ham.
imapsieve_from Junk {
sieve_script ham {
type = before
cause = copy
path = /etc/dovecot/sieve/learn-ham.sieve
}
}
EOF
# Create spam learning script
cat > /etc/dovecot/sieve/learn-spam.sieve << 'EOF'
require ["vnd.dovecot.pipe", "copy", "imapsieve"];
pipe :copy "rspamd-learn-spam.sh";
EOF
# Create ham learning script
cat > /etc/dovecot/sieve/learn-ham.sieve << 'EOF'
require ["vnd.dovecot.pipe", "copy", "imapsieve", "variables"];
pipe :copy "rspamd-learn-ham.sh";
EOF
# Compile the Sieve scripts
systemctl reload dovecot
sievec /etc/dovecot/sieve/learn-spam.sieve 2>/dev/null
sievec /etc/dovecot/sieve/learn-ham.sieve 2>/dev/null
# Fix permissions
chmod u=rw,go= /etc/dovecot/sieve/learn-{spam,ham}.{sieve,svbin}
chown vmail:vmail /etc/dovecot/sieve/learn-{spam,ham}.{sieve,svbin}
# Create the shell script for learning spam
cat > /etc/dovecot/sieve/rspamd-learn-spam.sh << 'EOF'
#!/bin/sh
# Receives an email from Dovecot's Sieve script and pipe it into rspamc
exec /usr/bin/rspamc learn_spam
EOF
# Create the shell script for learning ham
cat > /etc/dovecot/sieve/rspamd-learn-ham.sh << 'EOF'
#!/bin/sh
# Receives an email from Dovecot's Sieve script and pipe it into rspamc
exec /usr/bin/rspamc learn_ham
EOF
# Fix permissions of the shell scripts
chmod u=rwx,go= /etc/dovecot/sieve/rspamd-learn-{spam,ham}.sh
chown vmail:vmail /etc/dovecot/sieve/rspamd-learn-{spam,ham}.sh
cat > /etc/dovecot/conf.d/99-ispmail-sieve-debug.conf << 'EOF'
# Enable detailed logging of Sieve scripts
log_debug=category=sieve
EOF
cat > /etc/dovecot/conf.d/99-ispmail-autoexpunge.conf << 'EOF'
# Remove mails from the Junk and Trash folders after 30 days
mailbox Junk {
special_use = \Junk
auto = subscribe
mailbox_autoexpunge = 30d
}
mailbox Trash {
special_use = \Trash
auto = subscribe
mailbox_autoexpunge = 30d
}
# Make expunging more efficient
mailbox_list_index = yes
mail_always_cache_fields = date.save
EOF
# Restart Dovecot
systemctl restart dovecot
# Generate a random password that is 15 characters long
RSPAMD_PW=$(pwgen 15 1)
# Create a hash of that password
HASH=$(rspamadm pw -p $RSPAMD_PW)
# Add the hashed password to rspamd's configuration
echo "password = \"$HASH\"" > /etc/rspamd/local.d/worker-controller.inc
# Restart rspamd
systemctl reload rspamd
echo "✅ rspamd set up"
}
############ Show summary at the end #############
dkim_config() {
postconf smtpd_milters=inet:127.0.0.1:11332
postconf non_smtpd_milters=inet:127.0.0.1:11332
# Create the directory
mkdir -p /var/lib/rspamd/dkim
# Fix permissions
chown _rspamd:_rspamd /var/lib/rspamd/dkim
# Tell rspamd to look up selectors in our mapping file
cat > /etc/rspamd/local.d/dkim_signing.conf << 'EOF'
path = "/var/lib/rspamd/dkim/$domain.$selector.key";
selector_map = "/etc/rspamd/dkim_selectors.map";
EOF
# TODO: create a key for $FQDN and test it
# Restart rspamd
systemctl restart rspamd
echo "✅ DKIM signing ready to use"
}
############ Catch-all aliases #############
catchall_config() {
# Create the john-to-himself mapping
cat > /etc/postfix/mariadb-email2email.cf << EOF
user = mailserver
password = MAILSERVER-PASSWORD-HERE
hosts = 127.0.0.1
dbname = mailserver
query = SELECT email FROM virtual_users WHERE email='%s'
EOF
# Fix the permissions of that file
chgrp postfix /etc/postfix/mariadb-*.cf
chmod u=rw,g=r,o= /etc/postfix/mariadb-*.cf
# Add the new mapping to the virtual_alias_maps
# (The order is not important. Postfix will check all mapping files.)
postconf virtual_alias_maps=mysql:/etc/postfix/mariadb-virtual-alias-maps.cf,mysql:/etc/postfix/mariadb-email2email.cf
result=$(postmap -q john@example.org mysql:/etc/postfix/mariadb-email2email.cf)
if [[ $result != "john@example.org" ]]; then
echo "❌ email2email mapping for catch-all aliases do not work properly"
exit 10
fi
set -e
echo "✅ Catch-all aliases ready to use."
}
############ Going-live #############
going_live() {
# Create two random passwords
PW_MAILADMIN=$(pwgen -s 32 1)
PW_MAILSERVER=$(pwgen -s 32 1)
# Replace the dummy passwords
sed -i "s|MAILADMIN-PASSWORD-HERE|$PW_MAILADMIN|g" /etc/roundcube/plugins/password/config.inc.php
sed -i "s|MAILSERVER-PASSWORD-HERE|$PW_MAILSERVER|g" \
/etc/dovecot/conf.d/99-ispmail-sql.conf \
/etc/postfix/mariadb-virtual-mailbox-maps.cf \
/etc/postfix/mariadb-virtual-mailbox-domains.cf \
/etc/postfix/mariadb-virtual-alias-maps.cf \
/etc/postfix/mariadb-email2email.cf
# Restart the services
systemctl restart postfix dovecot
# Update MariaDB user passwords
mariadb <<EOF
ALTER USER 'mailadmin'@'localhost' IDENTIFIED BY '${PW_MAILADMIN}';
ALTER USER 'mailserver'@'127.0.0.1' IDENTIFIED BY '${PW_MAILSERVER}';
FLUSH PRIVILEGES;
EOF
echo "✅ Dummy passwords changed."
# TODO: remove example.org
}
############ Show summary at the end #############
show_passwords() {
echo "============================================================="
echo " PLEASE NOTE:"
echo "============================================================="
echo
echo "Web mail URL: https://$FQDN"
echo
echo "rspamd web interface URL: https://$FQDN/rspamd"
echo "rspamd web interface password: $RSPAMD_PW"
echo "Database passwords:"
echo "mailadmin password: $PW_MAILADMIN"
echo "mailserver password: $PW_MAILSERVER"
echo
echo "============================================================="
echo "Your mail server is now ready to be used. Now it is time to"
echo "add your domains, users and aliases to the database."
echo "Set up DNS records and DKIM keys."
echo "Details are found in the ISPmail guide at https://workaround.org"
echo
echo "Please report bugs at https://github.com/Signum/ispmail-workaround-org/issues"
}
############ Banner helper #############
banner() {
echo ---------------------------------------------
echo --- $1
echo ---------------------------------------------
}
############ MAIN #############
pre_flight
intro
read
banner "Installing packages"
apt_install
banner "Preparing database schema"
init_mariadb_schema
if [[ ! -d "/etc/letsencrypt/live/$FQDN" ]]; then
banner "Getting certificate from Let's Encrypt"
certbot --apache --register-unsafely-without-email --agree-tos -d $FQDN
fi
certbot_config
banner "Configuring Postfix"
postfix_config
banner "Configuring Dovecot"
dovecot_config
banner "Testing IMAP"
imap_test
banner "Configuring LMTP"
lmtp_config
banner "Configuring Apache and Roundcube"
apache_config
banner "Configuring relaying"
relaying_setup
banner "Configuring rspamd"
rspamd_config
banner "Preparing DKIM signing"
dkim_config
banner "Setting up catch-all aliases"
catchall_config
banner "Final steps changing dummy passwords"
going_live
show_passwords

View file

@ -12,17 +12,18 @@ Europaring 90
Germany Germany
``` ```
Contact: postmaster@christoph-haas.de or christoph.haas@gmail.com or via Email: online-impreessum@christoph-haas.de
[contact form](https://mein.online-impressum.de/workaround-org/)
Or via [contact form](https://mein.online-impressum.de/workaround-org/#Zweiter_Kontaktweg)
## Liability for Content ## Liability for Content
As a service provider, I am responsible for my own content on these pages under German law § 7 (1) Telemediengesetz. As a service provider, we are responsible for our own content on these pages under § 7 (1) TMG. However, we do not
However, I do not guarantee the accuracy, completeness, or timeliness of the content. guarantee the accuracy, completeness, or timeliness of the content.
## Liability for External Links ## Liability for External Links
This website contains links to external third-party websites over which I have no control. Therefore, I cannot assume This website contains links to external third-party websites over which we have no control. Therefore, we cannot assume
any liability for such external content. any liability for such external content.
## Open-Source Notice ## Open-Source Notice

View file

@ -23,8 +23,7 @@ server.
limit is the size of your disk. limit is the size of your disk.
- Have a **webmail** interface so users can access their emails securely from any location using a web browser. - Have a **webmail** interface so users can access their emails securely from any location using a web browser.
- Let your users fetch email using **IMAP** and send email through your servers using **SMTP**. - Let your users fetch email using **IMAP** and send email through your servers using **SMTP**.
- Add automatic cryptographic signatures using **DKIM** to outgoing emails to prove that you are the owner of your - Add automatic cryptographic signatures using DKIM to outgoing emails to prove that you are the owner of your domain.
domain.
- Allow users to manage server-based **filter rules**. Distribute incoming emails to different folders. Forward copies. - Allow users to manage server-based **filter rules**. Distribute incoming emails to different folders. Forward copies.
Or send out-of-office notifications. Or send out-of-office notifications.
- Mitigate **brute force** attacks. - Mitigate **brute force** attacks.
@ -52,10 +51,9 @@ server.
- An **internet domain** (or several) to receive emails for. You need to be able to set A, MX and TXT records for that - An **internet domain** (or several) to receive emails for. You need to be able to set A, MX and TXT records for that
domain. You should also be able to set PTR records for your IP address because some mail servers on the internet domain. You should also be able to set PTR records for your IP address because some mail servers on the internet
require you to have matching forward and reverse DNS records. require you to have matching forward and reverse DNS records.
- **Patience**. We will proceed slowly and after every step ensure that you are still on track. Dont hurry and skip - **Patience**. We will proceed slowly and after every step ensure that are still on track. Dont hurry and skip parts
parts even if they appear confusing at first. If you get lost just submit your question at the bottom of any page even if they appear confusing at first. If you get lost just submit your question at the bottom of any page throughout
throughout this guide and help is on the way. Or join the this guide and help is on the way. Or join the [chat channel](https://riot.im/app/#/room/#ispmail:matrix.org).
[chat channel](https://riot.im/app/#/room/#ispmail:matrix.org).
## What this is not about ## What this is not about

View file

@ -46,7 +46,7 @@ CREATE DATABASE mailserver;
## Create the database users ## Create the database users
In this section you will create the basic database called `mailserver` and two users: In this section you will create the basic database `mailserver` and two users:
| User | Permissions | Purpose | | User | Permissions | Purpose |
| :--------- | :---------- | :--------------------------------------------------------- | | :--------- | :---------- | :--------------------------------------------------------- |

View file

@ -113,8 +113,8 @@ to authenticate your users which is relevant later when we set up _relaying_.
Earlier in this guide you created both a key and a certificate file to encrypt the communication with IMAPS and HTTPS Earlier in this guide you created both a key and a certificate file to encrypt the communication with IMAPS and HTTPS
between the users and your mail server. You need to tell Dovecot where to find these files. Also set between the users and your mail server. You need to tell Dovecot where to find these files. Also set
[ssl=required](https://doc.dovecot.org/2.4.1/core/config/ssl.html#how-to-specify-when-ssl-tls-is-required) to prevent [ssl=required](https://doc.dovecot.org/2.4.1/core/config/ssl.html#how-to-specify-when-ssl-tls-is-required) to prevent
that someone sends their password without encryption. Again we will create a new file `99-ispmail-ssl.conf` to override that someone sends their password without encryption. Again we will create a new file `99-ispmail-ssl.conf` to
the defaults. override the defaults.
<Aside type="danger" title="Important"> <Aside type="danger" title="Important">
@ -178,32 +178,6 @@ Dovecot can also read the path to a user's home directory and the user-ID and gr
a fixed schema for the home directory (`/var/vmail/DOMAIN/USER`) (as defined by `mail_home`) and the user and group are a fixed schema for the home directory (`/var/vmail/DOMAIN/USER`) (as defined by `mail_home`) and the user and group are
always `vmail` and `vmail`. always `vmail` and `vmail`.
### 99-ispmail-managesieve.conf
Later, we will implement server-side automation using **Sieve** rules. By default, Dovecot exposes two TCP ports to the
internet: 2000 and 4190. Port 2000 is deprecated anyway and can be safely disabled, while port 4190 is the one we will
use. However, unless you have a compelling reason, it is recommended not to make this port publicly accessible. So let
us restrict it to the _localhost_ interface:
```sh title="Run this on your server"
cat > /etc/dovecot/conf.d/99-ispmail-managesieve.conf << EOF
service managesieve-login {
# Listen only on localhost
inet_listener sieve {
listen= 127.0.0.1
port = 4190
}
# Disable the deprecated listener
inet_listener sieve_deprecated {
port = 0
}
}
EOF
```
_(Thanks to Thomas Gauweiler for spotting that.)_
## Restart and test ## Restart and test
Restart Dovecot from the shell: Restart Dovecot from the shell:

View file

@ -219,7 +219,7 @@ EOF
chown root:postfix /etc/postfix/mariadb-email2email.cf chown root:postfix /etc/postfix/mariadb-email2email.cf
chmod u=rw,g=r,o= /etc/postfix/mariadb-email2email.cf chmod u=rw,g=r,o= /etc/postfix/mariadb-email2email.cf
# Tell Postfix to use this mapping # Tell Postfix to use thi smapping
postconf smtpd_sender_login_maps=mysql:/etc/postfix/mariadb-email2email.cf postconf smtpd_sender_login_maps=mysql:/etc/postfix/mariadb-email2email.cf
``` ```

View file

@ -299,7 +299,7 @@ that file for your own domain and selector:
```sh title="Run this on your server" ```sh title="Run this on your server"
# Add the mapping of domain/selector (use your own domain here) # Add the mapping of domain/selector (use your own domain here)
echo example.org 2025100901 > /etc/rspamd/dkim_selectors.map echo example.org 20251000901 > /etc/rspamd/dkim_selectors.map
``` ```
Thats all. rspamd now knows that whenever it sees an outgoing email from `anyone@example.org` it will get the DKIM Thats all. rspamd now knows that whenever it sees an outgoing email from `anyone@example.org` it will get the DKIM

View file

@ -8,313 +8,8 @@ sidebar:
import { Aside } from "@astrojs/starlight/components"; import { Aside } from "@astrojs/starlight/components";
<Aside type="tip" title="Optional feature"> <Aside type="tip" title="Coming soon">
This feature is completely optional. If you are eager to get finished then skip this page and maybe come back later. This page is currently rewritten. It will be online in a couple of days.
</Aside> </Aside>
Quotas are size limits for users. You can make sure that users do not waste arbitrary amounts of disk space but are ![Under construction](images/under-construction.jpg)
forced to clean up old emails every now and then.
The magic happens in two places:
1. Postfix needs to reject new emails if the users mailbox is over quota.
2. Dovecot needs to keep track of the quota and how much the user has already used up of it.
---
https://doc.dovecot.org/2.4.2/core/plugins/quota.html#quota-service
https://sys4.de/en/blog/postfix-dovecot-mailbox-quota/
---
https://www.postfix.org/SMTPD_POLICY_README.html
telnet 127.0.0.1 13373 Trying 127.0.0.1... Connected to 127.0.0.1. Escape character is '^]'.
recipient=chris@auenland.workaround.org
action=554 5.2.2 Quota exceeded (mailbox for user is full)
---
```swaks --from nonexistens@example.org --to chris@auenland.workaround.org
=== Trying auenland.workaround.org:25...
=== Connected to auenland.workaround.org.
<- 220 auenland ESMTP Postfix (Debian)
-> EHLO minty
<- 250-auenland
<- 250-PIPELINING
<- 250-SIZE 10240000
<- 250-VRFY
<- 250-ETRN
<- 250-STARTTLS
<- 250-ENHANCEDSTATUSCODES
<- 250-8BITMIME
<- 250-DSN
<- 250-SMTPUTF8
<- 250 CHUNKING
-> MAIL FROM:<nonexistens@example.org>
<- 250 2.1.0 Ok
-> RCPT TO:<chris@auenland.workaround.org>
<** 554 5.2.2 <chris@auenland.workaround.org>: Recipient address rejected: Quota exceeded (mailbox for user is full)
-> QUIT
<- 221 2.0.0 Bye
=== Connection closed with remote host.
```
---
```
cat 99-ispmail-quota.conf
mail_plugins {
quota = yes
}
quota "User quota" {
storage_size = 100K
storage_grace = 0
warning warn-95 {
quota_storage_percentage = 95
execute quota-warning {
args = 95 %{user}
}
}
warning warn-80 {
quota_storage_percentage = 80
execute quota-warning {
args = 80 %{user}
}
}
}
service quota-status {
executable = quota-status -p postfix
inet_listener quota-status {
port = 13373
}
client_limit = 1
}
# Example quota-warning service. The unix listener's permissions should be
# set in a way that mail processes can connect to it. Below example assumes
# that mail processes run as vmail user. If you use mode=0666, all system users
# can generate quota warnings to anyone.
service quota-warning {
executable = script /usr/local/bin/ispmail-quota-warning.sh
user = dovecot
unix_listener quota-warning {
user = vmail
}
}
##
## Quota backends
##
# Multiple backends are supported:
# count: Default and recommended, quota driver tracks the quota internally within Dovecot's index files.
# maildir: Maildir++ quota
# fs: Read-only support for filesystem quota
#quota "User quota" {
# driver = count
#}
```
---
```
cat /usr/local/bin/ispmail-quota-warning.sh
#!/bin/sh
PERCENT=$1
USER=$2
cat << EOF | /usr/lib/dovecot/dovecot-lda -d $USER -o quota_enforce=no
From: postmaster@domain.com
Subject: quota warning
Your mailbox is now $PERCENT% full.
EOF
```
---
### Dovecot quota policy service
Lets start with Dovecot. Find the file `/etc/dovecot/conf.d/90-quota.conf` and edit it. There are several `plugin {}`
sections. Take one and make it look like:
```
plugin {
quota = count:User quota
quota_vsizes = yes
quota_status_success = DUNNO
quota_status_nouser = DUNNO
quota_status_overquota = "452 4.2.2 Mailbox is full and cannot receive any more emails"
}
```
The first line defines that you want to calculate the used space in a users _maildir_. There are several
[backends](https://doc.dovecot.org/configuration_manual/quota_plugin/) like that but the
_[count](https://doc.dovecot.org/configuration_manual/quota/quota_count/#quota-backend-count)_ is the best choice in
this context. (Previous guides used _maildir_ here.) The string “User quota” is just an arbitrary string that may be
queried from a mail user agent.
The lines starting with “`quota_status_…`” set return values for the service that you will set up in a minute. It will
tell Postfix that it will not interfere (_DUNNO_ colloquial way to say “I dont know”). And it will return a string
with a return code 452 if the user is over quota. Codes starting with “4” mean temporary errors. It will tell the
sending party that it is worth retrying at a later time. However if the user does not resolve the issue it will lead to
a _bounce_ error email after three days.
In the same file (_90-quota.conf_) add another section:
```
service quota-status {
executable = /usr/lib/dovecot/quota-status -p postfix
unix_listener /var/spool/postfix/private/quota-status {
user = postfix
}
}
```
This creates a new [Dovecot service](https://doc.dovecot.org/configuration_manual/service_configuration/) responding to
requests from other processes. You surely recognize that we put it into the jail that Postfix runs in
(_/var/spool/postfix_), so that Postfix can access it.
Time to restart Dovecot:
```
systemctl restart dovecot
```
Take a look at the /var/spool/postfix/private directory. If all went as intended you will find a socket file called
`quota-status` there. Otherwise please check the `/var/log/mail.log` file for errors.
### Postfix recipient restrictions
If we stopped here, then Dovecot would reject emails for users who have no space left. However Postfix would still
happily receive new emails and attempt to forward them to Dovecot via LMTP. Dovecot however will deny that. It will then
keep the email in its queue and retry for a while. In the end it will send a _bounce_ back to the sender telling them
about the problem. So why is this bad?
1. The sender will assume that the email was delivered while it is stuck in the queue for up to three days.
2. Spam emails use forged senders. So at the time that Postfix generates the _bounce email_ it will likely send it to an
innocent person. This is called _backscatter_ and considered a mail server misconfiguration. Such a problem may get
your mail server blacklisted. You dont want that.
So the next logical step is to make Postfix check whether a mailbox is over quota whenever a new email arrives. Lets
hook up into the “RCPT TO” phase of the SMTP dialog when a new email comes in. Postfix checks its
_smtpd_recipient_restrictions_ configuration at this stage. Run this command in the shell:
```
postconf smtpd_recipient_restrictions=reject_unauth_destination, \
"check_policy_service unix:private/quota-status"
```
This adds two checks:
1. `reject_unauth_destination` checks whether the mail server is the final destination for the recipients email
address. This is pretty much the default behavior if you do not define any restrictions.
2. `check_policy_service` connects to the socket file at `/var/spool/postfix/private/quota-status` that was put there by
Dovecot. It will use it to ask Dovecot whether the user is over quota in which case the email would get rejected.
### Test it
If you are curious to see this working, then set Johns mailbox quota to 5 KB:
```sql
# mariadb mailserver
mysql> update virtual_users set quota=4000 where email='john@example.org';
```
Send him a few emails using the swaks tool:
```
swaks --server localhost --to john@example.org
```
After a few emails you will see the rejection message:
```
-> RCPT TO:john@example.org
<** 452 4.2.2 john@example.org: Recipient address rejected: Mailbox is full and cannot receive any more emails
```
### Troubleshooting
These are things you should consider if quotas do not seem to work properly:
- Check if you have enabled “quota” in the “mail_plugins” in the 10-mail.conf file.
- Your users may complain that they have deleted many emails but are still over quota. Let them check if they actually
emptied the _Trash_ folder. Of course emails in that folder also contribute to the disk space usage. Once the Trash
folder is expunged the problem should be gone. You may also allow your users more space in the Trash folder. Thats
explained in the [Dovecot documentation](https://doc.dovecot.org/configuration_manual/quota/#quota-rules).
- If you directly remove files from a users Maildir instead of properly accessing the mailbox using IMAP then you will
screw up the quota calculation. In that case let Dovecot recalculate the quota:
`doveadm quota recalc -u john@example.org`
### Automatic warning emails
The last step is to inform the poor users if they accidentally went over quota. After all they do not necessarily
recognize that on their own. Lets do that by sending them an email with a warning. Yes, we will make sure that the
email gets through even if the quota is reached.
Edit the `90-quota.conf` file again. Add this section to the file (derived from the
[Dovecot documentation](https://doc.dovecot.org/configuration_manual/quota/#quota-warning-scripts)):
```
plugin {
quota_warning = storage=95%% quota-warning 95 %u
quota_warning2 = storage=80%% quota-warning 80 %u
}
service quota-warning {
executable = script /usr/local/bin/quota-warning.sh
unix_listener quota-warning {
user = vmail
group = vmail
mode = 0660
}
}
```
This section defines two automatic quota warnings. The first (quota_warning) is triggered if the user reaches 95% of the
quota. The second (quota_warning2) at 80%. These lines follow this schema:
- **Trigger** (e.g. “storage=95%”). The “%” sign needs to be used twice if you want to emit a literal percent sign. So
this is not a typo.
- The **socket** you want to call in that case. Our socket is the “service quota-warning” that calls a shell script.
- Additional **parameters** that are passed to the shell script in our case. They tell the script the percentage that
has been reached (e.g. 95) and the address of the user who should get the warning.
Apparently we need the script to run. So please create a new file at `/usr/local/bin/quota-warning.sh` and put these
lines into it:
```
#!/bin/sh
PERCENT=$1
USER=$2
cat << EOF | /usr/lib/dovecot/dovecot-lda -d $USER -o "plugin/quota=maildir:User quota:noenforcing"
From: postmaster@webmail.example.org
Subject: Quota warning - $PERCENT% reached
Your mailbox can only store a limited amount of emails.
Currently it is $PERCENT% full. If you reach 100% then
new emails cannot be stored. Thanks for your understanding.
EOF
```
Make this file executable:
```
chmod +x /usr/local/bin/quota-warning.sh
```
Time to restart Dovecot again:
```
systemctl restart dovecot
```
Dovecots quota limits can be configured in many ways. If you have special needs then give
[their documentation](https://doc.dovecot.org/configuration_manual/quota/) a look.

View file

@ -36,12 +36,6 @@ ALTER USER 'mailserver'@'127.0.0.1' IDENTIFIED BY '${PW_MAILSERVER}';
FLUSH PRIVILEGES; FLUSH PRIVILEGES;
EOF EOF
# Delete example data
mariadb <<EOF
DELETE FROM virtual_domains where name='example.org';
EOF
# Print the passwords for us to write down # Print the passwords for us to write down
echo "mailadmin password: $PW_MAILADMIN" echo "mailadmin password: $PW_MAILADMIN"
echo "mailserver password: $PW_MAILSERVER" echo "mailserver password: $PW_MAILSERVER"

View file

@ -1,33 +0,0 @@
---
title: Automated installation
lastUpdated: 2025-12-17
slug: ispmail-trixie/automated-installation
sidebar:
order: 500
---
import { Aside } from "@astrojs/starlight/components";
I have spent an extra couple of days to provide you with a shell script that does all the steps of this guide
automatically on a fresh Debian Trixie system. The previous Bookworm guide contained an Ansible playbook for that
purpose. But although I know Ansible quite well, I have realized that it can make simple tasks very complicated. So I
thought that a shell script is the simplest way.
<Aside type="danger" title="I trust you">
You may be tempted to skip the entire guide and just download and run the installation script. I trust that you are
not doing that. Only use this script after you have set up at least one mail server while following this guide from
start to end. It is easy to use but it is meant for experienced users who want to install multiple mail servers.
</Aside>
To use the automated installer:
```sh
wget https://workaround.org/ispmail.sh
chmod +x ispmail.sh
./ispmail.sh -f example.org
```
Use your main FQDN instead of **example.org**. It will become the main host name for Roundcube, rspamd, SMTP, IMAP and
will be taken as the common name for the Let's Encrypt certificate.
This script is something new. So please report your experience while using it down in the comments.