From 6d1dd404843e3e2a6f6fad71728dad1933a7970e Mon Sep 17 00:00:00 2001 From: Matthew Saunders Brown Date: Mon, 13 Dec 2021 16:35:42 -0800 Subject: [PATCH] fix for forwarding save option, added SRS --- etc/exim4/bounce_message_text | 12 ++++ etc/exim4/exim4.conf | 122 +++++++++++++++++++++++++--------- install.sh | 16 ++++- systemd/srsd.service | 15 +++++ 4 files changed, 133 insertions(+), 32 deletions(-) create mode 100644 etc/exim4/bounce_message_text create mode 100644 systemd/srsd.service diff --git a/etc/exim4/bounce_message_text b/etc/exim4/bounce_message_text new file mode 100644 index 0000000..a2d2e30 --- /dev/null +++ b/etc/exim4/bounce_message_text @@ -0,0 +1,12 @@ +**** + +This message was created automatically by the mail delivery software on $primary_hostname: + +A message ${if eq{$sender_address}{$bounce_recipient}{that you sent}{sent by\n}} +${if ! eq{$sender_address}{$bounce_recipient} {${if match {$sender_address} {\N(?i)^SRS[01][=+-]\N} {${sg {${sg {$sender_address} {\N(?i)^SRS[01][=+-][^ @]+?[=+-][a-zA-Z0-9]{2}[=+-]([^ @]+?)[=+-]([^ @]++)@.*$\N} {\N $2@$1\N}} } {\N^ prvs=[^=]+?=(.*)$\N} {\N $1\N} }\n} {${if match {$sender_address} {\N(?i)^prvs=\N} {${sg {$sender_address} {\N(?i)^prvs=[^ @]+?=(.*@.*)$\N} {\N $1\n\N}}} { $sender_address\n}}} }}} +could not be delivered to one or more of its recipients. The following +address(es) failed: +**** +**** +**** +**** diff --git a/etc/exim4/exim4.conf b/etc/exim4/exim4.conf index 584382c..dacce0d 100644 --- a/etc/exim4/exim4.conf +++ b/etc/exim4/exim4.conf @@ -7,6 +7,7 @@ disable_ipv6 = true keep_environment = add_environment = PATH=/usr/sbin:/usr/bin:/sbin:/bin smtp_enforce_sync = false +bounce_message_file = /etc/exim4/bounce_message_text smtp_accept_max = 50 smtp_accept_max_per_host = 10 @@ -202,7 +203,7 @@ rfc1413_query_timeout = 0s ignore_bounce_errors_after = 0s -# This option cancels (removes) frozen messages that are older than a week. +# This option cancels (removes) frozen messages that are older than a day. timeout_frozen_after = 1d @@ -433,6 +434,32 @@ acl_rcpt_to: # Recipent Address Checks ###################################################################### + # Ensure only valid SRS prefixed bounce message get accepted + deny + senders = : + domains = +local_domains + local_parts = ${if match {$local_part} {(?i)\N^SRS[01][=+-]\N} {$local_part}} + control = caseful_local_part + condition = ${if match{${readsocket{/run/srsd/srsd.sock}{REVERSE $local_part@$domain}{5s}{\n}}}{^ERROR: .* Invalid hash at .*}} + message = Invalid reverse path (SRS check failed on $local_part@$domain). + + warn + senders = : + domains = +local_domains + local_parts = ${if match {$local_part} {\N^srs[01][=+-]\N} {$local_part}} + control = caseful_local_part + condition = ${if match{${readsocket{/run/srsd/srsd.sock}{REVERSE $local_part@$domain}{5s}{ }}}{^SRS: Case insensitive hash match detected. Someone smashed case in the local-part. .*}} + log_message = SRS hash smashed on the way for $local_part@$domain by case insensitive MTA. + + # this is for debugging only. can be safely removed any time + warn + senders = : + domains = +local_domains + local_parts = ${if match {$local_part} {(?i)\N^SRS[01][=+-]\N} {$local_part}} + control = caseful_local_part + condition = ${if !match{${readsocket{/run/srsd/srsd.sock}{REVERSE $local_part@$domain}{5s}{\n}}}{^ERROR: .* Invalid hash at .*}} + log_message = Incoming SRS bounce to $local_part@$domain + # Deny if the local part contains @ or % or / or | or !. These are # rarely found in genuine local parts, but are often tried by people # looking to circumvent relaying restrictions. @@ -697,6 +724,33 @@ autowhitelist_filter: unseen allow_filter = true +srs_bounce: + senders = : + driver = redirect + domains = +local_domains + allow_fail + allow_defer + local_part_prefix = srs0+ : srs0- : srs0= : srs1+ : srs1- : srs1= + caseful_local_part + address_data = ${readsocket{/run/srsd/srsd.sock}{REVERSE $local_part_prefix$local_part@$domain}{5s}{ }{:defer: SRS daemon failure}} + data = ${sg {$address_data} {^SRS: Case insensitive hash match detected. Someone smashed case in the local-part\. .* ([^ ]+)@([^ ]+)\$} {\N$1@$2\N} } + headers_add = X-SRS: Decoded valid SRS return address to ${quote_local_part:${local_part:$address_data}}@${domain:$address_data} by $primary_hostname + +srs_forward: + driver = redirect + senders = ! : ! *@+local_domains + domains = ! +local_domains : ! +relay_to_domains + condition = ${lookup mysql{SELECT vm_domains.id FROM vm_domains WHERE vm_domains.domain='${original_domain}' AND vm_domains.status = '1'}} + address_data = ${readsocket{/run/srsd/srsd.sock}\ + {FORWARD $sender_address_local_part@$sender_address_domain $original_domain\n}\ + {5s}{\n}{:defer: SRS daemon failure}} + errors_to = ${quote_local_part:${local_part:$address_data}}@${domain:$address_data} + data = ${quote_local_part:$local_part}@$domain + headers_add = X-SRS-Forward: from $sender_address to $original_local_part@$original_domain forwarded to $local_part@$domain by $primary_hostname + repeat_use = false + allow_defer + no_verify + # This router routes addresses that are not in local domains by doing a DNS # lookup on the domain name. Any domain that resolves to 0.0.0.0 or to a # loopback interface address (127.0.0.0/8) is treated as if it had no DNS @@ -735,24 +789,6 @@ junk_filter: condition = ${lookup mysql{SELECT vm_mboxes.id FROM vm_domains, vm_mboxes WHERE vm_domains.domain='${domain}' AND vm_mboxes.mbox='${local_part}' AND vm_domains.id = vm_mboxes.domain_id AND vm_domains.status = '1' AND vm_mboxes.status = '1' AND vm_mboxes.filter > '0'}} transport = junk_delivery -spamcheck_router: - driver = accept - domains = +local_domains - local_part_suffix = +* - local_part_suffix_optional = true - condition = ${if and { \ - { !eq {$received_protocol}{spam-scanned}} \ - { !eq {$sender_address_domain}{$domain}} \ - { < {$message_size}{512k}} \ - { !eq {$header_X-Junk-Flag:}{YES}} \ - { !eq {$header_X-Whitelist-Flag:}{YES}} \ - { eq {${lookup mysql{SELECT vm_mboxes.status FROM vm_domains, vm_mboxes WHERE vm_domains.domain='${domain}' AND vm_mboxes.mbox='${local_part}' AND vm_domains.id = vm_mboxes.domain_id AND vm_domains.status = '1'}{$value}fail}}{1} } \ - } {yes} {no}} - # check domain & mbox 'status'? - # Check for other headers too? Blacklist, SPF, DKIM failers go directly to Spam folder without spam scan??? - actually they should go to spam folder before this router is hit? - headers_remove = X-Spam-Checker-Version:X-Spam-Flag:X-Spam-Level:X-Spam-Status:X-Spam-Score:X-Spam-Report - transport = spamcheck - spam_filter: driver = accept domains = +local_domains @@ -771,8 +807,9 @@ virtual_vacation: domains = +local_domains # do not reply to errors or lists or spam-scanned messages, require vacation message in db condition = ${if and { \ - {!match {$h_precedence:} {(?i)junk|bulk|list}} \ - {!eq {$sender_address} {}} \ + { !match {$h_precedence:} {(?i)junk|bulk|list}} \ + { !eq {$received_protocol}{spam-scanned}} \ + { !eq {$sender_address} {}} \ { eq {${lookup mysql{SELECT vm_autoresponders.mode FROM vm_domains, vm_mboxes, vm_autoresponders WHERE vm_domains.domain='${domain}' AND vm_mboxes.mbox='${local_part}' AND vm_domains.id = vm_mboxes.domain_id AND vm_autoresponders.mbox_id = vm_mboxes.id AND vm_domains.status = '1' AND vm_mboxes.status = '1' AND vm_autoresponders.status = '1'}{$value}fail}}{Vacation}} \ } {yes} {no}} # add options for start & end date fields @@ -796,8 +833,9 @@ virtual_autoresponder: #local_part_suffix_optional = true # do not reply to errors or lists or spam-scanned messages, require autoresponder message in db condition = ${if and { \ - {!match {$h_precedence:} {(?i)junk|bulk|list}} \ - {!eq {$sender_address} {}} \ + { !match {$h_precedence:} {(?i)junk|bulk|list}} \ + { !eq {$received_protocol}{spam-scanned}} \ + { !eq {$sender_address} {}} \ { eq {${lookup mysql{SELECT vm_autoresponders.mode FROM vm_domains, vm_mboxes, vm_autoresponders WHERE vm_domains.domain='${domain}' AND vm_mboxes.mbox='${local_part}' AND vm_domains.id = vm_mboxes.domain_id AND vm_autoresponders.mbox_id = vm_mboxes.id AND vm_domains.status = '1' AND vm_mboxes.status = '1' AND vm_autoresponders.status = '1'}{$value}fail}}{Autoresponder} } \ } {yes} {no}} # add options for start & end date fields @@ -813,13 +851,38 @@ virtual_autoresponder: unseen no_verify -virtual_forward: +virtual_forward_and_drop: driver = redirect domains = +local_domains + condition = ${if !eq {$received_protocol}{spam-scanned}} + local_part_suffix = +* + local_part_suffix_optional = true + data = ${lookup mysql{SELECT vm_forwards.forward_to FROM vm_domains, vm_mboxes, vm_forwards WHERE vm_domains.domain='${domain}' AND vm_domains.id = vm_mboxes.domain_id AND vm_mboxes.mbox='${local_part}' AND vm_mboxes.id=vm_forwards.mbox_id AND vm_domains.status = '1' AND vm_mboxes.status = '1' AND vm_forwards.save_local='0'}} + +virtual_forward_and_keep: + driver = redirect + domains = +local_domains + condition = ${if !eq {$received_protocol}{spam-scanned}} + local_part_suffix = +* + local_part_suffix_optional = true + data = ${lookup mysql{SELECT CONCAT('${local_part}@${domain}\n', vm_forwards.forward_to) FROM vm_domains, vm_mboxes, vm_forwards WHERE vm_domains.domain='${domain}' AND vm_domains.id = vm_mboxes.domain_id AND vm_mboxes.mbox='${local_part}' AND vm_mboxes.id=vm_forwards.mbox_id AND vm_domains.status = '1' AND vm_mboxes.status = '1' AND vm_forwards.save_local='1'}} + +spamcheck_router: + driver = accept + domains = +local_domains local_part_suffix = +* local_part_suffix_optional = true - data = ${lookup mysql{SELECT vm_forwards.forward_to FROM vm_domains, vm_mboxes, vm_forwards WHERE vm_domains.domain='${domain}' AND vm_domains.id = vm_mboxes.domain_id AND vm_mboxes.mbox='${local_part}' AND vm_mboxes.id=vm_forwards.mbox_id AND vm_domains.status = '1' AND vm_mboxes.status = '1' }} - unseen = ${lookup mysql{SELECT vm_forwards.id FROM vm_domains, vm_mboxes, vm_forwards WHERE vm_domains.domain='${domain}' AND vm_domains.id = vm_mboxes.domain_id AND vm_mboxes.mbox='${local_part}' AND vm_mboxes.id=vm_forwards.mbox_id AND vm_domains.status = '1' AND vm_mboxes.status = '1' AND vm_forwards.save_local='1'}{true}{false}} + condition = ${if and { \ + { !eq {$received_protocol}{spam-scanned}} \ + { !eq {$sender_address_domain}{$domain}} \ + { < {$message_size}{512k}} \ + { !eq {$header_X-Junk-Flag:}{YES}} \ + { !eq {$header_X-Whitelist-Flag:}{YES}} \ + { eq {${lookup mysql{SELECT vm_mboxes.status FROM vm_domains, vm_mboxes WHERE vm_domains.domain='${domain}' AND vm_mboxes.mbox='${local_part}' AND vm_domains.id = vm_mboxes.domain_id AND vm_domains.status = '1' AND vm_mboxes.status = '1'}{$value}fail}}{1} } \ + } {yes} {no}} + # Check for other headers too? Blacklist, SPF, DKIM failers go directly to Spam folder without spam scan??? - actually they should go to spam folder before this router is hit? + headers_remove = X-Spam-Checker-Version:X-Spam-Flag:X-Spam-Level:X-Spam-Status:X-Spam-Score:X-Spam-Report + transport = spamcheck user_filter: driver = redirect @@ -1094,10 +1157,8 @@ address_reply: begin retry # This single retry rule applies to all domains and all errors. It specifies -# retries every 15 minutes for 2 hours, then increasing retry intervals, -# starting at 1 hour and increasing each time by a factor of 1.5, up to 16 -# hours, then retries every 6 hours until 4 days have passed since the first -# failed delivery. +# retries every 15 minutes for 2 hours, then every 2 hours until 1 full day +# has passed since the first delivery failed. # Domain Error Retries # ------ ----- ------- @@ -1114,7 +1175,6 @@ begin retry begin rewrite - ###################################################################### # AUTHENTICATION CONFIGURATION # ###################################################################### diff --git a/install.sh b/install.sh index 039b864..0daa494 100755 --- a/install.sh +++ b/install.sh @@ -59,7 +59,7 @@ mysql -e "GRANT ALL PRIVILEGES ON vmail.* TO 'vmail'@'localhost';" mysqladmin flush-privileges # install mail server software -apt -y install exim4-daemon-heavy spf-tools-perl spamassassin libclass-dbi-mysql-perl dovecot-core dovecot-imapd dovecot-mysql dovecot-pop3d dovecot-lmtpd +apt -y install exim4-daemon-heavy spf-tools-perl spamassassin srs libclass-dbi-mysql-perl dovecot-core dovecot-imapd dovecot-mysql dovecot-pop3d dovecot-lmtpd # configure system users apt -y install ssl-cert @@ -97,6 +97,16 @@ chmod 644 /etc/spamassassin/local.cf chown debian-spamd:mail /etc/spamassassin/sql.cf chmod 640 /etc/spamassassin/sql.cf +# srsd +# bug fixes for libmail-srs-perl. still needed as of v0.31-6 on Ubuntu 20.04 +sed -i 's|/tmp/srsd|/run/srsd/srsd.sock|' /usr/share/perl5/Mail/SRS/Daemon.pm +sed -i '/Until we decide that forward/,+3d' /usr/share/perl5/Mail/SRS/Daemon.pm +cp systemd/srsd.service /lib/systemd/system/srsd.service +chmod 644 /lib/systemd/system/srsd.service +systemctl daemon-reload +systemctl enable srsd +systemctl start srsd + # exim config maildomain=`hostname -d` sed -i 's/size 10M/daily/g' /etc/logrotate.d/exim4-paniclog @@ -112,6 +122,10 @@ chmod 640 /etc/exim4/skip_greylisting_hosts sed -i "s|example.com|$maildomain|g" /etc/exim4/skip_greylisting_hosts sed -i "s|password|$VMAILPASS|g" /etc/exim4/exim_local.conf sed -i "s|example.com|$maildomain|g" /etc/exim4/exim_local.conf +touch /etc/exim4/srsd.secret +chmod 640 /etc/exim4/srsd.secret +chown Debian-exim:Debian-exim /etc/exim4/srsd.secret +pwgen -N 1 -cny 64 > /etc/exim4/srsd.secret # dovecot config mkdir /etc/dovecot/sites.d diff --git a/systemd/srsd.service b/systemd/srsd.service new file mode 100644 index 0000000..0b37db6 --- /dev/null +++ b/systemd/srsd.service @@ -0,0 +1,15 @@ +[Unit] +Description=Sender Rewriting Scheme Daemon + +[Service] +Type=exec +User=Debian-exim +Group=Debian-exim +ExecStart=/usr/bin/srsd --secretfile /etc/exim4/srsd.secret --hashlength 24 +PIDFile=/run/srsd/srsd.pid +Restart=on-failure +RuntimeDirectory=srsd +RuntimeDirectoryMode=0750 + +[Install] +WantedBy=multi-user.target