From de7350baabe45836e01f42c0b2525783c84e23f3 Mon Sep 17 00:00:00 2001 From: Patrick Niebeling Date: Wed, 6 Nov 2024 17:13:46 +0100 Subject: [PATCH] First Shot Signed-off-by: Patrick Niebeling --- Stable2.0/Dockerfile | 60 ++ Stable2.0/conf/custom/bad_asn.map | 31 + Stable2.0/conf/custom/bad_header.map | 2 + Stable2.0/conf/custom/bad_languages.map | 1 + Stable2.0/conf/custom/bad_words.map | 29 + Stable2.0/conf/custom/bad_words_de.map | 17 + Stable2.0/conf/custom/bulk_header.map | 19 + Stable2.0/conf/custom/fishy_tlds.map | 65 ++ .../custom/global_mime_from_blacklist.map | 1 + .../custom/global_mime_from_whitelist.map | 1 + .../conf/custom/global_rcpt_blacklist.map | 1 + .../conf/custom/global_rcpt_whitelist.map | 1 + .../custom/global_smtp_from_blacklist.map | 1 + .../custom/global_smtp_from_whitelist.map | 1 + Stable2.0/conf/custom/ip_wl.map | 4 + Stable2.0/conf/custom/monitoring_nolog.map | 7 + Stable2.0/conf/dynmaps/aliasexp.php | 174 +++++ Stable2.0/conf/dynmaps/bcc.php | 88 +++ Stable2.0/conf/dynmaps/footer.php | 113 +++ Stable2.0/conf/dynmaps/forwardinghosts.php | 57 ++ Stable2.0/conf/dynmaps/index.html | 2 + Stable2.0/conf/dynmaps/sasl_logs.php | 2 + Stable2.0/conf/dynmaps/settings.php | 471 ++++++++++++ Stable2.0/conf/dynmaps/vars.inc.php | 6 + Stable2.0/conf/local.d/actions.conf | 3 + Stable2.0/conf/local.d/antivirus.conf | 11 + Stable2.0/conf/local.d/arc.conf | 32 + Stable2.0/conf/local.d/asn.conf | 6 + Stable2.0/conf/local.d/composites.conf | 110 +++ Stable2.0/conf/local.d/dkim_signing.conf | 35 + Stable2.0/conf/local.d/external_services.conf | 12 + Stable2.0/conf/local.d/force_actions.conf | 12 + Stable2.0/conf/local.d/fuzzy_check.conf | 54 ++ Stable2.0/conf/local.d/fuzzy_group.conf | 17 + Stable2.0/conf/local.d/greylist.conf | 4 + Stable2.0/conf/local.d/groups.conf | 59 ++ Stable2.0/conf/local.d/headers_group.conf | 7 + Stable2.0/conf/local.d/hfilter_group.conf | 5 + Stable2.0/conf/local.d/history_redis.conf | 1 + Stable2.0/conf/local.d/metadata_exporter.conf | 72 ++ Stable2.0/conf/local.d/milter_headers.conf | 43 ++ Stable2.0/conf/local.d/mime_types.conf | 47 ++ Stable2.0/conf/local.d/mime_types_group.conf | 7 + Stable2.0/conf/local.d/multimap.conf | 181 +++++ Stable2.0/conf/local.d/mx_check.conf | 7 + Stable2.0/conf/local.d/options.inc | 9 + Stable2.0/conf/local.d/phishing.conf | 1 + Stable2.0/conf/local.d/policies_group.conf | 26 + Stable2.0/conf/local.d/ratelimit.conf | 9 + Stable2.0/conf/local.d/rbl.conf | 26 + Stable2.0/conf/local.d/rbl_group.conf | 277 +++++++ Stable2.0/conf/local.d/redis.conf | 2 + Stable2.0/conf/local.d/reputation.conf | 9 + Stable2.0/conf/local.d/spamassassin.conf | 1 + Stable2.0/conf/local.d/statistic.conf | 26 + Stable2.0/conf/local.d/statistics_group.conf | 10 + Stable2.0/conf/lua/ratelimit.lua | 16 + Stable2.0/conf/lua/rspamd.local.lua | 701 ++++++++++++++++++ Stable2.0/conf/meta_exporter/pipe.php | 260 +++++++ Stable2.0/conf/meta_exporter/pipe_rl.php | 48 ++ Stable2.0/conf/meta_exporter/pushover.php | 275 +++++++ Stable2.0/conf/meta_exporter/vars.inc.php | 6 + Stable2.0/conf/override.d/logging.inc | 5 + Stable2.0/conf/override.d/ratelimit.conf | 4 + .../conf/override.d/worker-controller.inc | 7 + Stable2.0/conf/override.d/worker-fuzzy.inc | 12 + Stable2.0/conf/override.d/worker-normal.inc | 4 + Stable2.0/conf/override.d/worker-proxy.inc | 9 + Stable2.0/conf/plugins.d/README.md | 1 + Stable2.0/conf/rspamd.conf.local | 1 + Stable2.0/conf/rspamd.conf.override | 2 + Stable2.0/hooks/build | 17 + Stable2.0/hooks/post_push | 9 + Stable2.0/rspamd.conf.local.override | 13 + Stable2.0/worker-controller.inc | 1 + Stable2.0/worker-proxy.inc | 1 + 76 files changed, 3667 insertions(+) create mode 100644 Stable2.0/Dockerfile create mode 100644 Stable2.0/conf/custom/bad_asn.map create mode 100644 Stable2.0/conf/custom/bad_header.map create mode 100644 Stable2.0/conf/custom/bad_languages.map create mode 100644 Stable2.0/conf/custom/bad_words.map create mode 100644 Stable2.0/conf/custom/bad_words_de.map create mode 100644 Stable2.0/conf/custom/bulk_header.map create mode 100644 Stable2.0/conf/custom/fishy_tlds.map create mode 100644 Stable2.0/conf/custom/global_mime_from_blacklist.map create mode 100644 Stable2.0/conf/custom/global_mime_from_whitelist.map create mode 100644 Stable2.0/conf/custom/global_rcpt_blacklist.map create mode 100644 Stable2.0/conf/custom/global_rcpt_whitelist.map create mode 100644 Stable2.0/conf/custom/global_smtp_from_blacklist.map create mode 100644 Stable2.0/conf/custom/global_smtp_from_whitelist.map create mode 100644 Stable2.0/conf/custom/ip_wl.map create mode 100644 Stable2.0/conf/custom/monitoring_nolog.map create mode 100644 Stable2.0/conf/dynmaps/aliasexp.php create mode 100644 Stable2.0/conf/dynmaps/bcc.php create mode 100644 Stable2.0/conf/dynmaps/footer.php create mode 100644 Stable2.0/conf/dynmaps/forwardinghosts.php create mode 100644 Stable2.0/conf/dynmaps/index.html create mode 100644 Stable2.0/conf/dynmaps/sasl_logs.php create mode 100644 Stable2.0/conf/dynmaps/settings.php create mode 100644 Stable2.0/conf/dynmaps/vars.inc.php create mode 100644 Stable2.0/conf/local.d/actions.conf create mode 100644 Stable2.0/conf/local.d/antivirus.conf create mode 100644 Stable2.0/conf/local.d/arc.conf create mode 100644 Stable2.0/conf/local.d/asn.conf create mode 100644 Stable2.0/conf/local.d/composites.conf create mode 100644 Stable2.0/conf/local.d/dkim_signing.conf create mode 100644 Stable2.0/conf/local.d/external_services.conf create mode 100644 Stable2.0/conf/local.d/force_actions.conf create mode 100644 Stable2.0/conf/local.d/fuzzy_check.conf create mode 100644 Stable2.0/conf/local.d/fuzzy_group.conf create mode 100644 Stable2.0/conf/local.d/greylist.conf create mode 100644 Stable2.0/conf/local.d/groups.conf create mode 100644 Stable2.0/conf/local.d/headers_group.conf create mode 100644 Stable2.0/conf/local.d/hfilter_group.conf create mode 100644 Stable2.0/conf/local.d/history_redis.conf create mode 100644 Stable2.0/conf/local.d/metadata_exporter.conf create mode 100644 Stable2.0/conf/local.d/milter_headers.conf create mode 100644 Stable2.0/conf/local.d/mime_types.conf create mode 100644 Stable2.0/conf/local.d/mime_types_group.conf create mode 100644 Stable2.0/conf/local.d/multimap.conf create mode 100644 Stable2.0/conf/local.d/mx_check.conf create mode 100644 Stable2.0/conf/local.d/options.inc create mode 100644 Stable2.0/conf/local.d/phishing.conf create mode 100644 Stable2.0/conf/local.d/policies_group.conf create mode 100644 Stable2.0/conf/local.d/ratelimit.conf create mode 100644 Stable2.0/conf/local.d/rbl.conf create mode 100644 Stable2.0/conf/local.d/rbl_group.conf create mode 100644 Stable2.0/conf/local.d/redis.conf create mode 100644 Stable2.0/conf/local.d/reputation.conf create mode 100644 Stable2.0/conf/local.d/spamassassin.conf create mode 100644 Stable2.0/conf/local.d/statistic.conf create mode 100644 Stable2.0/conf/local.d/statistics_group.conf create mode 100644 Stable2.0/conf/lua/ratelimit.lua create mode 100644 Stable2.0/conf/lua/rspamd.local.lua create mode 100644 Stable2.0/conf/meta_exporter/pipe.php create mode 100644 Stable2.0/conf/meta_exporter/pipe_rl.php create mode 100644 Stable2.0/conf/meta_exporter/pushover.php create mode 100644 Stable2.0/conf/meta_exporter/vars.inc.php create mode 100644 Stable2.0/conf/override.d/logging.inc create mode 100644 Stable2.0/conf/override.d/ratelimit.conf create mode 100644 Stable2.0/conf/override.d/worker-controller.inc create mode 100644 Stable2.0/conf/override.d/worker-fuzzy.inc create mode 100644 Stable2.0/conf/override.d/worker-normal.inc create mode 100644 Stable2.0/conf/override.d/worker-proxy.inc create mode 100644 Stable2.0/conf/plugins.d/README.md create mode 100644 Stable2.0/conf/rspamd.conf.local create mode 100644 Stable2.0/conf/rspamd.conf.override create mode 100644 Stable2.0/hooks/build create mode 100644 Stable2.0/hooks/post_push create mode 100644 Stable2.0/rspamd.conf.local.override create mode 100644 Stable2.0/worker-controller.inc create mode 100644 Stable2.0/worker-proxy.inc diff --git a/Stable2.0/Dockerfile b/Stable2.0/Dockerfile new file mode 100644 index 0000000..f5135e8 --- /dev/null +++ b/Stable2.0/Dockerfile @@ -0,0 +1,60 @@ +FROM debian:stable-slim +LABEL maintainer="gnilebein - " + +# Set apt non-interactive +ENV DEBIAN_FRONTEND noninteractive + +# Install Rspamd +RUN set -x \ + && apt update \ + && apt --no-install-recommends install -y lsb-release wget gnupg openssl ca-certificates \ + && DEBIAN_CODE_NAME=`lsb_release -c -s` \ + && wget -O - https://rspamd.com/apt-stable/gpg.key | apt-key add - \ + && echo "deb http://rspamd.com/apt-stable/ $DEBIAN_CODE_NAME main" > /etc/apt/sources.list.d/rspamd.list \ + && apt purge -y lsb-release wget gnupg \ + && apt update \ + && apt --no-install-recommends install -y rspamd \ + && apt autoremove --purge -y \ + && apt clean \ + && rm -rf /var/lib/apt/lists/* + +# Override default settings +COPY rspamd.conf.local.override /etc/rspamd/ +COPY worker-controller.inc /etc/rspamd/override.d/ +COPY worker-proxy.inc /etc/rspamd/override.d/ + +# Keep database and configuration persistent +VOLUME /etc/rspamd/local.d +VOLUME /var/lib/rspamd + +# Port 11334 is for web frontend +# Port 11332 is for milter +# Port 11333 is for worker +EXPOSE 11332 11334 + +# Healtcheck if Rspamd is returning stats +HEALTHCHECK --interval=1m --timeout=5s --start-period=10s \ + CMD /usr/bin/rspamadm control stat || exit 1 + +# Run Rspamd +ENTRYPOINT ["/docker-entrypoint.sh"] +CMD ["/usr/bin/rspamd", "-f", "-u", "_rspamd", "-g", "_rspamd"] + +STOPSIGNAL SIGTERM + +# Setup Labels +ARG VERSION +ARG COMMIT +ARG BRANCH +ARG DATE + +LABEL org.label-schema.name="Rspamd" \ + org.label-schema.description="Rspamd Spam Filter - STABLE" \ + org.label-schema.usage="https://hub.docker.com/r/gnilebein/rspamd/" \ + org.label-schema.url="https://rspamd.com" \ + org.label-schema.vendor="gnilebein" \ + org.label-schema.schema-version="1.0" \ + org.label-schema.version=$VERSION \ + org.label-schema.vcs-url="https://github.com/rspamd/rspamd/" \ + org.label-schema.vcs-ref=$COMMIT \ + org.label-schema.build-date=$DATE \ diff --git a/Stable2.0/conf/custom/bad_asn.map b/Stable2.0/conf/custom/bad_asn.map new file mode 100644 index 0000000..a8d49cf --- /dev/null +++ b/Stable2.0/conf/custom/bad_asn.map @@ -0,0 +1,31 @@ +# High spam networks, disabled by default +# ASN SCORE DESC +# Remove comment to enable score +#12874 5 #Fastweb SpA, Italy +#12876 2 #ONLINE S.A.S, France +#13335 5 #Cloudflare Inc., United States +#14061 4 #DigitalOcean LLC, United States +#16276 2 #OVH SAS, France +#21100 2 #ITL LLC, Ukraine +#28753 5 #Leaseweb Deutschland GmbH, Germany +#29119 5 #ServiHosting Networks S.L., Spain +#29422 5 #Telia Inmics-Nebula Oy, Finland +#30823 3 #combahton GmbH, Germany +#31034 5 #Aruba S.p.A, Italy +#39364 4 #Hormoz IT & Network Waves Connection Co. (PJS), Iran +#42831 5 #UK Dedicated Servers Limited, United Kingdom +#43146 2 #Domain names registrar REG.RU Ltd, Russia +#44493 2 #Chelyabinsk-Signal LLC, Russia +#46606 2 #Unified Layer, United States +#49100 4 #Pishgaman Toseeh Ertebatat Company (Private Joint Stock), Iran +#49505 2 #OOO Network of data-centers Selectel, Russia +#53755 5 #Input Output Flood LLC, United States +#55293 4 #A2 Hosting Inc., United States +#61272 5 #Informacines sistemos ir technologijos - UAB, Lithuania +#62255 4 #Asmunda New Media Ltd., Seychelles +#63018 4 #Dedicated.com, United States +#197518 2 #Rackmarkt SL, Spain +#197695 2 #Domain names registrar REG.RU Ltd, Russia +#198068 2 #P.A.G.M. OU, Estonia +#201942 5 #Soltia Consulting SL, Spain +#213373 4 #IP Connect Inc \ No newline at end of file diff --git a/Stable2.0/conf/custom/bad_header.map b/Stable2.0/conf/custom/bad_header.map new file mode 100644 index 0000000..839c3c3 --- /dev/null +++ b/Stable2.0/conf/custom/bad_header.map @@ -0,0 +1,2 @@ +/Thread-Topic:\s[a-zA-Z]{3}\s[a-zA-Z]{2}[\s\r\n]{0,1}[^a-zA-Z0-9][\r\n]/i +/Thread-Topic:\s[a-zA-Z]{3}\s[a-zA-Z]{2}\s[a-zA-Z]{1}\s[a-zA-Z]{5}[\s\r\n]{0,1}[^a-zA-Z0-9][\r\n]/i diff --git a/Stable2.0/conf/custom/bad_languages.map b/Stable2.0/conf/custom/bad_languages.map new file mode 100644 index 0000000..cf9ce3e --- /dev/null +++ b/Stable2.0/conf/custom/bad_languages.map @@ -0,0 +1 @@ +# Regex! /de/ will also match /de_at/ etc. diff --git a/Stable2.0/conf/custom/bad_words.map b/Stable2.0/conf/custom/bad_words.map new file mode 100644 index 0000000..0d9af8b --- /dev/null +++ b/Stable2.0/conf/custom/bad_words.map @@ -0,0 +1,29 @@ +/\serotic\s/i +/\serection\s/i +/\ssexy\s/i +/\sass\s/i +/\sviagra\s/i +/\stits\s/i +/\stitty\s/i +/\stitties\s/i +/\scum\s/i +/\ssperm\s/i +/\sslut\s/i +/\sporn\s/i +/\scock\s/i +/\spharma\s/i +/\spharmacy\s/i +/\sseo\s/i +/\sjackpot\s/i +/\slottery\s/i +/bitcoin/i +/trojaner/i +/malware/i +/\sscooter\s/i +/testost/i +/web\sdevelopment/i +/\slottery\s/i +/\ssex\s/i +/\svagina\s/i +/\spenis\s/i +/\smarketing\s/i \ No newline at end of file diff --git a/Stable2.0/conf/custom/bad_words_de.map b/Stable2.0/conf/custom/bad_words_de.map new file mode 100644 index 0000000..ccdd586 --- /dev/null +++ b/Stable2.0/conf/custom/bad_words_de.map @@ -0,0 +1,17 @@ +/\slotto\s/i +/pillenversand/i +/\skredithilfe\s/i +/\skapital\s/i +/\skrankenversicherung\s/i +/pƤdophil/i +/paedophil/i +/freiberufler/i +/unternehmer/i +/masturbieren/i +/\sescooter\s/i +/\se-scooter\s/i +/testost/i +/\spotenz\s/i +/potenzmittel/i +/rezeptfrei/i +/apotheke/i \ No newline at end of file diff --git a/Stable2.0/conf/custom/bulk_header.map b/Stable2.0/conf/custom/bulk_header.map new file mode 100644 index 0000000..69a20af --- /dev/null +++ b/Stable2.0/conf/custom/bulk_header.map @@ -0,0 +1,19 @@ +/X-EMV-Platform; .*/i +/.*nur-1-click.*/i +/.*episerver.*/i +/.*supergewinne.*/i +/List-Unsubscribe.*nbps\.eu/i +/.*regiofinder.*/i +/.*EmailSocket.*/i +/List-Unsubscribe:.*respread.*/i +/.*greenflamingo.*/i +/.*senderemailglobal.*/i +/.*promio\.net.*/i +/.*promio\.de.*/i +/.*mailer-service\.com.*/i +/.*mailer-service\.de.*/i +/.*dynamic-lht.*/i +/.*light-house-traffic.*/i +/.*newsletterplus.*/i +/.*X-Chpo.*/i +/.*List-Unsubscribe:.*@nl\..*/i diff --git a/Stable2.0/conf/custom/fishy_tlds.map b/Stable2.0/conf/custom/fishy_tlds.map new file mode 100644 index 0000000..1b8b2b0 --- /dev/null +++ b/Stable2.0/conf/custom/fishy_tlds.map @@ -0,0 +1,65 @@ +/.+\.accountant$/i +/.+\.art$/i +/.+\.asia$/i +/.+\.bid$/i +/.+\.biz$/i +/.+\.care$/i +/.+\.cf$/i +/.+\.click$/i +/.+\.cloud$/i +/.+\.co$/i +/.+\.construction$/i +/.+\.country$/i +/.+\.cricket$/i +/.+\.date$/i +/.+\.desi$/i +/.+\.download$/i +/.+\.estate$/i +/.+\.faith$/i +/.+\.fit$/i +/.+\.flights$/i +/.+\.ga$/i +/.+\.gdn$/i +/.+\.gq$/i +/.+\.guru$/i +/.+\.icu$/i +/.+\.id$/i +/.+\.info$/i +/.+\.in.net$/i +/.+\.ir$/i +/.+\.jetzt$/i +/.+\.kim$/i +/.+\.life$/i +/.+\.link$/i +/.+\.loan$/i +/.+\.mk$/i +/.+\.ml$/i +/.+\.ninja$/i +/.+\.online$/i +/.+\.ooo$/i +/.+\.party$/i +/.+\.pro$/i +/.+\.ps$/i +/.+\.pw$/i +/.+\.racing$/i +/.+\.review$/i +/.+\.rocks$/i +/.+\.ryukyu$/i +/.+\.science$/i +/.+\.site$/i +/.+\.space$/i +/.+\.stream$/i +/.+\.sucks$/i +/.+\.tk$/i +/.+\.top$/i +/.+\.topica\.com$/i +/.+\.town$/i +/.+\.trade$/i +/.+\.uno$/i +/.+\.vip$/i +/.+\.webcam$/i +/.+\.website$/i +/.+\.win$/i +/.+\.work$/i +/.+\.world$/i +/.+\.xyz$/i diff --git a/Stable2.0/conf/custom/global_mime_from_blacklist.map b/Stable2.0/conf/custom/global_mime_from_blacklist.map new file mode 100644 index 0000000..3c87288 --- /dev/null +++ b/Stable2.0/conf/custom/global_mime_from_blacklist.map @@ -0,0 +1 @@ +# /.+example\.com/i diff --git a/Stable2.0/conf/custom/global_mime_from_whitelist.map b/Stable2.0/conf/custom/global_mime_from_whitelist.map new file mode 100644 index 0000000..3c87288 --- /dev/null +++ b/Stable2.0/conf/custom/global_mime_from_whitelist.map @@ -0,0 +1 @@ +# /.+example\.com/i diff --git a/Stable2.0/conf/custom/global_rcpt_blacklist.map b/Stable2.0/conf/custom/global_rcpt_blacklist.map new file mode 100644 index 0000000..3c87288 --- /dev/null +++ b/Stable2.0/conf/custom/global_rcpt_blacklist.map @@ -0,0 +1 @@ +# /.+example\.com/i diff --git a/Stable2.0/conf/custom/global_rcpt_whitelist.map b/Stable2.0/conf/custom/global_rcpt_whitelist.map new file mode 100644 index 0000000..3c87288 --- /dev/null +++ b/Stable2.0/conf/custom/global_rcpt_whitelist.map @@ -0,0 +1 @@ +# /.+example\.com/i diff --git a/Stable2.0/conf/custom/global_smtp_from_blacklist.map b/Stable2.0/conf/custom/global_smtp_from_blacklist.map new file mode 100644 index 0000000..3c87288 --- /dev/null +++ b/Stable2.0/conf/custom/global_smtp_from_blacklist.map @@ -0,0 +1 @@ +# /.+example\.com/i diff --git a/Stable2.0/conf/custom/global_smtp_from_whitelist.map b/Stable2.0/conf/custom/global_smtp_from_whitelist.map new file mode 100644 index 0000000..3c87288 --- /dev/null +++ b/Stable2.0/conf/custom/global_smtp_from_whitelist.map @@ -0,0 +1 @@ +# /.+example\.com/i diff --git a/Stable2.0/conf/custom/ip_wl.map b/Stable2.0/conf/custom/ip_wl.map new file mode 100644 index 0000000..c8bb552 --- /dev/null +++ b/Stable2.0/conf/custom/ip_wl.map @@ -0,0 +1,4 @@ +# IP whitelist +# 127.0.0.1 +# 1.2.3.4 +# ... diff --git a/Stable2.0/conf/custom/monitoring_nolog.map b/Stable2.0/conf/custom/monitoring_nolog.map new file mode 100644 index 0000000..0e00de7 --- /dev/null +++ b/Stable2.0/conf/custom/monitoring_nolog.map @@ -0,0 +1,7 @@ +# Skip logging for these addresses +/monitoring-system@everycloudtech\.us/i +/monitor@tools\.mailflowmonitoring\.com/i +/watchdog@localhost/i +/supertool@mxtoolbox\.com/i +/test@mxtoolboxsmtpdiag\.com/i +/open-relay-check@mailcow\.email/i diff --git a/Stable2.0/conf/dynmaps/aliasexp.php b/Stable2.0/conf/dynmaps/aliasexp.php new file mode 100644 index 0000000..2d75614 --- /dev/null +++ b/Stable2.0/conf/dynmaps/aliasexp.php @@ -0,0 +1,174 @@ + PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => false, +]; +try { + $pdo = new PDO($dsn, $database_user, $database_pass, $opt); +} +catch (PDOException $e) { + error_log("ALIASEXP: " . $e . PHP_EOL); + http_response_code(501); + exit; +} + +// Init Redis +$redis = new Redis(); +$redis->connect('redis-mailcow', 6379); + +function parse_email($email) { + if(!filter_var($email, FILTER_VALIDATE_EMAIL)) return false; + $a = strrpos($email, '@'); + return array('local' => substr($email, 0, $a), 'domain' => substr(substr($email, $a), 1)); +} +if (!function_exists('getallheaders')) { + function getallheaders() { + if (!is_array($_SERVER)) { + return array(); + } + $headers = array(); + foreach ($_SERVER as $name => $value) { + if (substr($name, 0, 5) == 'HTTP_') { + $headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value; + } + } + return $headers; + } +} + +// Read headers +$headers = getallheaders(); +// Get rcpt +$rcpt = $headers['Rcpt']; +// Remove tag +$rcpt = preg_replace('/^(.*?)\+.*(@.*)$/', '$1$2', $rcpt); +// Parse email address +$parsed_rcpt = parse_email($rcpt); +// Create array of final mailboxes +$rcpt_final_mailboxes = array(); + +// Skip if not a mailcow handled domain +try { + if (!$redis->hGet('DOMAIN_MAP', $parsed_rcpt['domain'])) { + exit; + } +} +catch (RedisException $e) { + error_log("ALIASEXP: " . $e . PHP_EOL); + http_response_code(504); + exit; +} + +// Always assume rcpt is not a final mailbox but an alias for a mailbox or further aliases +// +// rcpt +// | +// mailbox <-- goto ---> alias1, alias2, mailbox2 +// | | +// mailbox3 | +// | +// alias3 ---> mailbox4 +// +try { + $stmt = $pdo->prepare("SELECT `goto` FROM `alias` WHERE `address` = :rcpt AND `active` = '1'"); + $stmt->execute(array( + ':rcpt' => $rcpt + )); + $gotos = $stmt->fetch(PDO::FETCH_ASSOC)['goto']; + if (empty($gotos)) { + $stmt = $pdo->prepare("SELECT `goto` FROM `alias` WHERE `address` = :rcpt AND `active` = '1'"); + $stmt->execute(array( + ':rcpt' => '@' . $parsed_rcpt['domain'] + )); + $gotos = $stmt->fetch(PDO::FETCH_ASSOC)['goto']; + } + if (empty($gotos)) { + $stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :rcpt AND `active` = '1'"); + $stmt->execute(array(':rcpt' => $parsed_rcpt['domain'])); + $goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['target_domain']; + if ($goto_branch) { + $gotos = $parsed_rcpt['local'] . '@' . $goto_branch; + } + } + $gotos_array = explode(',', $gotos); + + $loop_c = 0; + + while (count($gotos_array) != 0 && $loop_c <= 20) { + + // Loop through all found gotos + foreach ($gotos_array as $index => &$goto) { + error_log("ALIAS EXPANDER: http pipe: query " . $goto . " as username from mailbox" . PHP_EOL); + $stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `username` = :goto AND (`active`= '1' OR `active`= '2');"); + $stmt->execute(array(':goto' => $goto)); + $username = $stmt->fetch(PDO::FETCH_ASSOC)['username']; + if (!empty($username)) { + error_log("ALIAS EXPANDER: http pipe: mailbox found: " . $username . PHP_EOL); + // Current goto is a mailbox, save to rcpt_final_mailboxes if not a duplicate + if (!in_array($username, $rcpt_final_mailboxes)) { + $rcpt_final_mailboxes[] = $username; + } + } + else { + $parsed_goto = parse_email($goto); + if (!$redis->hGet('DOMAIN_MAP', $parsed_goto['domain'])) { + error_log("ALIAS EXPANDER:" . $goto . " is not a mailcow handled mailbox or alias address" . PHP_EOL); + } + else { + $stmt = $pdo->prepare("SELECT `goto` FROM `alias` WHERE `address` = :goto AND `active` = '1'"); + $stmt->execute(array(':goto' => $goto)); + $goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['goto']; + if ($goto_branch) { + error_log("ALIAS EXPANDER: http pipe: goto address " . $goto . " is an alias branch for " . $goto_branch . PHP_EOL); + $goto_branch_array = explode(',', $goto_branch); + } else { + $stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain AND `active` AND '1'"); + $stmt->execute(array(':domain' => $parsed_goto['domain'])); + $goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['target_domain']; + if ($goto_branch) { + error_log("ALIAS EXPANDER: http pipe: goto domain " . $parsed_goto['domain'] . " is a domain alias branch for " . $goto_branch . PHP_EOL); + $goto_branch_array = array($parsed_goto['local'] . '@' . $goto_branch); + } + } + } + } + // goto item was processed, unset + unset($gotos_array[$index]); + } + + // Merge goto branch array derived from previous loop (if any), filter duplicates and unset goto branch array + if (!empty($goto_branch_array)) { + $gotos_array = array_unique(array_merge($gotos_array, $goto_branch_array)); + unset($goto_branch_array); + } + + // Reindex array + $gotos_array = array_values($gotos_array); + + // Force exit if loop cannot be solved + // Postfix does not allow for alias loops, so this should never happen. + $loop_c++; + error_log("ALIAS EXPANDER: http pipe: goto array count on loop #". $loop_c . " is " . count($gotos_array) . PHP_EOL); + } +} +catch (PDOException $e) { + error_log("ALIAS EXPANDER: " . $e->getMessage() . PHP_EOL); + http_response_code(502); + exit; +} + +// Does also return the mailbox name if question == answer (query == mailbox) +if (count($rcpt_final_mailboxes) == 1) { + error_log("ALIASEXP: direct alias " . $rcpt . " expanded to " . $rcpt_final_mailboxes[0] . PHP_EOL); + echo trim($rcpt_final_mailboxes[0]); +} diff --git a/Stable2.0/conf/dynmaps/bcc.php b/Stable2.0/conf/dynmaps/bcc.php new file mode 100644 index 0000000..87f91ca --- /dev/null +++ b/Stable2.0/conf/dynmaps/bcc.php @@ -0,0 +1,88 @@ + PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => false, +]; +try { + $pdo = new PDO($dsn, $database_user, $database_pass, $opt); +} +catch (PDOException $e) { + error_log("BCC MAP SQL ERROR: " . $e . PHP_EOL); + http_response_code(501); + exit; +} + +function parse_email($email) { + if(!filter_var($email, FILTER_VALIDATE_EMAIL)) return false; + $a = strrpos($email, '@'); + return array('local' => substr($email, 0, $a), 'domain' => substr(substr($email, $a), 1)); +} +if (!function_exists('getallheaders')) { + function getallheaders() { + if (!is_array($_SERVER)) { + return array(); + } + $headers = array(); + foreach ($_SERVER as $name => $value) { + if (substr($name, 0, 5) == 'HTTP_') { + $headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value; + } + } + return $headers; + } +} + +// Read headers +$headers = getallheaders(); +// Get rcpt +$rcpt = $headers['Rcpt']; +// Get from +$from = $headers['From']; +// Remove tags +$rcpt = preg_replace('/^(.*?)\+.*(@.*)$/', '$1$2', $rcpt); +$from = preg_replace('/^(.*?)\+.*(@.*)$/', '$1$2', $from); + +try { + if (!empty($rcpt)) { + $stmt = $pdo->prepare("SELECT `bcc_dest` FROM `bcc_maps` WHERE `type` = 'rcpt' AND `local_dest` = :local_dest AND `active` = '1'"); + $stmt->execute(array( + ':local_dest' => $rcpt + )); + $bcc_dest = $stmt->fetch(PDO::FETCH_ASSOC)['bcc_dest']; + if (!empty($bcc_dest) && filter_var($bcc_dest, FILTER_VALIDATE_EMAIL)) { + error_log("BCC MAP: returning ". $bcc_dest . " for " . $rcpt . PHP_EOL); + http_response_code(201); + echo trim($bcc_dest); + exit; + } + } + if (!empty($from)) { + $stmt = $pdo->prepare("SELECT `bcc_dest` FROM `bcc_maps` WHERE `type` = 'sender' AND `local_dest` = :local_dest AND `active` = '1'"); + $stmt->execute(array( + ':local_dest' => $from + )); + $bcc_dest = $stmt->fetch(PDO::FETCH_ASSOC)['bcc_dest']; + if (!empty($bcc_dest) && filter_var($bcc_dest, FILTER_VALIDATE_EMAIL)) { + error_log("BCC MAP: returning ". $bcc_dest . " for " . $from . PHP_EOL); + http_response_code(201); + echo trim($bcc_dest); + exit; + } + } +} +catch (PDOException $e) { + error_log("BCC MAP SQL ERROR: " . $e->getMessage() . PHP_EOL); + http_response_code(502); + exit; +} + diff --git a/Stable2.0/conf/dynmaps/footer.php b/Stable2.0/conf/dynmaps/footer.php new file mode 100644 index 0000000..545c45e --- /dev/null +++ b/Stable2.0/conf/dynmaps/footer.php @@ -0,0 +1,113 @@ + PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => false, +]; +try { + $pdo = new PDO($dsn, $database_user, $database_pass, $opt); +} +catch (PDOException $e) { + error_log("FOOTER: " . $e . PHP_EOL); + http_response_code(501); + exit; +} + +if (!function_exists('getallheaders')) { + function getallheaders() { + if (!is_array($_SERVER)) { + return array(); + } + $headers = array(); + foreach ($_SERVER as $name => $value) { + if (substr($name, 0, 5) == 'HTTP_') { + $headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value; + } + } + return $headers; + } +} + +// Read headers +$headers = getallheaders(); +// Get Domain +$domain = $headers['Domain']; +// Get Username +$username = $headers['Username']; +// Get From +$from = $headers['From']; +// define empty footer +$empty_footer = json_encode(array( + 'html' => '', + 'plain' => '', + 'skip_replies' => 0, + 'vars' => array() +)); + +error_log("FOOTER: checking for domain " . $domain . ", user " . $username . " and address " . $from . PHP_EOL); + +try { + // try get $target_domain if $domain is an alias_domain + $stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` + WHERE `alias_domain` = :alias_domain"); + $stmt->execute(array( + ':alias_domain' => $domain + )); + $alias_domain = $stmt->fetch(PDO::FETCH_ASSOC); + if (!$alias_domain) { + $target_domain = $domain; + } else { + $target_domain = $alias_domain['target_domain']; + } + + // get footer associated with the domain + $stmt = $pdo->prepare("SELECT `plain`, `html`, `mbox_exclude`, `alias_domain_exclude`, `skip_replies` FROM `domain_wide_footer` + WHERE `domain` = :domain"); + $stmt->execute(array( + ':domain' => $target_domain + )); + $footer = $stmt->fetch(PDO::FETCH_ASSOC); + + // check if the sender is excluded + if (in_array($from, json_decode($footer['mbox_exclude']))){ + $footer = false; + } + if (in_array($domain, json_decode($footer['alias_domain_exclude']))){ + $footer = false; + } + if (empty($footer)){ + echo $empty_footer; + exit; + } + error_log("FOOTER: " . json_encode($footer) . PHP_EOL); + + // footer will be applied + // get custom mailbox attributes to insert into the footer + $stmt = $pdo->prepare("SELECT `custom_attributes` FROM `mailbox` WHERE `username` = :username"); + $stmt->execute(array( + ':username' => $username + )); + $custom_attributes = $stmt->fetch(PDO::FETCH_ASSOC)['custom_attributes']; + if (empty($custom_attributes)){ + $custom_attributes = (object)array(); + } +} +catch (Exception $e) { + error_log("FOOTER: " . $e->getMessage() . PHP_EOL); + http_response_code(502); + exit; +} + + +// return footer +$footer["vars"] = $custom_attributes; +echo json_encode($footer); diff --git a/Stable2.0/conf/dynmaps/forwardinghosts.php b/Stable2.0/conf/dynmaps/forwardinghosts.php new file mode 100644 index 0000000..10285b7 --- /dev/null +++ b/Stable2.0/conf/dynmaps/forwardinghosts.php @@ -0,0 +1,57 @@ +connect('redis-mailcow', 6379); + +function in_net($addr, $net) { + $net = explode('/', $net); + if (count($net) > 1) { + $mask = $net[1]; + } + $net = inet_pton($net[0]); + $addr = inet_pton($addr); + $length = strlen($net); // 4 for IPv4, 16 for IPv6 + if (strlen($net) != strlen($addr)) { + return false; + } + if (!isset($mask)) { + $mask = $length * 8; + } + $addr_bin = ''; + $net_bin = ''; + for ($i = 0; $i < $length; ++$i) { + $addr_bin .= str_pad(decbin(ord(substr($addr, $i, $i+1))), 8, '0', STR_PAD_LEFT); + $net_bin .= str_pad(decbin(ord(substr($net, $i, $i+1))), 8, '0', STR_PAD_LEFT); + } + return substr($addr_bin, 0, $mask) == substr($net_bin, 0, $mask); +} + +if (isset($_GET['host'])) { + try { + foreach ($redis->hGetAll('WHITELISTED_FWD_HOST') as $host => $source) { + if (in_net($_GET['host'], $host)) { + echo '200 PERMIT'; + exit; + } + } + echo '200 DUNNO'; + } + catch (RedisException $e) { + echo '200 DUNNO'; + exit; + } +} else { + try { + echo '240.240.240.240' . PHP_EOL; + foreach ($redis->hGetAll('WHITELISTED_FWD_HOST') as $host => $source) { + echo $host . PHP_EOL; + } + } + catch (RedisException $e) { + echo '240.240.240.240' . PHP_EOL; + exit; + } +} +?> diff --git a/Stable2.0/conf/dynmaps/index.html b/Stable2.0/conf/dynmaps/index.html new file mode 100644 index 0000000..90531a4 --- /dev/null +++ b/Stable2.0/conf/dynmaps/index.html @@ -0,0 +1,2 @@ + + diff --git a/Stable2.0/conf/dynmaps/sasl_logs.php b/Stable2.0/conf/dynmaps/sasl_logs.php new file mode 100644 index 0000000..2d4cbe6 --- /dev/null +++ b/Stable2.0/conf/dynmaps/sasl_logs.php @@ -0,0 +1,2 @@ + PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => false, +]; +try { + $pdo = new PDO($dsn, $database_user, $database_pass, $opt); + $stmt = $pdo->query("SELECT '1' FROM `filterconf`"); +} +catch (PDOException $e) { + echo 'settings { }'; + exit; +} + +// Check if db changed and return header +$stmt = $pdo->prepare("SELECT GREATEST(COALESCE(MAX(UNIX_TIMESTAMP(UPDATE_TIME)), 1), COALESCE(MAX(UNIX_TIMESTAMP(CREATE_TIME)), 1)) AS `db_update_time` FROM `information_schema`.`tables` + WHERE (`TABLE_NAME` = 'filterconf' OR `TABLE_NAME` = 'settingsmap' OR `TABLE_NAME` = 'sogo_quick_contact' OR `TABLE_NAME` = 'alias') + AND TABLE_SCHEMA = :dbname;"); +$stmt->execute(array( + ':dbname' => $database_name +)); +$db_update_time = $stmt->fetch(PDO::FETCH_ASSOC)['db_update_time']; +if (empty($db_update_time)) { + $db_update_time = 1572048000; +} +if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && (strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) == $db_update_time)) { + header('Last-Modified: '.gmdate('D, d M Y H:i:s', $db_update_time).' GMT', true, 304); + exit; +} else { + header('Last-Modified: '.gmdate('D, d M Y H:i:s', $db_update_time).' GMT', true, 200); +} + +function parse_email($email) { + if (!filter_var($email, FILTER_VALIDATE_EMAIL)) return false; + $a = strrpos($email, '@'); + return array('local' => substr($email, 0, $a), 'domain' => substr($email, $a)); +} + +function normalize_email($email) { + $email = strtolower(str_replace('/', '\/', $email)); + $gm = "@gmail.com"; + if (substr_compare($email, $gm, -strlen($gm)) == 0) { + $email = explode('@', $email); + $email[0] = str_replace('.', '', $email[0]); + $email = implode('@', $email); + } + $gm_alt = "@googlemail.com"; + if (substr_compare($email, $gm_alt, -strlen($gm_alt)) == 0) { + $email = explode('@', $email); + $email[0] = str_replace('.', '', $email[0]); + $email[1] = str_replace('@', '', $gm); + $email = implode('@', $email); + } + if (str_contains($email, "+")) { + $email = explode('@', $email); + $user = explode('+', $email[0]); + $email[0] = $user[0]; + $email = implode('@', $email); + } + return $email; +} + +function wl_by_sogo() { + global $pdo; + $rcpt = array(); + $stmt = $pdo->query("SELECT DISTINCT(`sogo_folder_info`.`c_path2`) AS `user`, GROUP_CONCAT(`sogo_quick_contact`.`c_mail`) AS `contacts` FROM `sogo_folder_info` + INNER JOIN `sogo_quick_contact` ON `sogo_quick_contact`.`c_folder_id` = `sogo_folder_info`.`c_folder_id` + GROUP BY `c_path2`"); + $sogo_contacts = $stmt->fetchAll(PDO::FETCH_ASSOC); + while ($row = array_shift($sogo_contacts)) { + foreach (explode(',', $row['contacts']) as $contact) { + if (!filter_var($contact, FILTER_VALIDATE_EMAIL)) { + continue; + } + // Explicit from, no mime_from, no regex - envelope must match + // mailcow white and blacklists also cover mime_from + $rcpt[$row['user']][] = normalize_email($contact); + } + } + return $rcpt; +} + +function ucl_rcpts($object, $type) { + global $pdo; + $rcpt = array(); + if ($type == 'mailbox') { + // Standard aliases + $stmt = $pdo->prepare("SELECT `address` FROM `alias` + WHERE `goto` = :object_goto + AND `address` NOT LIKE '@%'"); + $stmt->execute(array( + ':object_goto' => $object + )); + $standard_aliases = $stmt->fetchAll(PDO::FETCH_ASSOC); + while ($row = array_shift($standard_aliases)) { + $local = parse_email($row['address'])['local']; + $domain = parse_email($row['address'])['domain']; + if (!empty($local) && !empty($domain)) { + $rcpt[] = '/^' . str_replace('/', '\/', $local) . '[+].*' . str_replace('/', '\/', $domain) . '$/i'; + } + $rcpt[] = str_replace('/', '\/', $row['address']); + } + // Aliases by alias domains + $stmt = $pdo->prepare("SELECT CONCAT(`local_part`, '@', `alias_domain`.`alias_domain`) AS `alias` FROM `mailbox` + LEFT OUTER JOIN `alias_domain` ON `mailbox`.`domain` = `alias_domain`.`target_domain` + WHERE `mailbox`.`username` = :object"); + $stmt->execute(array( + ':object' => $object + )); + $by_domain_aliases = $stmt->fetchAll(PDO::FETCH_ASSOC); + array_filter($by_domain_aliases); + while ($row = array_shift($by_domain_aliases)) { + if (!empty($row['alias'])) { + $local = parse_email($row['alias'])['local']; + $domain = parse_email($row['alias'])['domain']; + if (!empty($local) && !empty($domain)) { + $rcpt[] = '/^' . str_replace('/', '\/', $local) . '[+].*' . str_replace('/', '\/', $domain) . '$/i'; + } + $rcpt[] = str_replace('/', '\/', $row['alias']); + } + } + } + elseif ($type == 'domain') { + // Domain self + $rcpt[] = '/.*@' . $object . '/i'; + $stmt = $pdo->prepare("SELECT `alias_domain` FROM `alias_domain` + WHERE `target_domain` = :object"); + $stmt->execute(array(':object' => $object)); + $alias_domains = $stmt->fetchAll(PDO::FETCH_ASSOC); + array_filter($alias_domains); + while ($row = array_shift($alias_domains)) { + $rcpt[] = '/.*@' . $row['alias_domain'] . '/i'; + } + } + return $rcpt; +} +?> +settings { + watchdog { + priority = 10; + rcpt_mime = "/null@localhost/i"; + from_mime = "/watchdog@localhost/i"; + apply "default" { + symbols_disabled = ["HISTORY_SAVE", "ARC", "ARC_SIGNED", "DKIM", "DKIM_SIGNED", "CLAM_VIRUS"]; + want_spam = yes; + actions { + reject = 9999.0; + greylist = 9998.0; + "add header" = 9997.0; + } + + } + } +query("SELECT DISTINCT `object` FROM `filterconf` WHERE `option` = 'highspamlevel' OR `option` = 'lowspamlevel'"); +$rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + +while ($row = array_shift($rows)) { + $username_sane = preg_replace("/[^a-zA-Z0-9]+/", "", $row['object']); +?> + score_ { + priority = 4; + + rcpt = ; +prepare("SELECT `option`, `value` FROM `filterconf` + WHERE (`option` = 'highspamlevel' OR `option` = 'lowspamlevel') + AND `object`= :object"); + $stmt->execute(array(':object' => $row['object'])); + $spamscore = $stmt->fetchAll(PDO::FETCH_COLUMN|PDO::FETCH_GROUP); +?> + apply "default" { + actions { + reject = ; + greylist = ; + "add header" = ; + } + } + } + $contacts) { + $username_sane = preg_replace("/[^a-zA-Z0-9]+/", "", $user); +?> + whitelist_sogo_ { + + from = ; + + priority = 4; + + rcpt = ; + + apply "default" { + SOGO_CONTACT = -99.0; + } + symbols [ + "SOGO_CONTACT" + ] + } +query("SELECT DISTINCT `object` FROM `filterconf` WHERE `option` = 'whitelist_from'"); +$rows = $stmt->fetchAll(PDO::FETCH_ASSOC); +while ($row = array_shift($rows)) { + $username_sane = preg_replace("/[^a-zA-Z0-9]+/", "", $row['object']); +?> + whitelist_ { +prepare("SELECT `value` FROM `filterconf` + WHERE `object`= :object + AND `option` = 'whitelist_from'"); + $stmt->execute(array(':object' => $row['object'])); + $list_items = $stmt->fetchAll(PDO::FETCH_ASSOC); + foreach ($list_items as $item) { +?> + from = "//i"; + + priority = 5; + + rcpt = ; + + priority = 6; + + rcpt = ; + + apply "default" { + MAILCOW_WHITE = -999.0; + } + symbols [ + "MAILCOW_WHITE" + ] + } + whitelist_mime_ { + + from_mime = "//i"; + + priority = 5; + + rcpt = ; + + priority = 6; + + rcpt = ; + + apply "default" { + MAILCOW_WHITE = -999.0; + } + symbols [ + "MAILCOW_WHITE" + ] + } +query("SELECT DISTINCT `object` FROM `filterconf` WHERE `option` = 'blacklist_from'"); +$rows = $stmt->fetchAll(PDO::FETCH_ASSOC); +while ($row = array_shift($rows)) { + $username_sane = preg_replace("/[^a-zA-Z0-9]+/", "", $row['object']); +?> + blacklist_ { +prepare("SELECT `value` FROM `filterconf` + WHERE `object`= :object + AND `option` = 'blacklist_from'"); + $stmt->execute(array(':object' => $row['object'])); + $list_items = $stmt->fetchAll(PDO::FETCH_ASSOC); + foreach ($list_items as $item) { +?> + from = "//i"; + + priority = 5; + + rcpt = ; + + priority = 6; + + rcpt = ; + + apply "default" { + MAILCOW_BLACK = 999.0; + } + symbols [ + "MAILCOW_BLACK" + ] + } + blacklist_header_ { + + from_mime = "//i"; + + priority = 5; + + rcpt = ; + + priority = 6; + + rcpt = ; + + apply "default" { + MAILCOW_BLACK = 999.0; + } + symbols [ + "MAILCOW_BLACK" + ] + } + + ham_trap { + + rcpt = ; + + priority = 9; + apply "default" { + symbols_enabled = ["HISTORY_SAVE"]; + } + symbols [ + "HAM_TRAP" + ] + } + + spam_trap { + + rcpt = ; + + priority = 9; + apply "default" { + symbols_enabled = ["HISTORY_SAVE"]; + } + symbols [ + "SPAM_TRAP" + ] + } +query("SELECT `id`, `content` FROM `settingsmap` WHERE `active` = '1'"); +$rows = $stmt->fetchAll(PDO::FETCH_ASSOC); +while ($row = array_shift($rows)) { + $username_sane = preg_replace("/[^a-zA-Z0-9]+/", "", $row['id']); +?> + additional_settings_ { + + } + +} diff --git a/Stable2.0/conf/dynmaps/vars.inc.php b/Stable2.0/conf/dynmaps/vars.inc.php new file mode 100644 index 0000000..79566b0 --- /dev/null +++ b/Stable2.0/conf/dynmaps/vars.inc.php @@ -0,0 +1,6 @@ + diff --git a/Stable2.0/conf/local.d/actions.conf b/Stable2.0/conf/local.d/actions.conf new file mode 100644 index 0000000..3de63a5 --- /dev/null +++ b/Stable2.0/conf/local.d/actions.conf @@ -0,0 +1,3 @@ +reject = 15; +add_header = 8; +greylist = 7; diff --git a/Stable2.0/conf/local.d/antivirus.conf b/Stable2.0/conf/local.d/antivirus.conf new file mode 100644 index 0000000..c8d31d1 --- /dev/null +++ b/Stable2.0/conf/local.d/antivirus.conf @@ -0,0 +1,11 @@ +clamav { + # Scan whole message + scan_mime_parts = false; + #scan_text_mime = true; + #scan_image_mime = true; + symbol = "CLAM_VIRUS"; + type = "clamav"; + log_clean = true; + servers = "clamd:3310"; + max_size = 20971520; +} diff --git a/Stable2.0/conf/local.d/arc.conf b/Stable2.0/conf/local.d/arc.conf new file mode 100644 index 0000000..a857fc4 --- /dev/null +++ b/Stable2.0/conf/local.d/arc.conf @@ -0,0 +1,32 @@ +# If false, messages with empty envelope from are not signed +allow_envfrom_empty = true; +# If true, envelope/header domain mismatch is ignored +allow_hdrfrom_mismatch = true; +# If true, multiple from headers are allowed (but only first is used) +allow_hdrfrom_multiple = false; +# If true, username does not need to contain matching domain +allow_username_mismatch = false; +# If false, messages from authenticated users are not selected for signing +sign_authenticated = false; +# Default path to key, can include '$domain' and '$selector' variables +path = "/data/dkim/keys/$domain.dkim"; +# Default selector to use +selector = "dkim"; +# If false, messages from local networks are not selected for signing +sign_local = false; +# Symbol to add when message is signed +symbol = "ARC_SIGNED"; +# Whether to fallback to global config +try_fallback = true; +# Domain to use for DKIM signing: can be "header" or "envelope" +use_domain = "recipient"; +# Whether to normalise domains to eSLD +use_esld = false; +# Whether to get keys from Redis +use_redis = true; +# Hash for DKIM keys in Redis +key_prefix = "DKIM_PRIV_KEYS"; +# Selector map +selector_prefix = "DKIM_SELECTORS"; +sign_inbound = true; +use_domain_sign_inbound = "recipient"; diff --git a/Stable2.0/conf/local.d/asn.conf b/Stable2.0/conf/local.d/asn.conf new file mode 100644 index 0000000..42b6780 --- /dev/null +++ b/Stable2.0/conf/local.d/asn.conf @@ -0,0 +1,6 @@ +provider_type = "rspamd"; +provider_info { + ip4 = "asn.rspamd.com"; + ip6 = "asn6.rspamd.com"; +} +symbol = "ASN"; diff --git a/Stable2.0/conf/local.d/composites.conf b/Stable2.0/conf/local.d/composites.conf new file mode 100644 index 0000000..9bb8442 --- /dev/null +++ b/Stable2.0/conf/local.d/composites.conf @@ -0,0 +1,110 @@ +MX_IMPLICIT { + expression = "MX_GOOD & MX_MISSING"; + score = -0.01; +} +VIRUS_FOUND { + expression = "CLAM_VIRUS & !MAILCOW_WHITE"; + score = 2000.0; +} +# Bad policy from free mail providers +FREEMAIL_POLICY_FAILURE { + expression = "FREEMAIL_FROM & !DMARC_POLICY_ALLOW & !MAILLIST& !WHITELISTED_FWD_HOST & -g+:policies"; + score = 16.0; +} +# Applies to freemail with undisclosed recipients +FREEMAIL_TO_UNDISC_RCPT { + expression = "FREEMAIL_FROM & ( MISSING_TO | R_UNDISC_RCPT | TO_EQ_FROM )"; + score = 5.0; +} +# Bad policy from non-whitelisted senders +# Remove SOGO_CONTACT symbol for fwd hosts and senders with broken policy +SOGO_CONTACT_EXCLUDE { + expression = "(-WHITELISTED_FWD_HOST | -g+:policies) & ^SOGO_CONTACT & !DMARC_POLICY_ALLOW"; +} +# Remove MAILCOW_WHITE symbol for senders with broken policy recieved not from fwd hosts +MAILCOW_WHITE_EXCLUDE { + expression = "^MAILCOW_WHITE & (-DMARC_POLICY_REJECT | -DMARC_POLICY_QUARANTINE | -R_SPF_PERMFAIL) & !WHITELISTED_FWD_HOST"; +} +# Spoofed header from and broken policy (excluding sieve host, rspamd host, whitelisted senders, authenticated senders and forward hosts) +SPOOFED_UNAUTH { + expression = "!MAILCOW_AUTH & !MAILCOW_WHITE & !RSPAMD_HOST & !SIEVE_HOST & MAILCOW_DOMAIN_HEADER_FROM & !WHITELISTED_FWD_HOST & -g+:policies"; + score = 50.0; +} +# Only apply to inbound unauthed and not whitelisted +OLEFY_MACRO { + expression = "!MAILCOW_AUTH & !MAILCOW_WHITE & OLETOOLS"; + score = 20.0; + policy = "remove_weight"; +} +# Applies to a content filter map +BAD_WORD_BAD_TLD { + expression = "FISHY_TLD & ( BAD_WORDS | BAD_WORDS_DE )"; + score = 10.0; +} +# Forged with bad policies and not fwd host, keep bad policy symbols +FORGED_W_BAD_POLICY { + expression = "( -g+:policies | -R_SPF_NA) & ( ~FROM_NEQ_ENVFROM | ~FORGED_SENDER ) & !WHITELISTED_FWD_HOST & !DMARC_POLICY_ALLOW"; + score = 3.0; +} +# Keep negative (good) scores for rbl, policies and hfilter, disable neural group +WL_FWD_HOST { + expression = "-WHITELISTED_FWD_HOST & (^g+:rbl | ^g+:policies | ^g+:hfilter | ^g:neural)"; +} +# Exclude X-Spam like flags from scoring from fwd and sieve hosts +UPSTREAM_CHECKS_EXCLUDE_FWD_HOST { + expression = "(-SIEVE_HOST | -WHITELISTED_FWD_HOST) & (^UNITEDINTERNET_SPAM | ^SPAM_FLAG | ^KLMS_SPAM | ^AOL_SPAM | ^MICROSOFT_SPAM)"; +} +# Remove fuzzy group from bounces +BOUNCE_FUZZY { + expression = "-BOUNCE & ^g+:fuzzy"; +} +# Remove bayes ham if fuzzy denied +FUZZY_HAM_MISMATCH { + expression = "( -FUZZY_DENIED | -MAILCOW_FUZZY_DENIED | -LOCAL_FUZZY_DENIED ) & ( ^BAYES_HAM | ^NEURAL_HAM_LONG | ^NEURAL_HAM_SHORT )"; +} +# Remove bayes spam if local fuzzy white +FUZZY_SPAM_MISMATCH { + expression = "( -LOCAL_FUZZY_WHITE ) & ( ^BAYES_SPAM | ^NEURAL_SPAM_LONG | ^NEURAL_SPAM_SHORT )"; +} +WL_FWD_HOST { + expression = "-WHITELISTED_FWD_HOST & (^g+:rbl | ^g+:policies | ^g+:hfilter | ^g:neural)"; +} +ENCRYPTED_CHAT { + expression = "CHAT_VERSION_HEADER & ENCRYPTED_PGP"; +} + +CLAMD_SPAM_FOUND { + expression = "CLAM_SECI_SPAM & !MAILCOW_WHITE"; + description = "Probably Spam, Securite Spam Flag set through ClamAV"; + score = 5; +} + +CLAMD_BAD_PDF { + expression = "CLAM_SECI_PDF & !MAILCOW_WHITE"; + description = "Bad PDF Found, Securite bad PDF Flag set through ClamAV"; + score = 8; +} + +CLAMD_BAD_JPG { + expression = "CLAM_SECI_JPG & !MAILCOW_WHITE"; + description = "Bad JPG Found, Securite bad JPG Flag set through ClamAV"; + score = 8; +} + +CLAMD_ASCII_MALWARE { + expression = "CLAM_SECI_ASCII & !MAILCOW_WHITE"; + description = "ASCII malware found, Securite ASCII malware Flag set through ClamAV"; + score = 8; +} + +CLAMD_HTML_MALWARE { + expression = "CLAM_SECI_HTML & !MAILCOW_WHITE"; + description = "HTML malware found, Securite HTML malware Flag set through ClamAV"; + score = 8; +} + +CLAMD_JS_MALWARE { + expression = "CLAM_SECI_JS & !MAILCOW_WHITE"; + description = "JS malware found, Securite JS malware Flag set through ClamAV"; + score = 8; +} diff --git a/Stable2.0/conf/local.d/dkim_signing.conf b/Stable2.0/conf/local.d/dkim_signing.conf new file mode 100644 index 0000000..4fac27f --- /dev/null +++ b/Stable2.0/conf/local.d/dkim_signing.conf @@ -0,0 +1,35 @@ +# If false, messages with empty envelope from are not signed +allow_envfrom_empty = true; +# If true, envelope/header domain mismatch is ignored +allow_hdrfrom_mismatch = true; +# If true, multiple from headers are allowed (but only first is used) +allow_hdrfrom_multiple = true; +# If true, username does not need to contain matching domain +allow_username_mismatch = true; +# If false, messages from authenticated users are not selected for signing +sign_authenticated = true; +# Default path to key, can include '$domain' and '$selector' variables +path = "/data/dkim/keys/$domain.dkim"; +# Default selector to use +selector = "dkim"; +# If false, messages from local networks are not selected for signing +sign_local = true; +# Symbol to add when message is signed +symbol = "DKIM_SIGNED"; +# Whether to fallback to global config +try_fallback = true; +# Domain to use for DKIM signing: can be "header" or "envelope" +use_domain = "envelope"; +# Whether to normalise domains to eSLD +use_esld = false; +# Whether to get keys from Redis +use_redis = true; +# Hash for DKIM keys in Redis +key_prefix = "DKIM_PRIV_KEYS"; +# Selector map +selector_prefix = "DKIM_SELECTORS"; +# Sieve is in sign_networks only +# forwards are arc signed, rejects are dkim signed +sign_networks = "/etc/rspamd/custom/dovecot_trusted.map"; +use_domain_sign_networks = "header"; +sign_headers = "from:sender:reply-to:subject:date:message-id:to:cc:mime-version:content-type:content-transfer-encoding:content-language:resent-to:resent-cc:resent-from:resent-sender:resent-message-id:in-reply-to:references:list-id:list-help:list-owner:list-unsubscribe:list-subscribe:list-post:list-unsubscribe-post:disposition-notification-to:disposition-notification-options:original-recipient:openpgp:autocrypt"; diff --git a/Stable2.0/conf/local.d/external_services.conf b/Stable2.0/conf/local.d/external_services.conf new file mode 100644 index 0000000..2b091ff --- /dev/null +++ b/Stable2.0/conf/local.d/external_services.conf @@ -0,0 +1,12 @@ +oletools { + # default olefy settings + servers = "olefy:10055"; + # needs to be set explicitly for Rspamd < 1.9.5 + scan_mime_parts = true; + # mime-part regex matching in content-type or filename + # block all macros + extended = true; + max_size = 3145728; + timeout = 20.0; + retransmits = 1; +} diff --git a/Stable2.0/conf/local.d/force_actions.conf b/Stable2.0/conf/local.d/force_actions.conf new file mode 100644 index 0000000..a1b9899 --- /dev/null +++ b/Stable2.0/conf/local.d/force_actions.conf @@ -0,0 +1,12 @@ +rules { + WHITELIST_FORWARDING_HOST_NO_REJECT { + action = "add header"; + expression = "WHITELISTED_FWD_HOST"; + require_action = ["reject"]; + } + WHITELIST_FORWARDING_HOST_NO_GREYLIST { + action = "no action"; + expression = "WHITELISTED_FWD_HOST"; + require_action = ["greylist", "soft reject"]; + } +} diff --git a/Stable2.0/conf/local.d/fuzzy_check.conf b/Stable2.0/conf/local.d/fuzzy_check.conf new file mode 100644 index 0000000..855e8d0 --- /dev/null +++ b/Stable2.0/conf/local.d/fuzzy_check.conf @@ -0,0 +1,54 @@ +rule "local" { + # Fuzzy storage server list + servers = "localhost:11445"; + # Default symbol for unknown flags + symbol = "LOCAL_FUZZY_UNKNOWN"; + # Additional mime types to store/check + mime_types = ["application/*"]; + # Hash weight threshold for all maps + max_score = 100.0; + # Whether we can learn this storage + read_only = no; + # Ignore unknown flags + skip_unknown = yes; + # Hash generation algorithm + algorithm = "mumhash"; + + # Map flags to symbols + fuzzy_map = { + LOCAL_FUZZY_DENIED { + max_score = 10.0; + flag = 11; + } + LOCAL_FUZZY_WHITE { + max_score = 5.0; + flag = 13; + } + } +} + +rule "mailcow" { + # Fuzzy storage server list + servers = "fuzzy.mailcow.email:11445"; + # Default symbol for unknown flags + symbol = "MAILCOW_FUZZY_UNKNOWN"; + # Additional mime types to store/check + mime_types = ["application/*"]; + # Hash weight threshold for all maps + max_score = 100.0; + # Whether we can learn this storage + read_only = yes; + # Ignore unknown flags + skip_unknown = yes; + # Hash generation algorithm + algorithm = "mumhash"; + # Encrypt connection + encryption_key = "oa7xjgdr9u7w3hq1xbttas6brgau8qc17yi7ur5huaeq6paq8h4y"; + # Map flags to symbols + fuzzy_map = { + MAILCOW_FUZZY_DENIED { + max_score = 10.0; + flag = 11; + } + } +} diff --git a/Stable2.0/conf/local.d/fuzzy_group.conf b/Stable2.0/conf/local.d/fuzzy_group.conf new file mode 100644 index 0000000..561ac4e --- /dev/null +++ b/Stable2.0/conf/local.d/fuzzy_group.conf @@ -0,0 +1,17 @@ +symbols = { + "LOCAL_FUZZY_UNKNOWN" { + weight = 0.1; + } + "LOCAL_FUZZY_DENIED" { + weight = 15.0; + } + "MAILCOW_FUZZY_UNKNOWN" { + weight = 0.1; + } + "MAILCOW_FUZZY_DENIED" { + weight = 7.0; + } + "LOCAL_FUZZY_WHITE" { + weight = -10.0; + } +} diff --git a/Stable2.0/conf/local.d/greylist.conf b/Stable2.0/conf/local.d/greylist.conf new file mode 100644 index 0000000..c43c907 --- /dev/null +++ b/Stable2.0/conf/local.d/greylist.conf @@ -0,0 +1,4 @@ +whitelisted_ip = "http://nginx:8081/forwardinghosts.php"; +ipv4_mask = 24; +ipv6_mask = 64; +message = "Greylisted, please try again later"; diff --git a/Stable2.0/conf/local.d/groups.conf b/Stable2.0/conf/local.d/groups.conf new file mode 100644 index 0000000..f77d8a4 --- /dev/null +++ b/Stable2.0/conf/local.d/groups.conf @@ -0,0 +1,59 @@ +symbols { + "MAILCOW_AUTH" { + description = "mailcow authenticated"; + score = -20.0; + } + "CTYPE_MIXED_BOGUS" { + score = 0.0; + } + "BAD_REP_POLICIES" { + score = 2.0; + } + "BAD_HEADER" { + score = 10.0; + } + "BULK_HEADER" { + score = 4.0; + } + "ENCRYPTED_CHAT" { + score = -20.0; + } + "SOGO_CONTACT" { + score = -99.0; + } +} + +group "MX" { + "MX_INVALID" { + score = 0.5; + description = "No connectable MX"; + one_shot = true; + } + "MX_MISSING" { + score = 2.0; + description = "No MX record"; + one_shot = true; + } + "MX_GOOD" { + score = -0.01; + description = "MX was ok"; + one_shot = true; + } +} + +group "reputation" { + symbols = { + "IP_REPUTATION_HAM" { + weight = 1.0; + } + "IP_REPUTATION_SPAM" { + weight = 4.0; + } + "SENDER_REP_HAM" { + weight = 1.0; + } + "SENDER_REP_SPAM" { + weight = 2.0; + } + } +} diff --git a/Stable2.0/conf/local.d/headers_group.conf b/Stable2.0/conf/local.d/headers_group.conf new file mode 100644 index 0000000..1df92b5 --- /dev/null +++ b/Stable2.0/conf/local.d/headers_group.conf @@ -0,0 +1,7 @@ +symbols = { + "R_MIXED_CHARSET" { + weight = 1.0; + description = "Mixed characters in a message"; + one_shot = true; + } +} diff --git a/Stable2.0/conf/local.d/hfilter_group.conf b/Stable2.0/conf/local.d/hfilter_group.conf new file mode 100644 index 0000000..3c908c5 --- /dev/null +++ b/Stable2.0/conf/local.d/hfilter_group.conf @@ -0,0 +1,5 @@ +symbols = { + "HFILTER_HOSTNAME_UNKNOWN" { + score = 8.5; + } +} diff --git a/Stable2.0/conf/local.d/history_redis.conf b/Stable2.0/conf/local.d/history_redis.conf new file mode 100644 index 0000000..68a59b0 --- /dev/null +++ b/Stable2.0/conf/local.d/history_redis.conf @@ -0,0 +1 @@ +nrows = 1000; diff --git a/Stable2.0/conf/local.d/metadata_exporter.conf b/Stable2.0/conf/local.d/metadata_exporter.conf new file mode 100644 index 0000000..daaa79b --- /dev/null +++ b/Stable2.0/conf/local.d/metadata_exporter.conf @@ -0,0 +1,72 @@ +rules { + QUARANTINE { + backend = "http"; + url = "http://nginx:9081/pipe.php"; + selector = "reject_no_global_bl"; + formatter = "default"; + meta_headers = true; + } + RLINFO { + backend = "http"; + url = "http://nginx:9081/pipe_rl.php"; + selector = "ratelimited"; + formatter = "json"; + } + PUSHOVERMAIL { + backend = "http"; + url = "http://nginx:9081/pushover.php"; + selector = "mailcow_rcpt"; + formatter = "json"; + meta_headers = true; + } +} + +custom_select { + mailcow_rcpt = < substr($email, 0, $a), 'domain' => substr(substr($email, $a), 1)); +} +if (!function_exists('getallheaders')) { + function getallheaders() { + if (!is_array($_SERVER)) { + return array(); + } + $headers = array(); + foreach ($_SERVER as $name => $value) { + if (substr($name, 0, 5) == 'HTTP_') { + $headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value; + } + } + return $headers; + } +} + +$raw_data_content = file_get_contents('php://input'); +$raw_data = mb_convert_encoding($raw_data_content, 'HTML-ENTITIES', "UTF-8"); +$headers = getallheaders(); + +$qid = $headers['X-Rspamd-Qid']; +$fuzzy = $headers['X-Rspamd-Fuzzy']; +$subject = iconv_mime_decode($headers['X-Rspamd-Subject']); +$score = $headers['X-Rspamd-Score']; +$rcpts = $headers['X-Rspamd-Rcpt']; +$user = $headers['X-Rspamd-User']; +$ip = $headers['X-Rspamd-Ip']; +$action = $headers['X-Rspamd-Action']; +$sender = $headers['X-Rspamd-From']; +$symbols = $headers['X-Rspamd-Symbols']; + +$raw_size = (int)$_SERVER['CONTENT_LENGTH']; + +if (empty($sender)) { + error_log("QUARANTINE: Unknown sender, assuming empty-env-from@localhost" . PHP_EOL); + $sender = 'empty-env-from@localhost'; +} + +if ($fuzzy == 'unknown') { + $fuzzy = '[]'; +} + +try { + $max_size = (int)$redis->Get('Q_MAX_SIZE'); + if (($max_size * 1048576) < $raw_size) { + error_log(sprintf("QUARANTINE: Message too large: %d b exceeds %d b", $raw_size, ($max_size * 1048576)) . PHP_EOL); + http_response_code(505); + exit; + } + if ($exclude_domains = $redis->Get('Q_EXCLUDE_DOMAINS')) { + $exclude_domains = json_decode($exclude_domains, true); + } + $retention_size = (int)$redis->Get('Q_RETENTION_SIZE'); +} +catch (RedisException $e) { + error_log("QUARANTINE: " . $e . PHP_EOL); + http_response_code(504); + exit; +} + +$rcpt_final_mailboxes = array(); + +// Loop through all rcpts +foreach (json_decode($rcpts, true) as $rcpt) { + // Remove tag + $rcpt = preg_replace('/^(.*?)\+.*(@.*)$/', '$1$2', $rcpt); + + // Break rcpt into local part and domain part + $parsed_rcpt = parse_email($rcpt); + + // Skip if not a mailcow handled domain + try { + if (!$redis->hGet('DOMAIN_MAP', $parsed_rcpt['domain'])) { + continue; + } + } + catch (RedisException $e) { + error_log("QUARANTINE: " . $e . PHP_EOL); + http_response_code(504); + exit; + } + + // Skip if domain is excluded + if (in_array($parsed_rcpt['domain'], $exclude_domains)) { + error_log(sprintf("QUARANTINE: Skipped domain %s", $parsed_rcpt['domain']) . PHP_EOL); + continue; + } + + // Always assume rcpt is not a final mailbox but an alias for a mailbox or further aliases + // + // rcpt + // | + // mailbox <-- goto ---> alias1, alias2, mailbox2 + // | | + // mailbox3 | + // | + // alias3 ---> mailbox4 + // + try { + $stmt = $pdo->prepare("SELECT `goto` FROM `alias` WHERE `address` = :rcpt AND `active` = '1'"); + $stmt->execute(array( + ':rcpt' => $rcpt + )); + $gotos = $stmt->fetch(PDO::FETCH_ASSOC)['goto']; + if (empty($gotos)) { + $stmt = $pdo->prepare("SELECT `goto` FROM `alias` WHERE `address` = :rcpt AND `active` = '1'"); + $stmt->execute(array( + ':rcpt' => '@' . $parsed_rcpt['domain'] + )); + $gotos = $stmt->fetch(PDO::FETCH_ASSOC)['goto']; + } + if (empty($gotos)) { + $stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :rcpt AND `active` = '1'"); + $stmt->execute(array(':rcpt' => $parsed_rcpt['domain'])); + $goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['target_domain']; + if ($goto_branch) { + $gotos = $parsed_rcpt['local'] . '@' . $goto_branch; + } + } + $gotos_array = explode(',', $gotos); + + $loop_c = 0; + + while (count($gotos_array) != 0 && $loop_c <= 20) { + + // Loop through all found gotos + foreach ($gotos_array as $index => &$goto) { + error_log("RCPT RESOVLER: http pipe: query " . $goto . " as username from mailbox" . PHP_EOL); + $stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `username` = :goto AND (`active`= '1' OR `active`= '2');"); + $stmt->execute(array(':goto' => $goto)); + $username = $stmt->fetch(PDO::FETCH_ASSOC)['username']; + if (!empty($username)) { + error_log("RCPT RESOVLER: http pipe: mailbox found: " . $username . PHP_EOL); + // Current goto is a mailbox, save to rcpt_final_mailboxes if not a duplicate + if (!in_array($username, $rcpt_final_mailboxes)) { + $rcpt_final_mailboxes[] = $username; + } + } + else { + $parsed_goto = parse_email($goto); + if (!$redis->hGet('DOMAIN_MAP', $parsed_goto['domain'])) { + error_log("RCPT RESOVLER:" . $goto . " is not a mailcow handled mailbox or alias address" . PHP_EOL); + } + else { + $stmt = $pdo->prepare("SELECT `goto` FROM `alias` WHERE `address` = :goto AND `active` = '1'"); + $stmt->execute(array(':goto' => $goto)); + $goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['goto']; + if ($goto_branch) { + error_log("RCPT RESOVLER: http pipe: goto address " . $goto . " is an alias branch for " . $goto_branch . PHP_EOL); + $goto_branch_array = explode(',', $goto_branch); + } else { + $stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain AND `active` AND '1'"); + $stmt->execute(array(':domain' => $parsed_goto['domain'])); + $goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['target_domain']; + if ($goto_branch) { + error_log("RCPT RESOVLER: http pipe: goto domain " . $parsed_goto['domain'] . " is a domain alias branch for " . $goto_branch . PHP_EOL); + $goto_branch_array = array($parsed_goto['local'] . '@' . $goto_branch); + } + } + } + } + // goto item was processed, unset + unset($gotos_array[$index]); + } + + // Merge goto branch array derived from previous loop (if any), filter duplicates and unset goto branch array + if (!empty($goto_branch_array)) { + $gotos_array = array_unique(array_merge($gotos_array, $goto_branch_array)); + unset($goto_branch_array); + } + + // Reindex array + $gotos_array = array_values($gotos_array); + + // Force exit if loop cannot be solved + // Postfix does not allow for alias loops, so this should never happen. + $loop_c++; + error_log("RCPT RESOVLER: http pipe: goto array count on loop #". $loop_c . " is " . count($gotos_array) . PHP_EOL); + } + } + catch (PDOException $e) { + error_log("RCPT RESOVLER: " . $e->getMessage() . PHP_EOL); + http_response_code(502); + exit; + } +} + +foreach ($rcpt_final_mailboxes as $rcpt_final) { + error_log("QUARANTINE: quarantine pipe: processing quarantine message for rcpt " . $rcpt_final . PHP_EOL); + try { + $stmt = $pdo->prepare("INSERT INTO `quarantine` (`qid`, `subject`, `score`, `sender`, `rcpt`, `symbols`, `user`, `ip`, `msg`, `action`, `fuzzy_hashes`) + VALUES (:qid, :subject, :score, :sender, :rcpt, :symbols, :user, :ip, :msg, :action, :fuzzy_hashes)"); + $stmt->execute(array( + ':qid' => $qid, + ':subject' => $subject, + ':score' => $score, + ':sender' => $sender, + ':rcpt' => $rcpt_final, + ':symbols' => $symbols, + ':user' => $user, + ':ip' => $ip, + ':msg' => $raw_data, + ':action' => $action, + ':fuzzy_hashes' => $fuzzy + )); + $stmt = $pdo->prepare('DELETE FROM `quarantine` WHERE `rcpt` = :rcpt AND `id` NOT IN ( + SELECT `id` + FROM ( + SELECT `id` + FROM `quarantine` + WHERE `rcpt` = :rcpt2 + ORDER BY id DESC + LIMIT :retention_size + ) x + );'); + $stmt->execute(array( + ':rcpt' => $rcpt_final, + ':rcpt2' => $rcpt_final, + ':retention_size' => $retention_size + )); + } + catch (PDOException $e) { + error_log("QUARANTINE: " . $e->getMessage() . PHP_EOL); + http_response_code(503); + exit; + } +} + diff --git a/Stable2.0/conf/meta_exporter/pipe_rl.php b/Stable2.0/conf/meta_exporter/pipe_rl.php new file mode 100644 index 0000000..0bb1b31 --- /dev/null +++ b/Stable2.0/conf/meta_exporter/pipe_rl.php @@ -0,0 +1,48 @@ +connect(getenv('REDIS_SLAVEOF_IP'), getenv('REDIS_SLAVEOF_PORT')); + } + else { + $redis->connect('redis-mailcow', 6379); + } +} +catch (Exception $e) { + exit; +} + +$raw_data_content = file_get_contents('php://input'); +$raw_data_decoded = json_decode($raw_data_content, true); + +$data['time'] = time(); +$data['rcpt'] = implode(', ', $raw_data_decoded['rcpt']); +$data['from'] = $raw_data_decoded['from']; +$data['user'] = $raw_data_decoded['user']; +$symbol_rl_key = array_search('RATELIMITED', array_column($raw_data_decoded['symbols'], 'name')); +$data['rl_info'] = implode($raw_data_decoded['symbols'][$symbol_rl_key]['options']); +preg_match('/(.+)\((.+)\)/i', $data['rl_info'], $rl_matches); +if (!empty($rl_matches[1]) && !empty($rl_matches[2])) { + $data['rl_name'] = $rl_matches[1]; + $data['rl_hash'] = $rl_matches[2]; +} +else { + $data['rl_name'] = 'err'; + $data['rl_hash'] = 'err'; +} +$data['qid'] = $raw_data_decoded['qid']; +$data['ip'] = $raw_data_decoded['ip']; +$data['message_id'] = $raw_data_decoded['message_id']; +$data['header_subject'] = implode(' ', $raw_data_decoded['header_subject']); +$data['header_from'] = implode(', ', $raw_data_decoded['header_from']); + +$redis->lpush('RL_LOG', json_encode($data)); +exit; + diff --git a/Stable2.0/conf/meta_exporter/pushover.php b/Stable2.0/conf/meta_exporter/pushover.php new file mode 100644 index 0000000..d6d01e9 --- /dev/null +++ b/Stable2.0/conf/meta_exporter/pushover.php @@ -0,0 +1,275 @@ + PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => false, +]; +try { + $pdo = new PDO($dsn, $database_user, $database_pass, $opt); +} +catch (PDOException $e) { + error_log("NOTIFY: " . $e . PHP_EOL); + http_response_code(501); + exit; +} +// Init Redis +$redis = new Redis(); +$redis->connect('redis-mailcow', 6379); + +// Functions +function parse_email($email) { + if(!filter_var($email, FILTER_VALIDATE_EMAIL)) return false; + $a = strrpos($email, '@'); + return array('local' => substr($email, 0, $a), 'domain' => substr(substr($email, $a), 1)); +} +if (!function_exists('getallheaders')) { + function getallheaders() { + if (!is_array($_SERVER)) { + return array(); + } + $headers = array(); + foreach ($_SERVER as $name => $value) { + if (substr($name, 0, 5) == 'HTTP_') { + $headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value; + } + } + return $headers; + } +} + +$headers = getallheaders(); +$json_body = json_decode(file_get_contents('php://input')); + +$qid = $headers['X-Rspamd-Qid']; +$rcpts = $headers['X-Rspamd-Rcpt']; +$sender = $headers['X-Rspamd-From']; +$ip = $headers['X-Rspamd-Ip']; +$subject = iconv_mime_decode($headers['X-Rspamd-Subject']); +$messageid= $json_body->message_id; +$priority = 0; + +$symbols_array = json_decode($headers['X-Rspamd-Symbols'], true); +if (is_array($symbols_array)) { + foreach ($symbols_array as $symbol) { + if ($symbol['name'] == 'HAS_X_PRIO_ONE') { + $priority = 1; + break; + } + } +} + +$sender_address = $json_body->header_from[0]; +$sender_name = '-'; +if (preg_match('/(?.*?)<(?
.*?)>/i', $sender_address, $matches)) { + $sender_address = $matches['address']; + $sender_name = trim($matches['name'], '"\' '); +} + +$to_address = $json_body->header_to[0]; +$to_name = '-'; +if (preg_match('/(?.*?)<(?
.*?)>/i', $to_address, $matches)) { + $to_address = $matches['address']; + $to_name = trim($matches['name'], '"\' '); +} + +$rcpt_final_mailboxes = array(); + +// Loop through all rcpts +foreach (json_decode($rcpts, true) as $rcpt) { + // Remove tag + $rcpt = preg_replace('/^(.*?)\+.*(@.*)$/', '$1$2', $rcpt); + + // Break rcpt into local part and domain part + $parsed_rcpt = parse_email($rcpt); + + // Skip if not a mailcow handled domain + try { + if (!$redis->hGet('DOMAIN_MAP', $parsed_rcpt['domain'])) { + continue; + } + } + catch (RedisException $e) { + error_log("NOTIFY: " . $e . PHP_EOL); + http_response_code(504); + exit; + } + + // Always assume rcpt is not a final mailbox but an alias for a mailbox or further aliases + // + // rcpt + // | + // mailbox <-- goto ---> alias1, alias2, mailbox2 + // | | + // mailbox3 | + // | + // alias3 ---> mailbox4 + // + try { + $stmt = $pdo->prepare("SELECT `goto` FROM `alias` WHERE `address` = :rcpt AND `active` = '1'"); + $stmt->execute(array( + ':rcpt' => $rcpt + )); + $gotos = $stmt->fetch(PDO::FETCH_ASSOC)['goto']; + if (empty($gotos)) { + $stmt = $pdo->prepare("SELECT `goto` FROM `alias` WHERE `address` = :rcpt AND `active` = '1'"); + $stmt->execute(array( + ':rcpt' => '@' . $parsed_rcpt['domain'] + )); + $gotos = $stmt->fetch(PDO::FETCH_ASSOC)['goto']; + } + if (empty($gotos)) { + $stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :rcpt AND `active` = '1'"); + $stmt->execute(array(':rcpt' => $parsed_rcpt['domain'])); + $goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['target_domain']; + if ($goto_branch) { + $gotos = $parsed_rcpt['local'] . '@' . $goto_branch; + } + } + $gotos_array = explode(',', $gotos); + + $loop_c = 0; + + while (count($gotos_array) != 0 && $loop_c <= 20) { + + // Loop through all found gotos + foreach ($gotos_array as $index => &$goto) { + error_log("RCPT RESOVLER: http pipe: query " . $goto . " as username from mailbox" . PHP_EOL); + $stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `username` = :goto AND (`active`= '1' OR `active`= '2');"); + $stmt->execute(array(':goto' => $goto)); + $username = $stmt->fetch(PDO::FETCH_ASSOC)['username']; + if (!empty($username)) { + error_log("RCPT RESOVLER: http pipe: mailbox found: " . $username . PHP_EOL); + // Current goto is a mailbox, save to rcpt_final_mailboxes if not a duplicate + if (!in_array($username, $rcpt_final_mailboxes)) { + $rcpt_final_mailboxes[] = $username; + } + } + else { + $parsed_goto = parse_email($goto); + if (!$redis->hGet('DOMAIN_MAP', $parsed_goto['domain'])) { + error_log("RCPT RESOVLER:" . $goto . " is not a mailcow handled mailbox or alias address" . PHP_EOL); + } + else { + $stmt = $pdo->prepare("SELECT `goto` FROM `alias` WHERE `address` = :goto AND `active` = '1'"); + $stmt->execute(array(':goto' => $goto)); + $goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['goto']; + if ($goto_branch) { + error_log("RCPT RESOVLER: http pipe: goto address " . $goto . " is an alias branch for " . $goto_branch . PHP_EOL); + $goto_branch_array = explode(',', $goto_branch); + } else { + $stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain AND `active` AND '1'"); + $stmt->execute(array(':domain' => $parsed_goto['domain'])); + $goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['target_domain']; + if ($goto_branch) { + error_log("RCPT RESOVLER: http pipe: goto domain " . $parsed_goto['domain'] . " is a domain alias branch for " . $goto_branch . PHP_EOL); + $goto_branch_array = array($parsed_goto['local'] . '@' . $goto_branch); + } + } + } + } + // goto item was processed, unset + unset($gotos_array[$index]); + } + + // Merge goto branch array derived from previous loop (if any), filter duplicates and unset goto branch array + if (!empty($goto_branch_array)) { + $gotos_array = array_unique(array_merge($gotos_array, $goto_branch_array)); + unset($goto_branch_array); + } + + // Reindex array + $gotos_array = array_values($gotos_array); + + // Force exit if loop cannot be solved + // Postfix does not allow for alias loops, so this should never happen. + $loop_c++; + error_log("RCPT RESOVLER: http pipe: goto array count on loop #". $loop_c . " is " . count($gotos_array) . PHP_EOL); + } + } + catch (PDOException $e) { + error_log("RCPT RESOVLER: " . $e->getMessage() . PHP_EOL); + http_response_code(502); + exit; + } +} + + +foreach ($rcpt_final_mailboxes as $rcpt_final) { + error_log("NOTIFY: pushover pipe: processing pushover message for rcpt " . $rcpt_final . PHP_EOL); + $stmt = $pdo->prepare("SELECT * FROM `pushover` + WHERE `username` = :username AND `active` = '1'"); + $stmt->execute(array( + ':username' => $rcpt_final + )); + $api_data = $stmt->fetch(PDO::FETCH_ASSOC); + if (isset($api_data['key']) && isset($api_data['token'])) { + $title = (!empty($api_data['title'])) ? $api_data['title'] : 'Mail'; + $text = (!empty($api_data['text'])) ? $api_data['text'] : 'You\'ve got mail šŸ“§'; + $attributes = json_decode($api_data['attributes'], true); + $senders = explode(',', $api_data['senders']); + $senders = array_filter($senders); + $senders_regex = $api_data['senders_regex']; + $sender_validated = false; + if (empty($senders) && empty($senders_regex)) { + $sender_validated = true; + } + else { + if (!empty($senders)) { + if (in_array($sender, $senders)) { + $sender_validated = true; + } + } + if (!empty($senders_regex) && $sender_validated !== true) { + if (preg_match($senders_regex, $sender)) { + $sender_validated = true; + } + } + } + if ($sender_validated === false) { + error_log("NOTIFY: pushover pipe: skipping unwanted sender " . $sender); + continue; + } + if ($attributes['only_x_prio'] == "1" && $priority == 0) { + error_log("NOTIFY: pushover pipe: mail has no X-Priority: 1 header, skipping"); + continue; + } + $post_fields = array( + "token" => $api_data['token'], + "user" => $api_data['key'], + "title" => sprintf("%s", str_replace( + array('{SUBJECT}', '{SENDER}', '{SENDER_NAME}', '{SENDER_ADDRESS}', '{TO_NAME}', '{TO_ADDRESS}', '{MSG_ID}'), + array($subject, $sender, $sender_name, $sender_address, $to_name, $to_address, $messageid), $title) + ), + "priority" => $priority, + "message" => sprintf("%s", str_replace( + array('{SUBJECT}', '{SENDER}', '{SENDER_NAME}', '{SENDER_ADDRESS}', '{TO_NAME}', '{TO_ADDRESS}', '{MSG_ID}', '\n'), + array($subject, $sender, $sender_name, $sender_address, $to_name, $to_address, $messageid, PHP_EOL), $text) + ), + "sound" => $attributes['sound'] ?? "pushover" + ); + if ($attributes['evaluate_x_prio'] == "1" && $priority == 1) { + $post_fields['expire'] = 600; + $post_fields['retry'] = 120; + $post_fields['priority'] = 2; + } + curl_setopt_array($ch = curl_init(), array( + CURLOPT_URL => "https://api.pushover.net/1/messages.json", + CURLOPT_POSTFIELDS => $post_fields, + CURLOPT_SAFE_UPLOAD => true, + CURLOPT_RETURNTRANSFER => true, + )); + $result = curl_exec($ch); + $httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + error_log("NOTIFY: result: " . $httpcode . PHP_EOL); + } +} diff --git a/Stable2.0/conf/meta_exporter/vars.inc.php b/Stable2.0/conf/meta_exporter/vars.inc.php new file mode 100644 index 0000000..79566b0 --- /dev/null +++ b/Stable2.0/conf/meta_exporter/vars.inc.php @@ -0,0 +1,6 @@ + diff --git a/Stable2.0/conf/override.d/logging.inc b/Stable2.0/conf/override.d/logging.inc new file mode 100644 index 0000000..64d4064 --- /dev/null +++ b/Stable2.0/conf/override.d/logging.inc @@ -0,0 +1,5 @@ +level = "silent"; +type = "console"; +systemd = false; +.include "$CONFDIR/logging.inc" +.include(try=true; priority=20) "$CONFDIR/override.d/logging.custom.inc" diff --git a/Stable2.0/conf/override.d/ratelimit.conf b/Stable2.0/conf/override.d/ratelimit.conf new file mode 100644 index 0000000..3379149 --- /dev/null +++ b/Stable2.0/conf/override.d/ratelimit.conf @@ -0,0 +1,4 @@ +whitelisted_rcpts = "postmaster,mailer-daemon"; +max_rcpt = 25; +custom_keywords = "/etc/rspamd/lua/ratelimit.lua"; +info_symbol = "RATELIMITED"; diff --git a/Stable2.0/conf/override.d/worker-controller.inc b/Stable2.0/conf/override.d/worker-controller.inc new file mode 100644 index 0000000..8c929b1 --- /dev/null +++ b/Stable2.0/conf/override.d/worker-controller.inc @@ -0,0 +1,7 @@ +bind_socket = "*:11334"; +count = 1; +secure_ip = "127.0.0.1"; +secure_ip = "::1"; +bind_socket = "/var/lib/rspamd/rspamd.sock mode=0666 owner=nobody"; +.include(try=true; priority=10) "$CONFDIR/override.d/worker-controller-password.inc" +.include(try=true; priority=30) "$CONFDIR/override.d/worker-controller.custom.inc" diff --git a/Stable2.0/conf/override.d/worker-fuzzy.inc b/Stable2.0/conf/override.d/worker-fuzzy.inc new file mode 100644 index 0000000..291e615 --- /dev/null +++ b/Stable2.0/conf/override.d/worker-fuzzy.inc @@ -0,0 +1,12 @@ +# Socket to listen on (UDP and TCP from rspamd 1.3) +bind_socket = "*:11445"; +allow_update = ["127.0.0.1", "::1"]; +# Number of processes to serve this storage (useful for read scaling) +count = 1; +# Backend ("sqlite" or "redis" - default "sqlite") +backend = "redis"; +# Hashes storage time (3 months) +expire = 90d; +# Synchronize updates to the storage each minute +sync = 1min; + diff --git a/Stable2.0/conf/override.d/worker-normal.inc b/Stable2.0/conf/override.d/worker-normal.inc new file mode 100644 index 0000000..d206757 --- /dev/null +++ b/Stable2.0/conf/override.d/worker-normal.inc @@ -0,0 +1,4 @@ +bind_socket = "*:11333"; +task_timeout = 25s; +count = 1; +.include(try=true; priority=30) "$CONFDIR/override.d/worker-normal.custom.inc" diff --git a/Stable2.0/conf/override.d/worker-proxy.inc b/Stable2.0/conf/override.d/worker-proxy.inc new file mode 100644 index 0000000..9eb4775 --- /dev/null +++ b/Stable2.0/conf/override.d/worker-proxy.inc @@ -0,0 +1,9 @@ +bind_socket = "rspamd:9900"; +milter = true; +upstream "local" { + name = "localhost"; + default = true; + hosts = "rspamd:11333" +} +reject_message = "This message does not meet our delivery requirements"; +.include(try=true; priority=30) "$CONFDIR/override.d/worker-proxy.custom.inc" diff --git a/Stable2.0/conf/plugins.d/README.md b/Stable2.0/conf/plugins.d/README.md new file mode 100644 index 0000000..1516cf2 --- /dev/null +++ b/Stable2.0/conf/plugins.d/README.md @@ -0,0 +1 @@ +This is where you should copy any rspamd custom module diff --git a/Stable2.0/conf/rspamd.conf.local b/Stable2.0/conf/rspamd.conf.local new file mode 100644 index 0000000..9f2f8f1 --- /dev/null +++ b/Stable2.0/conf/rspamd.conf.local @@ -0,0 +1 @@ +# rspamd.conf.local diff --git a/Stable2.0/conf/rspamd.conf.override b/Stable2.0/conf/rspamd.conf.override new file mode 100644 index 0000000..d033e8e --- /dev/null +++ b/Stable2.0/conf/rspamd.conf.override @@ -0,0 +1,2 @@ +# rspamd.conf.override + diff --git a/Stable2.0/hooks/build b/Stable2.0/hooks/build new file mode 100644 index 0000000..f7670db --- /dev/null +++ b/Stable2.0/hooks/build @@ -0,0 +1,17 @@ +#!/bin/bash +# hooks/build +# https://docs.docker.com/docker-cloud/builds/advanced/ + +# $IMAGE_NAME var is injected into the build so the tag is correct. +echo "[***] Build hook running" + +VERSION=$(git ls-remote --tags -q https://github.com/rspamd/rspamd | sed -n "s/^[[:xdigit:]]\{40\}[[:blank:]]refs\/tags\/\([0-9]\{1\}\.[0-9]\{1,2\}\($\|\.[0-9]\{1,2\}$\)\)/\1/p" | sort --version-sort | tail -1) + +IMAGE_NAME=docker-rspamd + +docker build \ + --build-arg VERSION=${VERSION} \ + --build-arg COMMIT=$(git ls-remote --tags -q https://github.com/rspamd/rspamd | sed -n "s/^\([[:xdigit:]]\{40\}\)[[:blank:]]refs\/tags\/${VERSION}^{}$/\1/p" | xargs git rev-parse --short) \ + --build-arg BRANCH=$(git rev-parse --abbrev-ref HEAD) \ + --build-arg DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \ + -t ${IMAGE_NAME} . \ No newline at end of file diff --git a/Stable2.0/hooks/post_push b/Stable2.0/hooks/post_push new file mode 100644 index 0000000..3a3788f --- /dev/null +++ b/Stable2.0/hooks/post_push @@ -0,0 +1,9 @@ +#!/bin/bash + +VERSION=$(git ls-remote --tags -q https://github.com/rspamd/rspamd | sed -n "s/^[[:xdigit:]]\{40\}[[:blank:]]refs\/tags\/\([0-9]\{1\}\.[0-9]\{1,2\}\($\|\.[0-9]\{1,2\}$\)\)/\1/p" | sort --version-sort | tail -1) + +docker tag \ + "${IMAGE_NAME}" \ + "${DOCKER_REPO}:stable-${VERSION}" +docker push \ + "${DOCKER_REPO}:stable-${VERSION}" diff --git a/Stable2.0/rspamd.conf.local.override b/Stable2.0/rspamd.conf.local.override new file mode 100644 index 0000000..226a170 --- /dev/null +++ b/Stable2.0/rspamd.conf.local.override @@ -0,0 +1,13 @@ +options { + pidfile = false; + .include "$CONFDIR/options.inc" + .include(try=true; priority=1,duplicate=merge) "$LOCAL_CONFDIR/local.d/options.inc" + .include(try=true; priority=10) "$LOCAL_CONFDIR/override.d/options.inc" +} + +logging { + type = "console"; + .include "$CONFDIR/logging.inc" + .include(try=true; priority=1,duplicate=merge) "$LOCAL_CONFDIR/local.d/logging.inc" + .include(try=true; priority=10) "$LOCAL_CONFDIR/override.d/logging.inc" +} \ No newline at end of file diff --git a/Stable2.0/worker-controller.inc b/Stable2.0/worker-controller.inc new file mode 100644 index 0000000..2b6378a --- /dev/null +++ b/Stable2.0/worker-controller.inc @@ -0,0 +1 @@ +bind_socket = "*:11334"; diff --git a/Stable2.0/worker-proxy.inc b/Stable2.0/worker-proxy.inc new file mode 100644 index 0000000..0fe2cf0 --- /dev/null +++ b/Stable2.0/worker-proxy.inc @@ -0,0 +1 @@ +bind_socket = *:11332;