diff --git a/.gitignore b/.gitignore index 5c459500..23f9e430 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ data/conf/sogo/sieve.creds data/conf/dovecot/dovecot-master.passwd mailcow.conf mailcow.conf_backup +data/conf/nginx/listen.active diff --git a/data/Dockerfiles/postfix/Dockerfile b/data/Dockerfiles/postfix/Dockerfile index bc6015d7..a3781bd4 100644 --- a/data/Dockerfiles/postfix/Dockerfile +++ b/data/Dockerfiles/postfix/Dockerfile @@ -13,6 +13,7 @@ RUN apt-get update RUN apt-get install -y --no-install-recommends supervisor \ postfix \ sasl2-bin \ + libsasl2-modules \ postfix \ postfix-mysql \ postfix-pcre \ diff --git a/data/Dockerfiles/sogo/reconf-domains.sh b/data/Dockerfiles/sogo/reconf-domains.sh index 78482859..8cc24052 100755 --- a/data/Dockerfiles/sogo/reconf-domains.sh +++ b/data/Dockerfiles/sogo/reconf-domains.sh @@ -1,17 +1,27 @@ #!/bin/bash -# Go in a 5 minute loop -while true; do +# Recreate view - # Wait for MySQL to warm-up - while ! mysqladmin ping --host mysql --silent; do - sleep 1 - done +mysql --host mysql -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "DROP VIEW IF EXISTS sogo_view" - mkdir -p /var/lib/sogo/GNUstep/Defaults/ +mysql --host mysql -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF +CREATE VIEW sogo_view (c_uid, domain, c_name, c_password, c_cn, mail, aliases, ad_aliases, senderacl, home) AS +SELECT mailbox.username, mailbox.domain, mailbox.username, mailbox.password, mailbox.name, mailbox.username, IFNULL(ga.aliases, ''), IFNULL(gda.ad_alias, ''), IFNULL(gs.send_as, ''), CONCAT('/var/vmail/', maildir) FROM mailbox +LEFT OUTER JOIN grouped_mail_aliases ga ON ga.username = mailbox.username +LEFT OUTER JOIN grouped_sender_acl gs ON gs.username = mailbox.username +LEFT OUTER JOIN grouped_domain_alias_address gda ON gda.username = mailbox.username +WHERE mailbox.active = '1'; +EOF - # Generate plist header with timezone data - cat < /var/lib/sogo/GNUstep/Defaults/sogod.plist +# Wait for MySQL to warm-up +while ! mysqladmin ping --host mysql --silent; do + sleep 1 +done + +mkdir -p /var/lib/sogo/GNUstep/Defaults/ + +# Generate plist header with timezone data +cat < /var/lib/sogo/GNUstep/Defaults/sogod.plist @@ -22,6 +32,8 @@ while true; do mysql://${DBUSER}:${DBPASS}@mysql:3306/${DBNAME}/sogo_cache_folder OCSEMailAlarmsFolderURL mysql://${DBUSER}:${DBPASS}@mysql:3306/${DBNAME}/sogo_alarms_folder + DomainFieldName + domain OCSFolderInfoURL mysql://${DBUSER}:${DBPASS}@mysql:3306/${DBNAME}/sogo_folder_info OCSSessionsFolderURL @@ -36,14 +48,14 @@ while true; do EOF - # Generate multi-domain setup - while read line +# Generate multi-domain setup +while read line do DOMAIN_SANE=$(echo ${line} | tr '-' 'b' | tr '.' 'p' | tr -cd '[[:alnum:]]') echo " ${line} SOGoMailDomain - $(echo ${line} | tr '-' 'b' | tr '.' 'p') + ${DOMAIN_SANE} SOGoUserSources @@ -72,30 +84,19 @@ EOF userPasswordAlgorithm ssha256 viewURL - mysql://${DBUSER}:${DBPASS}@mysql:3306/${DBNAME}/sogo_view_${DOMAIN_SANE} + mysql://${DBUSER}:${DBPASS}@mysql:3306/${DBNAME}/sogo_view " >> /var/lib/sogo/GNUstep/Defaults/sogod.plist - mysql --host mysql -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "DROP VIEW IF EXISTS sogo_view_${DOMAIN_SANE}" - mysql --host mysql -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF -CREATE VIEW sogo_view_${DOMAIN_SANE} (c_uid, c_name, c_password, c_cn, mail, aliases, ad_aliases, senderacl, home) AS -SELECT mailbox.username, mailbox.username, mailbox.password, mailbox.name, mailbox.username, IFNULL(ga.aliases, ''), IFNULL(gda.ad_alias, ''), IFNULL(gs.send_as, ''), CONCAT('/var/vmail/', maildir) FROM mailbox -LEFT OUTER JOIN grouped_mail_aliases ga ON ga.username = mailbox.username -LEFT OUTER JOIN grouped_sender_acl gs ON gs.username = mailbox.username -LEFT OUTER JOIN grouped_domain_alias_address gda ON gda.username = mailbox.username -WHERE mailbox.active = '1' AND domain = '${line}'; -EOF done < <(mysql --host mysql -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT domain FROM domain;" -B -N) - # Generate footer - echo ' +# Generate footer +echo ' ' >> /var/lib/sogo/GNUstep/Defaults/sogod.plist - # Fix permissions - chown sogo:sogo -R /var/lib/sogo/ - chmod 600 /var/lib/sogo/GNUstep/Defaults/sogod.plist +# Fix permissions +chown sogo:sogo -R /var/lib/sogo/ +chmod 600 /var/lib/sogo/GNUstep/Defaults/sogod.plist - sleep 300 - -done +sleep infinite diff --git a/data/conf/dovecot/dovecot.conf b/data/conf/dovecot/dovecot.conf index e6dc6f70..9b299847 100644 --- a/data/conf/dovecot/dovecot.conf +++ b/data/conf/dovecot/dovecot.conf @@ -13,7 +13,9 @@ mail_location = maildir:~/ mail_plugins = quota acl zlib antispam auth_username_chars = abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890.-_@ ssl_protocols = !SSLv3 !SSLv2 -ssl_cipher_list = EDH+CAMELLIA:EDH+aRSA:EECDH+aRSA+AESGCM:EECDH+aRSA+SHA384:EECDH+aRSA+SHA256:EECDH:+CAMELLIA256:+AES256:+CAMELLIA128:+AES128:+SSLv3:!aNULL:!eNULL:!LOW:!3DES:!MD5:!EXP:!PSK:!DSS:!RC4:!SEED:!ECDSA:CAMELLIA256-SHA:AES256-SHA:CAMELLIA128-SHA:AES128-SHA +ssl_prefer_server_ciphers = yes +ssl_cipher_list = EDH+CAMELLIA:EDH+aRSA:EECDH+aRSA+AESGCM:EECDH+aRSA+SHA256:EECDH:+CAMELLIA128:+AES128:+SSLv3:!aNULL:!eNULL:!LOW:!3DES:!MD5:!EXP:!PSK:!DSS:!RC4:!SEED:!IDEA:!ECDSA:kEDH:CAMELLIA128-SHA:AES128-SHA +ssl_options = no_compression # Automatically regenerates every week ssl_dh_parameters_length = 2048 log_timestamp = "%Y-%m-%d %H:%M:%S " diff --git a/data/conf/dovecot/sieve_after b/data/conf/dovecot/sieve_after index ae6aa5fe..0b43dbcf 100644 --- a/data/conf/dovecot/sieve_after +++ b/data/conf/dovecot/sieve_after @@ -7,6 +7,18 @@ require "envelope"; if header :contains "X-Spam-Flag" "YES" { fileinto "Junk"; } -if envelope :detail :matches "to" "*" { - fileinto :create "INBOX/${1}"; + +if allof ( + envelope :detail :matches "to" "*", + header :contains "X-Moo-Tag" "YES", + mailboxexists "INBOX/${s}" + ) { + fileinto "INBOX/${s}"; +} +elsif allof ( + envelope :detail :matches "to" "*", + header :contains "X-Moo-Tag" "YES" + ) { + set :lower "s" "${1}"; + fileinto :create "INBOX/${s}"; } diff --git a/data/conf/nginx/site.conf b/data/conf/nginx/site.conf index 43570d0c..0ea01dfe 100644 --- a/data/conf/nginx/site.conf +++ b/data/conf/nginx/site.conf @@ -5,9 +5,9 @@ server { ssl_certificate_key /etc/ssl/mail/key.pem; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_prefer_server_ciphers on; - ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH"; + ssl_ciphers 'EDH+CAMELLIA:EDH+aRSA:EECDH+aRSA+AESGCM:EECDH+aRSA+SHA256:EECDH:+CAMELLIA128:+AES128:+SSLv3:!aNULL:!eNULL:!LOW:!3DES:!MD5:!EXP:!PSK:!DSS:!RC4:!SEED:!IDEA:!ECDSA:kEDH:CAMELLIA128-SHA:AES128-SHA'; + add_header Strict-Transport-Security "max-age=15768000; includeSubDomains"; ssl_ecdh_curve secp384r1; - add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"; index index.php index.html; server_name _ autodiscover.* autoconfig.*; error_log /var/log/nginx/error.log; @@ -27,6 +27,10 @@ server { include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param PATH_INFO $fastcgi_path_info; + fastcgi_param PHP_VALUE "max_execution_time = 1200 + max_input_time = 1200 + memory_limit = 64M"; + fastcgi_read_timeout 1200; } rewrite ^(/save.+)$ /rspamd$1 last; @@ -53,7 +57,7 @@ server { } location ^~ /Microsoft-Server-ActiveSync { - proxy_pass http://sogo/SOGo/Microsoft-Server-ActiveSync; + proxy_pass http://sogo:20000/SOGo/Microsoft-Server-ActiveSync; proxy_connect_timeout 1000; proxy_next_upstream timeout error; proxy_send_timeout 1000; diff --git a/data/conf/postfix/main.cf b/data/conf/postfix/main.cf index 6912d6b4..1a96eff2 100644 --- a/data/conf/postfix/main.cf +++ b/data/conf/postfix/main.cf @@ -56,7 +56,7 @@ smtpd_error_sleep_time = 10s smtpd_hard_error_limit = ${stress?1}${stress:5} smtpd_helo_required = yes smtpd_proxy_timeout = 600s -smtpd_recipient_restrictions = check_recipient_access proxy:mysql:/opt/postfix/conf/sql/mysql_tls_enforce_in_policy.cf, permit_sasl_authenticated, permit_mynetworks, reject_invalid_helo_hostname, reject_unknown_reverse_client_hostname, reject_unauth_destination +smtpd_recipient_restrictions = permit_sasl_authenticated, permit_mynetworks, check_recipient_access proxy:mysql:/opt/postfix/conf/sql/mysql_tls_enforce_in_policy.cf, reject_invalid_helo_hostname, reject_unknown_reverse_client_hostname, reject_unauth_destination smtpd_sasl_auth_enable = yes smtpd_sasl_authenticated_header = yes smtpd_sasl_path = inet:dovecot:10001 @@ -69,12 +69,16 @@ smtpd_tls_dh1024_param_file = /etc/ssl/mail/dhparams.pem smtpd_tls_eecdh_grade = strong smtpd_tls_exclude_ciphers = ECDHE-RSA-RC4-SHA, RC4, aNULL smtpd_tls_loglevel = 1 -smtpd_tls_mandatory_ciphers = high -smtpd_tls_mandatory_exclude_ciphers = ECDHE-RSA-RC4-SHA, RC4, aNULL +smtp_tls_mandatory_protocols = !SSLv2, !SSLv3 +smtp_tls_protocols = !SSLv2, !SSLv3 +lmtp_tls_mandatory_protocols = !SSLv2, !SSLv3 +lmtp_tls_protocols = !SSLv2, !SSLv3 smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3 smtpd_tls_protocols = !SSLv2, !SSLv3 +smtpd_tls_mandatory_ciphers = high smtpd_tls_security_level = may -tls_high_cipherlist = EDH+CAMELLIA:EDH+aRSA:EECDH+aRSA+AESGCM:EECDH+aRSA+SHA384:EECDH+aRSA+SHA256:EECDH:+CAMELLIA256:+AES256:+CAMELLIA128:+AES128:+SSLv3:!aNULL:!eNULL:!LOW:!3DES:!MD5:!EXP:!PSK:!DSS:!RC4:!SEED:!ECDSA:CAMELLIA256-SHA:AES256-SHA:CAMELLIA128-SHA:AES128-SHA +tls_ssl_options = NO_COMPRESSION +tls_high_cipherlist = EDH+CAMELLIA:EDH+aRSA:EECDH+aRSA+AESGCM:EECDH+aRSA+SHA256:EECDH:+CAMELLIA128:+AES128:+SSLv3:!aNULL:!eNULL:!LOW:!3DES:!MD5:!EXP:!PSK:!DSS:!RC4:!SEED:!IDEA:!ECDSA:kEDH:CAMELLIA128-SHA:AES128-SHA virtual_alias_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_alias_maps.cf, proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_spamalias_maps.cf, proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_alias_domain_maps.cf, proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_alias_domain_catchall_maps.cf virtual_gid_maps = static:5000 virtual_mailbox_base = /var/vmail/ @@ -86,3 +90,4 @@ virtual_uid_maps = static:5000 smtpd_milters = inet:rmilter:9900 non_smtpd_milters = inet:rmilter:9900 milter_mail_macros = i {mail_addr} {client_addr} {client_name} {auth_authen} +mydestination = localhost.localdomain, localhost diff --git a/data/conf/rspamd/dynmaps/authoritative.php b/data/conf/rspamd/dynmaps/authoritative.php new file mode 100644 index 00000000..b2c101f7 --- /dev/null +++ b/data/conf/rspamd/dynmaps/authoritative.php @@ -0,0 +1,22 @@ + PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => false, +]; +$pdo = new PDO($dsn, $database_user, $database_pass, $opt); +$stmt = $pdo->query("SELECT `domain` FROM `domain`"); +$rows = $stmt->fetchAll(PDO::FETCH_ASSOC); +while ($row = array_shift($rows)) { + echo strtolower(trim($row['domain'])) . PHP_EOL; +} +$stmt = $pdo->query("SELECT `alias_domain` FROM `alias_domain`"); +$rows = $stmt->fetchAll(PDO::FETCH_ASSOC); +while ($row = array_shift($rows)) { + echo strtolower(trim($row['alias_domain'])) . PHP_EOL; +} +?> \ No newline at end of file diff --git a/data/conf/rspamd/dynmaps/tags.php b/data/conf/rspamd/dynmaps/tags.php new file mode 100644 index 00000000..cc6435fe --- /dev/null +++ b/data/conf/rspamd/dynmaps/tags.php @@ -0,0 +1,17 @@ + PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => false, +]; +$pdo = new PDO($dsn, $database_user, $database_pass, $opt); +$stmt = $pdo->query("SELECT `username` FROM `mailbox` WHERE `wants_tagged_subject` = '1'"); +$rows = $stmt->fetchAll(PDO::FETCH_ASSOC); +while ($row = array_shift($rows)) { + echo strtolower(trim($row['username'])) . PHP_EOL; +} +?> \ No newline at end of file diff --git a/data/conf/rspamd/lua/rspamd.local.lua b/data/conf/rspamd/lua/rspamd.local.lua index 292b7d08..7e254d3b 100644 --- a/data/conf/rspamd/lua/rspamd.local.lua +++ b/data/conf/rspamd/lua/rspamd.local.lua @@ -10,3 +10,52 @@ rspamd_config.MAILCOW_AUTH = { rspamd_config.MAILCOW_MOO = function (task) return true end + +local modify_subject_map = rspamd_config:add_map({ + url = 'http://nginx:8081/tags.php', + type = 'map', + description = 'Map of users to use subject tags for' +}) + +local auth_domain_map = rspamd_config:add_map({ + url = 'http://nginx:8081/authoritative.php', + type = 'map', + description = 'Map of domains we are authoritative for' +}) + +rspamd_config.ADD_DELIMITER_TAG = { + callback = function(task) + local util = require("rspamd_util") + local rspamd_logger = require "rspamd_logger" + local user_tagged = task:get_recipients(1)[1]['user'] + local domain = task:get_recipients(1)[1]['domain'] + local user, tag = user_tagged:match("([^+]+)+(.*)") + local authdomain = auth_domain_map:get_key(domain) + + if tag and authdomain then + rspamd_logger.infox("Domain %s is part of mailcow, start reading tag settings", domain) + local user_untagged = user .. '@' .. domain + rspamd_logger.infox("Querying tag settings for user %1", user_untagged) + if modify_subject_map:get_key(user_untagged) then + rspamd_logger.infox("User wants subject modified for tagged mail") + local sbj = task:get_header('Subject') + if tag then + rspamd_logger.infox("Found tag %1, will modify subject header", tag) + new_sbj = '=?UTF-8?B?' .. tostring(util.encode_base64('[' .. tag .. '] ' .. sbj)) .. '?=' + task:set_rmilter_reply({ + remove_headers = {['Subject'] = 1}, + add_headers = {['Subject'] = new_sbj} + }) + end + else + rspamd_logger.infox("Add X-Moo-Tag header") + task:set_rmilter_reply({ + add_headers = {['X-Moo-Tag'] = 'YES'} + }) + end + else + rspamd_logger.infox("Skip delimiter handling for untagged message or authenticated user") + end + return false + end +} diff --git a/data/web/inc/footer.inc.php b/data/web/inc/footer.inc.php index e9a8a18d..d2adca8e 100644 --- a/data/web/inc/footer.inc.php +++ b/data/web/inc/footer.inc.php @@ -1,17 +1,17 @@ diff --git a/docker-compose.yml b/docker-compose.yml index b91c5516..a31b4244 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -58,6 +58,7 @@ services: - ./data/conf/rspamd/local.d/:/etc/rspamd/local.d:ro - ./data/conf/rspamd/lua/:/etc/rspamd/lua/:ro - dkim-vol-1:/data/dkim + - rspamd-vol-1:/var/lib/rspamd restart: always dns: - 172.22.1.254 @@ -69,6 +70,7 @@ services: php-fpm-mailcow: image: andryyy/mailcow-dockerized:phpfpm + command: "php-fpm -d date.timezone=${TZ}" depends_on: - pdns-mailcow volumes: @@ -83,7 +85,6 @@ services: - DBUSER=${DBUSER} - DBPASS=${DBPASS} - MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME} - - TZ=${TZ} restart: always networks: mailcow-network: @@ -234,3 +235,4 @@ volumes: mysql-vol-1: dkim-vol-1: redis-vol-1: + rspamd-vol-1: diff --git a/generate_config.sh b/generate_config.sh index 196a7db5..aa32c5ba 100755 --- a/generate_config.sh +++ b/generate_config.sh @@ -12,10 +12,16 @@ if [[ -f mailcow.conf ]]; then esac fi -read -p "Hostname (FQDN): " -ei "mx.example.org" MAILCOW_HOSTNAME -read -p "Timezone: " -ei "Europe/Berlin" TZ -read -p "WebUI Default Language: " -ei "en/pt/de/nl" MAILCOW_LANGUAGE +if [ -z "$MAILCOW_LANGUAGE" ]; then + read -p "WebUI Default Language: " -ei "en/pt/de/nl" MAILCOW_LANGUAGE +fi +if [ -z "$MAILCOW_HOSTNAME" ]; then + read -p "Hostname (FQDN): " -ei "mx.example.org" MAILCOW_HOSTNAME +fi +if [ -z "$TZ" ]; then + read -p "Timezone: " -ei "Europe/Berlin" TZ +fi cat << EOF > data/web/inc/vars.local.inc.php mailcow.conf # ------------------------------ # mailcow web ui configuration