Compare commits
No commits in common. "c8d9f03587d5d8c6ca601b5232daa12a2807c091" and "4f159716ef5fd324ed6a37fbc00d6dd42fc3634f" have entirely different histories.
c8d9f03587
...
4f159716ef
10 changed files with 18 additions and 1380 deletions
|
|
@ -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
|
|
||||||
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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. Don’t hurry and skip
|
- **Patience**. We will proceed slowly and after every step ensure that are still on track. Don’t 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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 |
|
||||||
| :--------- | :---------- | :--------------------------------------------------------- |
|
| :--------- | :---------- | :--------------------------------------------------------- |
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
```
|
```
|
||||||
|
|
||||||
That’s all. rspamd now knows that whenever it sees an outgoing email from `anyone@example.org` it will get the DKIM
|
That’s all. rspamd now knows that whenever it sees an outgoing email from `anyone@example.org` it will get the DKIM
|
||||||
|
|
|
||||||
|
|
@ -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
|

|
||||||
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 user’s 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
|
|
||||||
|
|
||||||
Let’s 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 user’s _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 don’t 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 don’t want that.
|
|
||||||
|
|
||||||
So the next logical step is to make Postfix check whether a mailbox is over quota whenever a new email arrives. Let’s
|
|
||||||
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 recipient’s 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 John’s 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. That’s
|
|
||||||
explained in the [Dovecot documentation](https://doc.dovecot.org/configuration_manual/quota/#quota-rules).
|
|
||||||
- If you directly remove files from a user’s 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. Let’s 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
|
|
||||||
```
|
|
||||||
|
|
||||||
Dovecot’s 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.
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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.
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue