First Shot
Signed-off-by: Patrick Niebeling <patrick.niebeling@adacor.com>
This commit is contained in:
60
Stable2.0/Dockerfile
Normal file
60
Stable2.0/Dockerfile
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
FROM debian:stable-slim
|
||||||
|
LABEL maintainer="gnilebein - <docker@gnilebein.nl>"
|
||||||
|
|
||||||
|
# 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 \
|
31
Stable2.0/conf/custom/bad_asn.map
Normal file
31
Stable2.0/conf/custom/bad_asn.map
Normal file
@ -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
|
2
Stable2.0/conf/custom/bad_header.map
Normal file
2
Stable2.0/conf/custom/bad_header.map
Normal file
@ -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
|
1
Stable2.0/conf/custom/bad_languages.map
Normal file
1
Stable2.0/conf/custom/bad_languages.map
Normal file
@ -0,0 +1 @@
|
|||||||
|
# Regex! /de/ will also match /de_at/ etc.
|
29
Stable2.0/conf/custom/bad_words.map
Normal file
29
Stable2.0/conf/custom/bad_words.map
Normal file
@ -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
|
17
Stable2.0/conf/custom/bad_words_de.map
Normal file
17
Stable2.0/conf/custom/bad_words_de.map
Normal file
@ -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
|
19
Stable2.0/conf/custom/bulk_header.map
Normal file
19
Stable2.0/conf/custom/bulk_header.map
Normal file
@ -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
|
65
Stable2.0/conf/custom/fishy_tlds.map
Normal file
65
Stable2.0/conf/custom/fishy_tlds.map
Normal file
@ -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
|
1
Stable2.0/conf/custom/global_mime_from_blacklist.map
Normal file
1
Stable2.0/conf/custom/global_mime_from_blacklist.map
Normal file
@ -0,0 +1 @@
|
|||||||
|
# /.+example\.com/i
|
1
Stable2.0/conf/custom/global_mime_from_whitelist.map
Normal file
1
Stable2.0/conf/custom/global_mime_from_whitelist.map
Normal file
@ -0,0 +1 @@
|
|||||||
|
# /.+example\.com/i
|
1
Stable2.0/conf/custom/global_rcpt_blacklist.map
Normal file
1
Stable2.0/conf/custom/global_rcpt_blacklist.map
Normal file
@ -0,0 +1 @@
|
|||||||
|
# /.+example\.com/i
|
1
Stable2.0/conf/custom/global_rcpt_whitelist.map
Normal file
1
Stable2.0/conf/custom/global_rcpt_whitelist.map
Normal file
@ -0,0 +1 @@
|
|||||||
|
# /.+example\.com/i
|
1
Stable2.0/conf/custom/global_smtp_from_blacklist.map
Normal file
1
Stable2.0/conf/custom/global_smtp_from_blacklist.map
Normal file
@ -0,0 +1 @@
|
|||||||
|
# /.+example\.com/i
|
1
Stable2.0/conf/custom/global_smtp_from_whitelist.map
Normal file
1
Stable2.0/conf/custom/global_smtp_from_whitelist.map
Normal file
@ -0,0 +1 @@
|
|||||||
|
# /.+example\.com/i
|
4
Stable2.0/conf/custom/ip_wl.map
Normal file
4
Stable2.0/conf/custom/ip_wl.map
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# IP whitelist
|
||||||
|
# 127.0.0.1
|
||||||
|
# 1.2.3.4
|
||||||
|
# ...
|
7
Stable2.0/conf/custom/monitoring_nolog.map
Normal file
7
Stable2.0/conf/custom/monitoring_nolog.map
Normal file
@ -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
|
174
Stable2.0/conf/dynmaps/aliasexp.php
Normal file
174
Stable2.0/conf/dynmaps/aliasexp.php
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
<?php
|
||||||
|
// File size is limited by Nginx site to 10M
|
||||||
|
// To speed things up, we do not include prerequisites
|
||||||
|
header('Content-Type: text/plain');
|
||||||
|
require_once "vars.inc.php";
|
||||||
|
// Do not show errors, we log to using error_log
|
||||||
|
ini_set('error_reporting', 0);
|
||||||
|
// Init database
|
||||||
|
//$dsn = $database_type . ':host=' . $database_host . ';dbname=' . $database_name;
|
||||||
|
$dsn = $database_type . ":unix_socket=" . $database_sock . ";dbname=" . $database_name;
|
||||||
|
$opt = [
|
||||||
|
PDO::ATTR_ERRMODE => 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]);
|
||||||
|
}
|
88
Stable2.0/conf/dynmaps/bcc.php
Normal file
88
Stable2.0/conf/dynmaps/bcc.php
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
<?php
|
||||||
|
// File size is limited by Nginx site to 10M
|
||||||
|
// To speed things up, we do not include prerequisites
|
||||||
|
header('Content-Type: text/plain');
|
||||||
|
require_once "vars.inc.php";
|
||||||
|
// Do not show errors, we log to using error_log
|
||||||
|
ini_set('error_reporting', 0);
|
||||||
|
// Init database
|
||||||
|
//$dsn = $database_type . ':host=' . $database_host . ';dbname=' . $database_name;
|
||||||
|
$dsn = $database_type . ":unix_socket=" . $database_sock . ";dbname=" . $database_name;
|
||||||
|
$opt = [
|
||||||
|
PDO::ATTR_ERRMODE => 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;
|
||||||
|
}
|
||||||
|
|
113
Stable2.0/conf/dynmaps/footer.php
Normal file
113
Stable2.0/conf/dynmaps/footer.php
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
<?php
|
||||||
|
// File size is limited by Nginx site to 10M
|
||||||
|
// To speed things up, we do not include prerequisites
|
||||||
|
header('Content-Type: text/plain');
|
||||||
|
require_once "vars.inc.php";
|
||||||
|
// Do not show errors, we log to using error_log
|
||||||
|
ini_set('error_reporting', 0);
|
||||||
|
// Init database
|
||||||
|
//$dsn = $database_type . ':host=' . $database_host . ';dbname=' . $database_name;
|
||||||
|
$dsn = $database_type . ":unix_socket=" . $database_sock . ";dbname=" . $database_name;
|
||||||
|
$opt = [
|
||||||
|
PDO::ATTR_ERRMODE => 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);
|
57
Stable2.0/conf/dynmaps/forwardinghosts.php
Normal file
57
Stable2.0/conf/dynmaps/forwardinghosts.php
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: text/plain');
|
||||||
|
ini_set('error_reporting', 0);
|
||||||
|
|
||||||
|
$redis = new Redis();
|
||||||
|
$redis->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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
2
Stable2.0/conf/dynmaps/index.html
Normal file
2
Stable2.0/conf/dynmaps/index.html
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
<html>
|
||||||
|
</html>
|
2
Stable2.0/conf/dynmaps/sasl_logs.php
Normal file
2
Stable2.0/conf/dynmaps/sasl_logs.php
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
<?php
|
||||||
|
// PoC
|
471
Stable2.0/conf/dynmaps/settings.php
Normal file
471
Stable2.0/conf/dynmaps/settings.php
Normal file
@ -0,0 +1,471 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
The match section performs AND operation on different matches: for example, if you have from and rcpt in the same rule,
|
||||||
|
then the rule matches only when from AND rcpt match. For similar matches, the OR rule applies: if you have multiple rcpt matches,
|
||||||
|
then any of these will trigger the rule. If a rule is triggered then no more rules are matched.
|
||||||
|
*/
|
||||||
|
header('Content-Type: text/plain');
|
||||||
|
require_once "vars.inc.php";
|
||||||
|
// Getting headers sent by the client.
|
||||||
|
ini_set('error_reporting', 0);
|
||||||
|
|
||||||
|
//$dsn = $database_type . ':host=' . $database_host . ';dbname=' . $database_name;
|
||||||
|
$dsn = $database_type . ":unix_socket=" . $database_sock . ";dbname=" . $database_name;
|
||||||
|
$opt = [
|
||||||
|
PDO::ATTR_ERRMODE => 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
// Start custom scores for users
|
||||||
|
*/
|
||||||
|
|
||||||
|
$stmt = $pdo->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_<?=$username_sane;?> {
|
||||||
|
priority = 4;
|
||||||
|
<?php
|
||||||
|
foreach (ucl_rcpts($row['object'], strpos($row['object'], '@') === FALSE ? 'domain' : 'mailbox') as $rcpt) {
|
||||||
|
?>
|
||||||
|
rcpt = <?=json_encode($rcpt, JSON_UNESCAPED_SLASHES);?>;
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
$stmt = $pdo->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 = <?=$spamscore['highspamlevel'][0];?>;
|
||||||
|
greylist = <?=$spamscore['lowspamlevel'][0] - 1;?>;
|
||||||
|
"add header" = <?=$spamscore['lowspamlevel'][0];?>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
// Start SOGo contacts whitelist
|
||||||
|
// Priority 4, lower than a domain whitelist (5) and lower than a mailbox whitelist (6)
|
||||||
|
*/
|
||||||
|
|
||||||
|
foreach (wl_by_sogo() as $user => $contacts) {
|
||||||
|
$username_sane = preg_replace("/[^a-zA-Z0-9]+/", "", $user);
|
||||||
|
?>
|
||||||
|
whitelist_sogo_<?=$username_sane;?> {
|
||||||
|
<?php
|
||||||
|
foreach ($contacts as $contact) {
|
||||||
|
?>
|
||||||
|
from = <?=json_encode($contact, JSON_UNESCAPED_SLASHES);?>;
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
priority = 4;
|
||||||
|
<?php
|
||||||
|
foreach (ucl_rcpts($user, 'mailbox') as $rcpt) {
|
||||||
|
?>
|
||||||
|
rcpt = <?=json_encode($rcpt, JSON_UNESCAPED_SLASHES);?>;
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
apply "default" {
|
||||||
|
SOGO_CONTACT = -99.0;
|
||||||
|
}
|
||||||
|
symbols [
|
||||||
|
"SOGO_CONTACT"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
// Start whitelist
|
||||||
|
*/
|
||||||
|
|
||||||
|
$stmt = $pdo->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_<?=$username_sane;?> {
|
||||||
|
<?php
|
||||||
|
$list_items = array();
|
||||||
|
$stmt = $pdo->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 = "/<?='^' . str_replace('\*', '.*', preg_quote($item['value'], '/')) . '$' ;?>/i";
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
if (!filter_var(trim($row['object']), FILTER_VALIDATE_EMAIL)) {
|
||||||
|
?>
|
||||||
|
priority = 5;
|
||||||
|
<?php
|
||||||
|
foreach (ucl_rcpts($row['object'], strpos($row['object'], '@') === FALSE ? 'domain' : 'mailbox') as $rcpt) {
|
||||||
|
?>
|
||||||
|
rcpt = <?=json_encode($rcpt, JSON_UNESCAPED_SLASHES);?>;
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
?>
|
||||||
|
priority = 6;
|
||||||
|
<?php
|
||||||
|
foreach (ucl_rcpts($row['object'], strpos($row['object'], '@') === FALSE ? 'domain' : 'mailbox') as $rcpt) {
|
||||||
|
?>
|
||||||
|
rcpt = <?=json_encode($rcpt, JSON_UNESCAPED_SLASHES);?>;
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
apply "default" {
|
||||||
|
MAILCOW_WHITE = -999.0;
|
||||||
|
}
|
||||||
|
symbols [
|
||||||
|
"MAILCOW_WHITE"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
whitelist_mime_<?=$username_sane;?> {
|
||||||
|
<?php
|
||||||
|
foreach ($list_items as $item) {
|
||||||
|
?>
|
||||||
|
from_mime = "/<?='^' . str_replace('\*', '.*', preg_quote($item['value'], '/')) . '$' ;?>/i";
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
if (!filter_var(trim($row['object']), FILTER_VALIDATE_EMAIL)) {
|
||||||
|
?>
|
||||||
|
priority = 5;
|
||||||
|
<?php
|
||||||
|
foreach (ucl_rcpts($row['object'], strpos($row['object'], '@') === FALSE ? 'domain' : 'mailbox') as $rcpt) {
|
||||||
|
?>
|
||||||
|
rcpt = <?=json_encode($rcpt, JSON_UNESCAPED_SLASHES);?>;
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
?>
|
||||||
|
priority = 6;
|
||||||
|
<?php
|
||||||
|
foreach (ucl_rcpts($row['object'], strpos($row['object'], '@') === FALSE ? 'domain' : 'mailbox') as $rcpt) {
|
||||||
|
?>
|
||||||
|
rcpt = <?=json_encode($rcpt, JSON_UNESCAPED_SLASHES);?>;
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
apply "default" {
|
||||||
|
MAILCOW_WHITE = -999.0;
|
||||||
|
}
|
||||||
|
symbols [
|
||||||
|
"MAILCOW_WHITE"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
// Start blacklist
|
||||||
|
*/
|
||||||
|
|
||||||
|
$stmt = $pdo->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_<?=$username_sane;?> {
|
||||||
|
<?php
|
||||||
|
$list_items = array();
|
||||||
|
$stmt = $pdo->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 = "/<?='^' . str_replace('\*', '.*', preg_quote($item['value'], '/')) . '$' ;?>/i";
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
if (!filter_var(trim($row['object']), FILTER_VALIDATE_EMAIL)) {
|
||||||
|
?>
|
||||||
|
priority = 5;
|
||||||
|
<?php
|
||||||
|
foreach (ucl_rcpts($row['object'], strpos($row['object'], '@') === FALSE ? 'domain' : 'mailbox') as $rcpt) {
|
||||||
|
?>
|
||||||
|
rcpt = <?=json_encode($rcpt, JSON_UNESCAPED_SLASHES);?>;
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
?>
|
||||||
|
priority = 6;
|
||||||
|
<?php
|
||||||
|
foreach (ucl_rcpts($row['object'], strpos($row['object'], '@') === FALSE ? 'domain' : 'mailbox') as $rcpt) {
|
||||||
|
?>
|
||||||
|
rcpt = <?=json_encode($rcpt, JSON_UNESCAPED_SLASHES);?>;
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
apply "default" {
|
||||||
|
MAILCOW_BLACK = 999.0;
|
||||||
|
}
|
||||||
|
symbols [
|
||||||
|
"MAILCOW_BLACK"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
blacklist_header_<?=$username_sane;?> {
|
||||||
|
<?php
|
||||||
|
foreach ($list_items as $item) {
|
||||||
|
?>
|
||||||
|
from_mime = "/<?='^' . str_replace('\*', '.*', preg_quote($item['value'], '/')) . '$' ;?>/i";
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
if (!filter_var(trim($row['object']), FILTER_VALIDATE_EMAIL)) {
|
||||||
|
?>
|
||||||
|
priority = 5;
|
||||||
|
<?php
|
||||||
|
foreach (ucl_rcpts($row['object'], strpos($row['object'], '@') === FALSE ? 'domain' : 'mailbox') as $rcpt) {
|
||||||
|
?>
|
||||||
|
rcpt = <?=json_encode($rcpt, JSON_UNESCAPED_SLASHES);?>;
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
?>
|
||||||
|
priority = 6;
|
||||||
|
<?php
|
||||||
|
foreach (ucl_rcpts($row['object'], strpos($row['object'], '@') === FALSE ? 'domain' : 'mailbox') as $rcpt) {
|
||||||
|
?>
|
||||||
|
rcpt = <?=json_encode($rcpt, JSON_UNESCAPED_SLASHES);?>;
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
apply "default" {
|
||||||
|
MAILCOW_BLACK = 999.0;
|
||||||
|
}
|
||||||
|
symbols [
|
||||||
|
"MAILCOW_BLACK"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
// Start traps
|
||||||
|
*/
|
||||||
|
|
||||||
|
?>
|
||||||
|
ham_trap {
|
||||||
|
<?php
|
||||||
|
foreach (ucl_rcpts('ham@localhost', 'mailbox') as $rcpt) {
|
||||||
|
?>
|
||||||
|
rcpt = <?=json_encode($rcpt, JSON_UNESCAPED_SLASHES);?>;
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
priority = 9;
|
||||||
|
apply "default" {
|
||||||
|
symbols_enabled = ["HISTORY_SAVE"];
|
||||||
|
}
|
||||||
|
symbols [
|
||||||
|
"HAM_TRAP"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
spam_trap {
|
||||||
|
<?php
|
||||||
|
foreach (ucl_rcpts('spam@localhost', 'mailbox') as $rcpt) {
|
||||||
|
?>
|
||||||
|
rcpt = <?=json_encode($rcpt, JSON_UNESCAPED_SLASHES);?>;
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
priority = 9;
|
||||||
|
apply "default" {
|
||||||
|
symbols_enabled = ["HISTORY_SAVE"];
|
||||||
|
}
|
||||||
|
symbols [
|
||||||
|
"SPAM_TRAP"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
<?php
|
||||||
|
// Start additional content
|
||||||
|
|
||||||
|
$stmt = $pdo->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_<?=intval($row['id']);?> {
|
||||||
|
<?php
|
||||||
|
$content = preg_split('/\r\n|\r|\n/', $row['content']);
|
||||||
|
foreach ($content as $line) {
|
||||||
|
echo ' ' . $line . PHP_EOL;
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
}
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
}
|
6
Stable2.0/conf/dynmaps/vars.inc.php
Normal file
6
Stable2.0/conf/dynmaps/vars.inc.php
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?php
|
||||||
|
require_once('../../../web/inc/vars.inc.php');
|
||||||
|
if (file_exists('../../../web/inc/vars.local.inc.php')) {
|
||||||
|
include_once('../../../web/inc/vars.local.inc.php');
|
||||||
|
}
|
||||||
|
?>
|
3
Stable2.0/conf/local.d/actions.conf
Normal file
3
Stable2.0/conf/local.d/actions.conf
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
reject = 15;
|
||||||
|
add_header = 8;
|
||||||
|
greylist = 7;
|
11
Stable2.0/conf/local.d/antivirus.conf
Normal file
11
Stable2.0/conf/local.d/antivirus.conf
Normal file
@ -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;
|
||||||
|
}
|
32
Stable2.0/conf/local.d/arc.conf
Normal file
32
Stable2.0/conf/local.d/arc.conf
Normal file
@ -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";
|
6
Stable2.0/conf/local.d/asn.conf
Normal file
6
Stable2.0/conf/local.d/asn.conf
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
provider_type = "rspamd";
|
||||||
|
provider_info {
|
||||||
|
ip4 = "asn.rspamd.com";
|
||||||
|
ip6 = "asn6.rspamd.com";
|
||||||
|
}
|
||||||
|
symbol = "ASN";
|
110
Stable2.0/conf/local.d/composites.conf
Normal file
110
Stable2.0/conf/local.d/composites.conf
Normal file
@ -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;
|
||||||
|
}
|
35
Stable2.0/conf/local.d/dkim_signing.conf
Normal file
35
Stable2.0/conf/local.d/dkim_signing.conf
Normal file
@ -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";
|
12
Stable2.0/conf/local.d/external_services.conf
Normal file
12
Stable2.0/conf/local.d/external_services.conf
Normal file
@ -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;
|
||||||
|
}
|
12
Stable2.0/conf/local.d/force_actions.conf
Normal file
12
Stable2.0/conf/local.d/force_actions.conf
Normal file
@ -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"];
|
||||||
|
}
|
||||||
|
}
|
54
Stable2.0/conf/local.d/fuzzy_check.conf
Normal file
54
Stable2.0/conf/local.d/fuzzy_check.conf
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
17
Stable2.0/conf/local.d/fuzzy_group.conf
Normal file
17
Stable2.0/conf/local.d/fuzzy_group.conf
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
4
Stable2.0/conf/local.d/greylist.conf
Normal file
4
Stable2.0/conf/local.d/greylist.conf
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
whitelisted_ip = "http://nginx:8081/forwardinghosts.php";
|
||||||
|
ipv4_mask = 24;
|
||||||
|
ipv6_mask = 64;
|
||||||
|
message = "Greylisted, please try again later";
|
59
Stable2.0/conf/local.d/groups.conf
Normal file
59
Stable2.0/conf/local.d/groups.conf
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
7
Stable2.0/conf/local.d/headers_group.conf
Normal file
7
Stable2.0/conf/local.d/headers_group.conf
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
symbols = {
|
||||||
|
"R_MIXED_CHARSET" {
|
||||||
|
weight = 1.0;
|
||||||
|
description = "Mixed characters in a message";
|
||||||
|
one_shot = true;
|
||||||
|
}
|
||||||
|
}
|
5
Stable2.0/conf/local.d/hfilter_group.conf
Normal file
5
Stable2.0/conf/local.d/hfilter_group.conf
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
symbols = {
|
||||||
|
"HFILTER_HOSTNAME_UNKNOWN" {
|
||||||
|
score = 8.5;
|
||||||
|
}
|
||||||
|
}
|
1
Stable2.0/conf/local.d/history_redis.conf
Normal file
1
Stable2.0/conf/local.d/history_redis.conf
Normal file
@ -0,0 +1 @@
|
|||||||
|
nrows = 1000;
|
72
Stable2.0/conf/local.d/metadata_exporter.conf
Normal file
72
Stable2.0/conf/local.d/metadata_exporter.conf
Normal file
@ -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 = <<EOD
|
||||||
|
return function(task)
|
||||||
|
local action = task:get_metric_action('default')
|
||||||
|
if task:has_symbol('NO_LOG_STAT') or (action == 'soft reject' or action == 'reject' or action == 'add header' or action == 'rewrite subject') then
|
||||||
|
return false
|
||||||
|
else
|
||||||
|
if task:get_symbol("RCPT_MAILCOW_DOMAIN") then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
EOD;
|
||||||
|
ratelimited = <<EOD
|
||||||
|
return function(task)
|
||||||
|
local ratelimited = task:get_symbol("RATELIMITED")
|
||||||
|
if ratelimited then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
EOD;
|
||||||
|
reject_no_global_bl = <<EOD
|
||||||
|
return function(task)
|
||||||
|
if not task:has_symbol('GLOBAL_SMTP_FROM_BL')
|
||||||
|
and not task:has_symbol('GLOBAL_MIME_FROM_BL')
|
||||||
|
and not task:has_symbol('LOCAL_BL_ASN')
|
||||||
|
and not task:has_symbol('GLOBAL_RCPT_BL')
|
||||||
|
and not task:has_symbol('BAD_SUBJECT_00')
|
||||||
|
and not task:has_symbol('MAILCOW_BLACK') then
|
||||||
|
local action = task:get_metric_action('default')
|
||||||
|
if action == 'reject' or action == 'add header' or action == 'rewrite subject' then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
EOD;
|
||||||
|
}
|
||||||
|
|
||||||
|
custom_format {
|
||||||
|
msgid = <<EOD
|
||||||
|
return function(task)
|
||||||
|
return task:get_message_id()
|
||||||
|
end
|
||||||
|
EOD;
|
||||||
|
}
|
||||||
|
|
43
Stable2.0/conf/local.d/milter_headers.conf
Normal file
43
Stable2.0/conf/local.d/milter_headers.conf
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
use = ["spam-header", "x-spamd-result", "x-rspamd-queue-id", "authentication-results", "fuzzy-hashes"];
|
||||||
|
skip_local = false;
|
||||||
|
skip_authenticated = true;
|
||||||
|
routines {
|
||||||
|
spam-header {
|
||||||
|
header = "X-Spam-Flag";
|
||||||
|
value = "YES";
|
||||||
|
remove = 1;
|
||||||
|
}
|
||||||
|
fuzzy-hashes {
|
||||||
|
header = "X-Rspamd-Fuzzy";
|
||||||
|
}
|
||||||
|
authentication-results {
|
||||||
|
header = "Authentication-Results";
|
||||||
|
add_smtp_user = false;
|
||||||
|
remove = 1;
|
||||||
|
spf_symbols {
|
||||||
|
pass = "R_SPF_ALLOW";
|
||||||
|
fail = "R_SPF_FAIL";
|
||||||
|
softfail = "R_SPF_SOFTFAIL";
|
||||||
|
neutral = "R_SPF_NEUTRAL";
|
||||||
|
temperror = "R_SPF_DNSFAIL";
|
||||||
|
none = "R_SPF_NA";
|
||||||
|
permerror = "R_SPF_PERMFAIL";
|
||||||
|
}
|
||||||
|
dkim_symbols {
|
||||||
|
pass = "R_DKIM_ALLOW";
|
||||||
|
fail = "R_DKIM_REJECT";
|
||||||
|
temperror = "R_DKIM_TEMPFAIL";
|
||||||
|
none = "R_DKIM_NA";
|
||||||
|
permerror = "R_DKIM_PERMFAIL";
|
||||||
|
}
|
||||||
|
dmarc_symbols {
|
||||||
|
pass = "DMARC_POLICY_ALLOW";
|
||||||
|
permerror = "DMARC_BAD_POLICY";
|
||||||
|
temperror = "DMARC_DNSFAIL";
|
||||||
|
none = "DMARC_NA";
|
||||||
|
reject = "DMARC_POLICY_REJECT";
|
||||||
|
softfail = "DMARC_POLICY_SOFTFAIL";
|
||||||
|
quarantine = "DMARC_POLICY_QUARANTINE";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
47
Stable2.0/conf/local.d/mime_types.conf
Normal file
47
Stable2.0/conf/local.d/mime_types.conf
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
# Extensions that are treated as 'bad'
|
||||||
|
# Number is score multiply factor
|
||||||
|
bad_extensions = {
|
||||||
|
scr = 20,
|
||||||
|
lnk = 20,
|
||||||
|
exe = 20,
|
||||||
|
msi = 1,
|
||||||
|
msp = 1,
|
||||||
|
msu = 1,
|
||||||
|
jar = 2,
|
||||||
|
com = 20,
|
||||||
|
bat = 4,
|
||||||
|
cmd = 4,
|
||||||
|
ps1 = 4,
|
||||||
|
ace = 4,
|
||||||
|
arj = 4,
|
||||||
|
cab = 20,
|
||||||
|
vbs = 20,
|
||||||
|
hta = 4,
|
||||||
|
shs = 4,
|
||||||
|
wsc = 4,
|
||||||
|
wsf = 4,
|
||||||
|
iso = 8,
|
||||||
|
img = 8
|
||||||
|
};
|
||||||
|
|
||||||
|
# Extensions that are particularly penalized for archives
|
||||||
|
bad_archive_extensions = {
|
||||||
|
pptx = 0.5,
|
||||||
|
docx = 0.5,
|
||||||
|
xlsx = 0.5,
|
||||||
|
pdf = 1.0,
|
||||||
|
jar = 3,
|
||||||
|
js = 0.5,
|
||||||
|
vbs = 20,
|
||||||
|
exe = 20
|
||||||
|
};
|
||||||
|
|
||||||
|
# Used to detect another archive in archive
|
||||||
|
archive_extensions = {
|
||||||
|
zip = 1,
|
||||||
|
arj = 1,
|
||||||
|
rar = 1,
|
||||||
|
ace = 1,
|
||||||
|
7z = 1,
|
||||||
|
cab = 1
|
||||||
|
};
|
7
Stable2.0/conf/local.d/mime_types_group.conf
Normal file
7
Stable2.0/conf/local.d/mime_types_group.conf
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
symbols = {
|
||||||
|
"MIME_DOUBLE_BAD_EXTENSION" {
|
||||||
|
weight = 0; # This rule has dynamic weight up to 4.0
|
||||||
|
description = "Bad extension cloaking";
|
||||||
|
one_shot = true;
|
||||||
|
}
|
||||||
|
}
|
181
Stable2.0/conf/local.d/multimap.conf
Normal file
181
Stable2.0/conf/local.d/multimap.conf
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
RCPT_MAILCOW_DOMAIN {
|
||||||
|
type = "rcpt";
|
||||||
|
filter = "email:domain";
|
||||||
|
map = "redis://DOMAIN_MAP";
|
||||||
|
symbols_set = ["RCPT_MAILCOW_DOMAIN"];
|
||||||
|
}
|
||||||
|
|
||||||
|
WHITELISTED_FWD_HOST {
|
||||||
|
type = "ip";
|
||||||
|
map = "redis://WHITELISTED_FWD_HOST";
|
||||||
|
symbols_set = ["WHITELISTED_FWD_HOST"];
|
||||||
|
}
|
||||||
|
|
||||||
|
BULK_HEADER {
|
||||||
|
type = "content";
|
||||||
|
map = "${LOCAL_CONFDIR}/custom/bulk_header.map";
|
||||||
|
filter = "headers"
|
||||||
|
regexp = true;
|
||||||
|
symbols_set = ["BULK_HEADER"];
|
||||||
|
}
|
||||||
|
|
||||||
|
CHAT_VERSION_HEADER {
|
||||||
|
type = "header";
|
||||||
|
header = "Chat-Version";
|
||||||
|
map = "${LOCAL_CONFDIR}/custom/chat_versions.map";
|
||||||
|
regexp = true;
|
||||||
|
symbols_set = ["CHAT_VERSION_HEADER"];
|
||||||
|
}
|
||||||
|
|
||||||
|
BAD_HEADER {
|
||||||
|
type = "content";
|
||||||
|
map = "${LOCAL_CONFDIR}/custom/bad_header.map";
|
||||||
|
filter = "headers"
|
||||||
|
regexp = true;
|
||||||
|
symbols_set = ["BAD_HEADER"];
|
||||||
|
}
|
||||||
|
|
||||||
|
LOCAL_BL_ASN {
|
||||||
|
require_symbols = "!MAILCOW_WHITE";
|
||||||
|
type = "asn";
|
||||||
|
map = "${LOCAL_CONFDIR}/custom/bad_asn.map";
|
||||||
|
score = 5;
|
||||||
|
description = "Sender's ASN is on the local blacklist";
|
||||||
|
symbols_set = ["LOCAL_BL_ASN"];
|
||||||
|
}
|
||||||
|
|
||||||
|
GLOBAL_SMTP_FROM_WL {
|
||||||
|
type = "from";
|
||||||
|
map = "${LOCAL_CONFDIR}/custom/global_smtp_from_whitelist.map";
|
||||||
|
regexp = true;
|
||||||
|
score = -2050;
|
||||||
|
}
|
||||||
|
|
||||||
|
GLOBAL_SMTP_FROM_BL {
|
||||||
|
type = "from";
|
||||||
|
map = "${LOCAL_CONFDIR}/custom/global_smtp_from_blacklist.map";
|
||||||
|
regexp = true;
|
||||||
|
score = 2050;
|
||||||
|
}
|
||||||
|
|
||||||
|
GLOBAL_MIME_FROM_WL {
|
||||||
|
type = "header";
|
||||||
|
header = "from";
|
||||||
|
filter = "email:addr";
|
||||||
|
map = "${LOCAL_CONFDIR}/custom/global_mime_from_whitelist.map";
|
||||||
|
regexp = true;
|
||||||
|
score = -2050;
|
||||||
|
}
|
||||||
|
|
||||||
|
GLOBAL_MIME_FROM_BL {
|
||||||
|
type = "header";
|
||||||
|
header = "from";
|
||||||
|
filter = "email:addr";
|
||||||
|
map = "${LOCAL_CONFDIR}/custom/global_mime_from_blacklist.map";
|
||||||
|
regexp = true;
|
||||||
|
score = 2050;
|
||||||
|
}
|
||||||
|
|
||||||
|
GLOBAL_RCPT_WL {
|
||||||
|
type = "rcpt";
|
||||||
|
map = "${LOCAL_CONFDIR}/custom/global_rcpt_whitelist.map";
|
||||||
|
regexp = true;
|
||||||
|
prefilter = true;
|
||||||
|
action = "accept";
|
||||||
|
}
|
||||||
|
|
||||||
|
GLOBAL_RCPT_BL {
|
||||||
|
type = "rcpt";
|
||||||
|
map = "${LOCAL_CONFDIR}/custom/global_rcpt_blacklist.map";
|
||||||
|
regexp = true;
|
||||||
|
prefilter = true;
|
||||||
|
action = "reject";
|
||||||
|
}
|
||||||
|
|
||||||
|
SIEVE_HOST {
|
||||||
|
type = "ip";
|
||||||
|
map = "${LOCAL_CONFDIR}/custom/dovecot_trusted.map";
|
||||||
|
symbols_set = ["SIEVE_HOST"];
|
||||||
|
}
|
||||||
|
|
||||||
|
RSPAMD_HOST {
|
||||||
|
type = "ip";
|
||||||
|
map = "${LOCAL_CONFDIR}/custom/rspamd_trusted.map";
|
||||||
|
symbols_set = ["RSPAMD_HOST"];
|
||||||
|
}
|
||||||
|
|
||||||
|
MAILCOW_DOMAIN_HEADER_FROM {
|
||||||
|
type = "header";
|
||||||
|
header = "from";
|
||||||
|
filter = "email:domain";
|
||||||
|
map = "redis://DOMAIN_MAP";
|
||||||
|
}
|
||||||
|
|
||||||
|
IP_WHITELIST {
|
||||||
|
type = "ip";
|
||||||
|
map = "${LOCAL_CONFDIR}/custom/ip_wl.map";
|
||||||
|
symbols_set = ["IP_WHITELIST"];
|
||||||
|
score = -2050;
|
||||||
|
}
|
||||||
|
|
||||||
|
FISHY_TLD {
|
||||||
|
type = "from";
|
||||||
|
filter = "email:domain";
|
||||||
|
map = "${LOCAL_CONFDIR}/custom/fishy_tlds.map";
|
||||||
|
regexp = true;
|
||||||
|
score = 0.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
BAD_WORDS {
|
||||||
|
type = "content";
|
||||||
|
filter = "text";
|
||||||
|
map = "${LOCAL_CONFDIR}/custom/bad_words.map";
|
||||||
|
regexp = true;
|
||||||
|
score = 0.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
BAD_WORDS_DE {
|
||||||
|
type = "content";
|
||||||
|
filter = "text";
|
||||||
|
map = "${LOCAL_CONFDIR}/custom/bad_words_de.map";
|
||||||
|
regexp = true;
|
||||||
|
score = 0.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
BAD_LANG {
|
||||||
|
type = 'selector';
|
||||||
|
selector = 'languages';
|
||||||
|
map = "${LOCAL_CONFDIR}/custom/bad_languages.map";
|
||||||
|
symbols_set = ["LANG_FILTER"];
|
||||||
|
regexp = true;
|
||||||
|
score = 5.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
BAZAAR_ABUSE_CH {
|
||||||
|
type = "selector";
|
||||||
|
selector = "attachments(hex,md5)";
|
||||||
|
map = "https://bazaar.abuse.ch/export/txt/md5/recent/";
|
||||||
|
score = 10.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
URLHAUS_ABUSE_CH {
|
||||||
|
type = "selector";
|
||||||
|
selector = "urls";
|
||||||
|
map = "https://urlhaus.abuse.ch/downloads/text_online/";
|
||||||
|
score = 10.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
SMTP_LIMITED_ACCESS {
|
||||||
|
type = "user";
|
||||||
|
map = "redis://SMTP_LIMITED_ACCESS";
|
||||||
|
symbols_set = ["SMTP_LIMITED_ACCESS"];
|
||||||
|
}
|
||||||
|
|
||||||
|
BAD_SUBJECT_00 {
|
||||||
|
type = "header";
|
||||||
|
header = "subject";
|
||||||
|
regexp = true;
|
||||||
|
map = "http://fuzzy.mailcow.email/bad-subject-regex.txt";
|
||||||
|
score = 6.0;
|
||||||
|
symbols_set = ["BAD_SUBJECT_00"];
|
||||||
|
}
|
7
Stable2.0/conf/local.d/mx_check.conf
Normal file
7
Stable2.0/conf/local.d/mx_check.conf
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
timeout = 8.0;
|
||||||
|
symbol_bad_mx = "MX_INVALID";
|
||||||
|
symbol_no_mx = "MX_MISSING";
|
||||||
|
symbol_good_mx = "MX_GOOD";
|
||||||
|
expire = 86400;
|
||||||
|
key_prefix = "rmx";
|
||||||
|
enabled = true;
|
9
Stable2.0/conf/local.d/options.inc
Normal file
9
Stable2.0/conf/local.d/options.inc
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
dns {
|
||||||
|
enable_dnssec = true;
|
||||||
|
}
|
||||||
|
map_watch_interval = 30s;
|
||||||
|
disable_monitoring = true;
|
||||||
|
# In case a task times out (like DNS lookup), soft reject the message
|
||||||
|
# instead of silently accepting the message without further processing.
|
||||||
|
soft_reject_on_timeout = true;
|
||||||
|
local_addrs = /etc/rspamd/custom/mailcow_networks.map;
|
1
Stable2.0/conf/local.d/phishing.conf
Normal file
1
Stable2.0/conf/local.d/phishing.conf
Normal file
@ -0,0 +1 @@
|
|||||||
|
phishtank_enabled = false;
|
26
Stable2.0/conf/local.d/policies_group.conf
Normal file
26
Stable2.0/conf/local.d/policies_group.conf
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
symbols = {
|
||||||
|
"ARC_REJECT" {
|
||||||
|
score = 0.1;
|
||||||
|
}
|
||||||
|
"R_SPF_FAIL" {
|
||||||
|
score = 8.0;
|
||||||
|
}
|
||||||
|
"R_SPF_PERMFAIL" {
|
||||||
|
score = 8.0;
|
||||||
|
}
|
||||||
|
"R_SPF_SOFTFAIL" {
|
||||||
|
score = 0.1;
|
||||||
|
}
|
||||||
|
"R_DKIM_REJECT" {
|
||||||
|
score = 8.0;
|
||||||
|
}
|
||||||
|
"DMARC_POLICY_REJECT" {
|
||||||
|
weight = 16.0;
|
||||||
|
}
|
||||||
|
"DMARC_POLICY_QUARANTINE" {
|
||||||
|
weight = 8.0;
|
||||||
|
}
|
||||||
|
"DMARC_POLICY_SOFTFAIL" {
|
||||||
|
weight = 0.1;
|
||||||
|
}
|
||||||
|
}
|
9
Stable2.0/conf/local.d/ratelimit.conf
Normal file
9
Stable2.0/conf/local.d/ratelimit.conf
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# Uncomment below to apply the ratelimits globally. Use Ratelimits inside mailcow UI to overwrite them for a specific domain/mailbox.
|
||||||
|
# rates {
|
||||||
|
# # Format: "1 / 1h" or "20 / 1m" etc.
|
||||||
|
# to = "100 / 1s";
|
||||||
|
# to_ip = "100 / 1s";
|
||||||
|
# to_ip_from = "100 / 1s";
|
||||||
|
# bounce_to = "100 / 1h";
|
||||||
|
# bounce_to_ip = "7 / 1m";
|
||||||
|
# }
|
26
Stable2.0/conf/local.d/rbl.conf
Normal file
26
Stable2.0/conf/local.d/rbl.conf
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
rbls {
|
||||||
|
interserver_ip {
|
||||||
|
symbol = "RBL_INTERSERVER_IP";
|
||||||
|
rbl = "rbl.interserver.net";
|
||||||
|
from = true;
|
||||||
|
ipv6 = false;
|
||||||
|
returncodes {
|
||||||
|
RBL_INTERSERVER_BAD_IP = "127.0.0.2";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
interserver_uri {
|
||||||
|
symbol = "RBL_INTERSERVER_URI";
|
||||||
|
rbl = "rbluri.interserver.net";
|
||||||
|
ignore_defaults = true;
|
||||||
|
no_ip = true;
|
||||||
|
dkim = true;
|
||||||
|
emails = true;
|
||||||
|
urls = true;
|
||||||
|
returncodes = {
|
||||||
|
RBL_INTERSERVER_BAD_URI = "127.0.0.2";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.include(try=true,override=true,priority=5) "$LOCAL_CONFDIR/custom/dqs-rbl.conf"
|
||||||
|
|
||||||
|
}
|
277
Stable2.0/conf/local.d/rbl_group.conf
Normal file
277
Stable2.0/conf/local.d/rbl_group.conf
Normal file
@ -0,0 +1,277 @@
|
|||||||
|
symbols = {
|
||||||
|
"RBL_UCEPROTECT_LEVEL1" {
|
||||||
|
score = 3.5;
|
||||||
|
}
|
||||||
|
"RBL_UCEPROTECT_LEVEL2" {
|
||||||
|
score = 1.5;
|
||||||
|
}
|
||||||
|
"RECEIVED_SPAMHAUS_XBL" {
|
||||||
|
weight = 0.0;
|
||||||
|
description = "Received address is listed in ZEN XBL";
|
||||||
|
}
|
||||||
|
"RBL_INTERSERVER_BAD_URI" {
|
||||||
|
score = 4.0;
|
||||||
|
description = "Listed on Interserver RBL";
|
||||||
|
}
|
||||||
|
"RBL_INTERSERVER_BAD_IP" {
|
||||||
|
score = 4.0;
|
||||||
|
description = "Listed on Interserver RBL";
|
||||||
|
}
|
||||||
|
|
||||||
|
"SPAMHAUS_ZEN" {
|
||||||
|
weight = 7.0;
|
||||||
|
}
|
||||||
|
"SH_AUTHBL_RECEIVED" {
|
||||||
|
weight = 4.0;
|
||||||
|
}
|
||||||
|
"RBL_DBL_SPAM" {
|
||||||
|
weight = 7.0;
|
||||||
|
}
|
||||||
|
"RBL_DBL_PHISH" {
|
||||||
|
weight = 7.0;
|
||||||
|
}
|
||||||
|
"RBL_DBL_MALWARE" {
|
||||||
|
weight = 7.0;
|
||||||
|
}
|
||||||
|
"RBL_DBL_BOTNET" {
|
||||||
|
weight = 7.0;
|
||||||
|
}
|
||||||
|
"RBL_DBL_ABUSED_SPAM" {
|
||||||
|
weight = 3.0;
|
||||||
|
}
|
||||||
|
"RBL_DBL_ABUSED_PHISH" {
|
||||||
|
weight = 3.0;
|
||||||
|
}
|
||||||
|
"RBL_DBL_ABUSED_MALWARE" {
|
||||||
|
weight = 3.0;
|
||||||
|
}
|
||||||
|
"RBL_DBL_ABUSED_BOTNET" {
|
||||||
|
weight = 3.0;
|
||||||
|
}
|
||||||
|
"RBL_ZRD_VERY_FRESH_DOMAIN" {
|
||||||
|
weight = 7.0;
|
||||||
|
}
|
||||||
|
"RBL_ZRD_FRESH_DOMAIN" {
|
||||||
|
weight = 4.0;
|
||||||
|
}
|
||||||
|
"ZRD_VERY_FRESH_DOMAIN" {
|
||||||
|
weight = 7.0;
|
||||||
|
}
|
||||||
|
"ZRD_FRESH_DOMAIN" {
|
||||||
|
weight = 4.0;
|
||||||
|
}
|
||||||
|
"SH_EMAIL_DBL" {
|
||||||
|
weight = 7.0;
|
||||||
|
}
|
||||||
|
"SH_EMAIL_DBL_ABUSED" {
|
||||||
|
weight = 7.0;
|
||||||
|
}
|
||||||
|
"SH_EMAIL_ZRD_VERY_FRESH_DOMAIN" {
|
||||||
|
weight = 7.0;
|
||||||
|
}
|
||||||
|
"SH_EMAIL_ZRD_FRESH_DOMAIN" {
|
||||||
|
weight = 4.0;
|
||||||
|
}
|
||||||
|
"RBL_DBL_DONT_QUERY_IPS" {
|
||||||
|
weight = 0.0;
|
||||||
|
}
|
||||||
|
"RBL_ZRD_DONT_QUERY_IPS" {
|
||||||
|
weight = 0.0;
|
||||||
|
}
|
||||||
|
"SH_EMAIL_ZRD_DONT_QUERY_IPS" {
|
||||||
|
weight = 0.0;
|
||||||
|
}
|
||||||
|
"SH_EMAIL_DBL_DONT_QUERY_IPS" {
|
||||||
|
weight = 0.0;
|
||||||
|
}
|
||||||
|
"DBL" {
|
||||||
|
weight = 0.0;
|
||||||
|
description = "DBL unknown result";
|
||||||
|
groups = ["spamhaus"];
|
||||||
|
}
|
||||||
|
"DBL_SPAM" {
|
||||||
|
weight = 7;
|
||||||
|
description = "DBL uribl spam";
|
||||||
|
groups = ["spamhaus"];
|
||||||
|
}
|
||||||
|
"DBL_PHISH" {
|
||||||
|
weight = 7;
|
||||||
|
description = "DBL uribl phishing";
|
||||||
|
groups = ["spamhaus"];
|
||||||
|
}
|
||||||
|
"DBL_MALWARE" {
|
||||||
|
weight = 7;
|
||||||
|
description = "DBL uribl malware";
|
||||||
|
groups = ["spamhaus"];
|
||||||
|
}
|
||||||
|
"DBL_BOTNET" {
|
||||||
|
weight = 7;
|
||||||
|
description = "DBL uribl botnet C&C domain";
|
||||||
|
groups = ["spamhaus"];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
"DBLABUSED_SPAM_FULLURLS" {
|
||||||
|
weight = 5.5;
|
||||||
|
description = "DBL uribl abused legit spam";
|
||||||
|
groups = ["spamhaus"];
|
||||||
|
}
|
||||||
|
"DBLABUSED_PHISH_FULLURLS" {
|
||||||
|
weight = 5.5;
|
||||||
|
description = "DBL uribl abused legit phish";
|
||||||
|
groups = ["spamhaus"];
|
||||||
|
}
|
||||||
|
"DBLABUSED_MALWARE_FULLURLS" {
|
||||||
|
weight = 5.5;
|
||||||
|
description = "DBL uribl abused legit malware";
|
||||||
|
groups = ["spamhaus"];
|
||||||
|
}
|
||||||
|
"DBLABUSED_BOTNET_FULLURLS" {
|
||||||
|
weight = 5.5;
|
||||||
|
description = "DBL uribl abused legit botnet";
|
||||||
|
groups = ["spamhaus"];
|
||||||
|
}
|
||||||
|
|
||||||
|
"DBL_ABUSE" {
|
||||||
|
weight = 5.5;
|
||||||
|
description = "DBL uribl abused legit spam";
|
||||||
|
groups = ["spamhaus"];
|
||||||
|
}
|
||||||
|
"DBL_ABUSE_REDIR" {
|
||||||
|
weight = 1.5;
|
||||||
|
description = "DBL uribl abused spammed redirector domain";
|
||||||
|
groups = ["spamhaus"];
|
||||||
|
}
|
||||||
|
"DBL_ABUSE_PHISH" {
|
||||||
|
weight = 5.5;
|
||||||
|
description = "DBL uribl abused legit phish";
|
||||||
|
groups = ["spamhaus"];
|
||||||
|
}
|
||||||
|
"DBL_ABUSE_MALWARE" {
|
||||||
|
weight = 5.5;
|
||||||
|
description = "DBL uribl abused legit malware";
|
||||||
|
groups = ["spamhaus"];
|
||||||
|
}
|
||||||
|
"DBL_ABUSE_BOTNET" {
|
||||||
|
weight = 5.5;
|
||||||
|
description = "DBL uribl abused legit botnet C&C";
|
||||||
|
groups = ["spamhaus"];
|
||||||
|
}
|
||||||
|
"DBL_PROHIBIT" {
|
||||||
|
weight = 0.0;
|
||||||
|
description = "DBL uribl IP queries prohibited!";
|
||||||
|
groups = ["spamhaus"];
|
||||||
|
}
|
||||||
|
"DBL_BLOCKED_OPENRESOLVER" {
|
||||||
|
weight = 0.0;
|
||||||
|
description = "You are querying Spamhaus from an open resolver, please see https://www.spamhaus.org/returnc/pub/";
|
||||||
|
groups = ["spamhaus"];
|
||||||
|
}
|
||||||
|
"DBL_BLOCKED" {
|
||||||
|
weight = 0.0;
|
||||||
|
description = "You are exceeding the query limit, please see https://www.spamhaus.org/returnc/vol/";
|
||||||
|
groups = ["spamhaus"];
|
||||||
|
}
|
||||||
|
"SPAMHAUS_ZEN_URIBL" {
|
||||||
|
weight = 0.0;
|
||||||
|
description = "Spamhaus ZEN URIBL: Filtered result";
|
||||||
|
groups = ["spamhaus"];
|
||||||
|
}
|
||||||
|
"URIBL_SBL" {
|
||||||
|
weight = 6.5;
|
||||||
|
description = "A domain in the message body resolves to an IP listed in Spamhaus SBL";
|
||||||
|
one_shot = true;
|
||||||
|
groups = ["spamhaus"];
|
||||||
|
}
|
||||||
|
"URIBL_SBL_CSS" {
|
||||||
|
weight = 6.5;
|
||||||
|
description = "A domain in the message body resolves to an IP listed in Spamhaus SBL CSS";
|
||||||
|
one_shot = true;
|
||||||
|
groups = ["spamhaus"];
|
||||||
|
}
|
||||||
|
"URIBL_PBL" {
|
||||||
|
weight = 0.01;
|
||||||
|
description = "A domain in the message body resolves to an IP listed in Spamhaus PBL";
|
||||||
|
one_shot = true;
|
||||||
|
groups = ["spamhaus"];
|
||||||
|
}
|
||||||
|
"URIBL_DROP" {
|
||||||
|
weight = 6.5;
|
||||||
|
description = "A domain in the message body resolves to an IP listed in Spamhaus DROP";
|
||||||
|
one_shot = true;
|
||||||
|
groups = ["spamhaus"];
|
||||||
|
}
|
||||||
|
"URIBL_XBL" {
|
||||||
|
weight = 5.0;
|
||||||
|
description = "A domain in the message body resolves to an IP listed in Spamhaus XBL";
|
||||||
|
one_shot = true;
|
||||||
|
groups = ["spamhaus"];
|
||||||
|
}
|
||||||
|
"SPAMHAUS_SBL_URL" {
|
||||||
|
weight = 6.5;
|
||||||
|
description = "A numeric URL in the message body is listed in Spamhaus SBL";
|
||||||
|
one_shot = true;
|
||||||
|
groups = ["spamhaus"];
|
||||||
|
}
|
||||||
|
|
||||||
|
"SH_HBL_EMAIL" {
|
||||||
|
weight = 7;
|
||||||
|
description = "Email listed in HBL";
|
||||||
|
groups = ["spamhaus"];
|
||||||
|
}
|
||||||
|
|
||||||
|
"SH_HBL_FILE_MALICIOUS" {
|
||||||
|
weight = 7;
|
||||||
|
description = "An attachment hash is listed in Spamhaus HBL as malicious";
|
||||||
|
groups = ["spamhaus"];
|
||||||
|
}
|
||||||
|
|
||||||
|
"SH_HBL_FILE_SUSPICIOUS" {
|
||||||
|
weight = 5;
|
||||||
|
description = "An attachment hash is listed in Spamhaus HBL as suspicious";
|
||||||
|
groups = ["spamhaus"];
|
||||||
|
}
|
||||||
|
|
||||||
|
"RBL_SPAMHAUS_CW_BTC" {
|
||||||
|
score = 7;
|
||||||
|
description = "Bitcoin found in Spamhaus cryptowallet list";
|
||||||
|
groups = ["spamhaus"];
|
||||||
|
}
|
||||||
|
|
||||||
|
"RBL_SPAMHAUS_CW_ETH" {
|
||||||
|
score = 7;
|
||||||
|
description = "Ethereum found in Spamhaus cryptowallet list";
|
||||||
|
groups = ["spamhaus"];
|
||||||
|
}
|
||||||
|
|
||||||
|
"RBL_SPAMHAUS_CW_BCH" {
|
||||||
|
score = 7;
|
||||||
|
description = "Bitcoinhash found in Spamhaus cryptowallet list";
|
||||||
|
groups = ["spamhaus"];
|
||||||
|
}
|
||||||
|
|
||||||
|
"RBL_SPAMHAUS_CW_XMR" {
|
||||||
|
score = 7;
|
||||||
|
description = "Monero found in Spamhaus cryptowallet list";
|
||||||
|
groups = ["spamhaus"];
|
||||||
|
}
|
||||||
|
|
||||||
|
"RBL_SPAMHAUS_CW_LTC" {
|
||||||
|
score = 7;
|
||||||
|
description = "Litecoin found in Spamhaus cryptowallet list";
|
||||||
|
groups = ["spamhaus"];
|
||||||
|
}
|
||||||
|
|
||||||
|
"RBL_SPAMHAUS_CW_XRP" {
|
||||||
|
score = 7;
|
||||||
|
description = "Ripple found in Spamhaus cryptowallet list";
|
||||||
|
groups = ["spamhaus"];
|
||||||
|
}
|
||||||
|
|
||||||
|
"RBL_SPAMHAUS_HBL_URL" {
|
||||||
|
score = 7;
|
||||||
|
description = "URL found in spamhaus HBL blocklist";
|
||||||
|
groups = ["spamhaus"];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
2
Stable2.0/conf/local.d/redis.conf
Normal file
2
Stable2.0/conf/local.d/redis.conf
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
servers = "redis:6379";
|
||||||
|
timeout = 10;
|
9
Stable2.0/conf/local.d/reputation.conf
Normal file
9
Stable2.0/conf/local.d/reputation.conf
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
rules {
|
||||||
|
ip_reputation = {
|
||||||
|
selector "ip" {
|
||||||
|
}
|
||||||
|
backend "redis" {
|
||||||
|
}
|
||||||
|
symbol = "IP_REPUTATION";
|
||||||
|
}
|
||||||
|
}
|
1
Stable2.0/conf/local.d/spamassassin.conf
Normal file
1
Stable2.0/conf/local.d/spamassassin.conf
Normal file
@ -0,0 +1 @@
|
|||||||
|
ruleset = "/etc/rspamd/custom/sa-rules";
|
26
Stable2.0/conf/local.d/statistic.conf
Normal file
26
Stable2.0/conf/local.d/statistic.conf
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
classifier "bayes" {
|
||||||
|
# name = "custom"; # 'name' parameter must be set if multiple classifiers are defined
|
||||||
|
learn_condition = 'return require("lua_bayes_learn").can_learn';
|
||||||
|
new_schema = true;
|
||||||
|
tokenizer {
|
||||||
|
name = "osb";
|
||||||
|
}
|
||||||
|
backend = "redis";
|
||||||
|
min_tokens = 11;
|
||||||
|
min_learns = 5;
|
||||||
|
expire = 7776000;
|
||||||
|
statfile {
|
||||||
|
symbol = "BAYES_HAM";
|
||||||
|
spam = false;
|
||||||
|
}
|
||||||
|
statfile {
|
||||||
|
symbol = "BAYES_SPAM";
|
||||||
|
spam = true;
|
||||||
|
}
|
||||||
|
autolearn {
|
||||||
|
spam_threshold = 12.0;
|
||||||
|
ham_threshold = -4.5;
|
||||||
|
check_balance = true;
|
||||||
|
min_balance = 0.9;
|
||||||
|
}
|
||||||
|
}
|
10
Stable2.0/conf/local.d/statistics_group.conf
Normal file
10
Stable2.0/conf/local.d/statistics_group.conf
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
symbols = {
|
||||||
|
"BAYES_SPAM" {
|
||||||
|
weight = 4.5;
|
||||||
|
description = "Message probably spam, probability: ";
|
||||||
|
}
|
||||||
|
"BAYES_HAM" {
|
||||||
|
weight = -5.5;
|
||||||
|
description = "Message probably ham, probability: ";
|
||||||
|
}
|
||||||
|
}
|
16
Stable2.0/conf/lua/ratelimit.lua
Normal file
16
Stable2.0/conf/lua/ratelimit.lua
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
local custom_keywords = {}
|
||||||
|
|
||||||
|
custom_keywords.mailcow = function(task)
|
||||||
|
local rspamd_logger = require "rspamd_logger"
|
||||||
|
local dyn_rl_symbol = task:get_symbol("DYN_RL")
|
||||||
|
if dyn_rl_symbol then
|
||||||
|
local rl_value = dyn_rl_symbol[1].options[1]
|
||||||
|
local rl_object = dyn_rl_symbol[1].options[2]
|
||||||
|
if rl_value and rl_object then
|
||||||
|
rspamd_logger.infox(rspamd_config, "DYN_RL symbol has value %s for object %s, returning %s...", rl_value, rl_object, "rs_dynrl_" .. rl_object)
|
||||||
|
return "rs_dynrl_" .. rl_object, rl_value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return custom_keywords
|
701
Stable2.0/conf/lua/rspamd.local.lua
Normal file
701
Stable2.0/conf/lua/rspamd.local.lua
Normal file
@ -0,0 +1,701 @@
|
|||||||
|
rspamd_config.MAILCOW_AUTH = {
|
||||||
|
callback = function(task)
|
||||||
|
local uname = task:get_user()
|
||||||
|
if uname then
|
||||||
|
return 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
|
local monitoring_hosts = rspamd_config:add_map{
|
||||||
|
url = "/etc/rspamd/custom/monitoring_nolog.map",
|
||||||
|
description = "Monitoring hosts",
|
||||||
|
type = "regexp"
|
||||||
|
}
|
||||||
|
|
||||||
|
rspamd_config:register_symbol({
|
||||||
|
name = 'SMTP_ACCESS',
|
||||||
|
type = 'postfilter',
|
||||||
|
callback = function(task)
|
||||||
|
local util = require("rspamd_util")
|
||||||
|
local rspamd_logger = require "rspamd_logger"
|
||||||
|
local rspamd_ip = require 'rspamd_ip'
|
||||||
|
local uname = task:get_user()
|
||||||
|
local limited_access = task:get_symbol("SMTP_LIMITED_ACCESS")
|
||||||
|
|
||||||
|
if not uname then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
if not limited_access then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local hash_key = 'SMTP_ALLOW_NETS_' .. uname
|
||||||
|
|
||||||
|
local redis_params = rspamd_parse_redis_server('smtp_access')
|
||||||
|
local ip = task:get_from_ip()
|
||||||
|
|
||||||
|
if ip == nil or not ip:is_valid() then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local from_ip_string = tostring(ip)
|
||||||
|
smtp_access_table = {from_ip_string}
|
||||||
|
|
||||||
|
local maxbits = 128
|
||||||
|
local minbits = 32
|
||||||
|
if ip:get_version() == 4 then
|
||||||
|
maxbits = 32
|
||||||
|
minbits = 8
|
||||||
|
end
|
||||||
|
for i=maxbits,minbits,-1 do
|
||||||
|
local nip = ip:apply_mask(i):to_string() .. "/" .. i
|
||||||
|
table.insert(smtp_access_table, nip)
|
||||||
|
end
|
||||||
|
local function smtp_access_cb(err, data)
|
||||||
|
if err then
|
||||||
|
rspamd_logger.infox(rspamd_config, "smtp_access query request for ip %s returned invalid or empty data (\"%s\") or error (\"%s\")", ip, data, err)
|
||||||
|
return false
|
||||||
|
else
|
||||||
|
rspamd_logger.infox(rspamd_config, "checking ip %s for smtp_access in %s", from_ip_string, hash_key)
|
||||||
|
for k,v in pairs(data) do
|
||||||
|
if (v and v ~= userdata and v == '1') then
|
||||||
|
rspamd_logger.infox(rspamd_config, "found ip in smtp_access map")
|
||||||
|
task:insert_result(true, 'SMTP_ACCESS', 0.0, from_ip_string)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
rspamd_logger.infox(rspamd_config, "couldnt find ip in smtp_access map")
|
||||||
|
task:insert_result(true, 'SMTP_ACCESS', 999.0, from_ip_string)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
table.insert(smtp_access_table, 1, hash_key)
|
||||||
|
local redis_ret_user = rspamd_redis_make_request(task,
|
||||||
|
redis_params, -- connect params
|
||||||
|
hash_key, -- hash key
|
||||||
|
false, -- is write
|
||||||
|
smtp_access_cb, --callback
|
||||||
|
'HMGET', -- command
|
||||||
|
smtp_access_table -- arguments
|
||||||
|
)
|
||||||
|
if not redis_ret_user then
|
||||||
|
rspamd_logger.infox(rspamd_config, "cannot check smtp_access redis map")
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
priority = 10
|
||||||
|
})
|
||||||
|
|
||||||
|
rspamd_config:register_symbol({
|
||||||
|
name = 'POSTMASTER_HANDLER',
|
||||||
|
type = 'prefilter',
|
||||||
|
callback = function(task)
|
||||||
|
local rcpts = task:get_recipients('smtp')
|
||||||
|
local rspamd_logger = require "rspamd_logger"
|
||||||
|
local lua_util = require "lua_util"
|
||||||
|
local from = task:get_from(1)
|
||||||
|
|
||||||
|
-- not applying to mails with more than one rcpt to avoid bypassing filters by addressing postmaster
|
||||||
|
if rcpts and #rcpts == 1 then
|
||||||
|
for _,rcpt in ipairs(rcpts) do
|
||||||
|
local rcpt_split = rspamd_str_split(rcpt['addr'], '@')
|
||||||
|
if #rcpt_split == 2 then
|
||||||
|
if rcpt_split[1] == 'postmaster' then
|
||||||
|
task:set_pre_result('accept', 'whitelisting postmaster smtp rcpt')
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if from then
|
||||||
|
for _,fr in ipairs(from) do
|
||||||
|
local fr_split = rspamd_str_split(fr['addr'], '@')
|
||||||
|
if #fr_split == 2 then
|
||||||
|
if fr_split[1] == 'postmaster' and task:get_user() then
|
||||||
|
-- no whitelist, keep signatures
|
||||||
|
task:insert_result(true, 'POSTMASTER_FROM', -2500.0)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end,
|
||||||
|
priority = 10
|
||||||
|
})
|
||||||
|
|
||||||
|
rspamd_config:register_symbol({
|
||||||
|
name = 'KEEP_SPAM',
|
||||||
|
type = 'prefilter',
|
||||||
|
callback = function(task)
|
||||||
|
local util = require("rspamd_util")
|
||||||
|
local rspamd_logger = require "rspamd_logger"
|
||||||
|
local rspamd_ip = require 'rspamd_ip'
|
||||||
|
local uname = task:get_user()
|
||||||
|
|
||||||
|
if uname then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local redis_params = rspamd_parse_redis_server('keep_spam')
|
||||||
|
local ip = task:get_from_ip()
|
||||||
|
|
||||||
|
if ip == nil or not ip:is_valid() then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local from_ip_string = tostring(ip)
|
||||||
|
ip_check_table = {from_ip_string}
|
||||||
|
|
||||||
|
local maxbits = 128
|
||||||
|
local minbits = 32
|
||||||
|
if ip:get_version() == 4 then
|
||||||
|
maxbits = 32
|
||||||
|
minbits = 8
|
||||||
|
end
|
||||||
|
for i=maxbits,minbits,-1 do
|
||||||
|
local nip = ip:apply_mask(i):to_string() .. "/" .. i
|
||||||
|
table.insert(ip_check_table, nip)
|
||||||
|
end
|
||||||
|
local function keep_spam_cb(err, data)
|
||||||
|
if err then
|
||||||
|
rspamd_logger.infox(rspamd_config, "keep_spam query request for ip %s returned invalid or empty data (\"%s\") or error (\"%s\")", ip, data, err)
|
||||||
|
return false
|
||||||
|
else
|
||||||
|
for k,v in pairs(data) do
|
||||||
|
if (v and v ~= userdata and v == '1') then
|
||||||
|
rspamd_logger.infox(rspamd_config, "found ip in keep_spam map, setting pre-result")
|
||||||
|
task:set_pre_result('accept', 'ip matched with forward hosts')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
table.insert(ip_check_table, 1, 'KEEP_SPAM')
|
||||||
|
local redis_ret_user = rspamd_redis_make_request(task,
|
||||||
|
redis_params, -- connect params
|
||||||
|
'KEEP_SPAM', -- hash key
|
||||||
|
false, -- is write
|
||||||
|
keep_spam_cb, --callback
|
||||||
|
'HMGET', -- command
|
||||||
|
ip_check_table -- arguments
|
||||||
|
)
|
||||||
|
if not redis_ret_user then
|
||||||
|
rspamd_logger.infox(rspamd_config, "cannot check keep_spam redis map")
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
priority = 19
|
||||||
|
})
|
||||||
|
|
||||||
|
rspamd_config:register_symbol({
|
||||||
|
name = 'TLS_HEADER',
|
||||||
|
type = 'postfilter',
|
||||||
|
callback = function(task)
|
||||||
|
local rspamd_logger = require "rspamd_logger"
|
||||||
|
local tls_tag = task:get_request_header('TLS-Version')
|
||||||
|
if type(tls_tag) == 'nil' then
|
||||||
|
task:set_milter_reply({
|
||||||
|
add_headers = {['X-Last-TLS-Session-Version'] = 'None'}
|
||||||
|
})
|
||||||
|
else
|
||||||
|
task:set_milter_reply({
|
||||||
|
add_headers = {['X-Last-TLS-Session-Version'] = tostring(tls_tag)}
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
priority = 12
|
||||||
|
})
|
||||||
|
|
||||||
|
rspamd_config:register_symbol({
|
||||||
|
name = 'TAG_MOO',
|
||||||
|
type = 'postfilter',
|
||||||
|
callback = function(task)
|
||||||
|
local util = require("rspamd_util")
|
||||||
|
local rspamd_logger = require "rspamd_logger"
|
||||||
|
local redis_params = rspamd_parse_redis_server('taghandler')
|
||||||
|
local rspamd_http = require "rspamd_http"
|
||||||
|
local rcpts = task:get_recipients('smtp')
|
||||||
|
local lua_util = require "lua_util"
|
||||||
|
|
||||||
|
local tagged_rcpt = task:get_symbol("TAGGED_RCPT")
|
||||||
|
local mailcow_domain = task:get_symbol("RCPT_MAILCOW_DOMAIN")
|
||||||
|
|
||||||
|
local function remove_moo_tag()
|
||||||
|
local moo_tag_header = task:get_header('X-Moo-Tag', false)
|
||||||
|
if moo_tag_header then
|
||||||
|
task:set_milter_reply({
|
||||||
|
remove_headers = {['X-Moo-Tag'] = 0},
|
||||||
|
})
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
if tagged_rcpt and tagged_rcpt[1].options and mailcow_domain then
|
||||||
|
local tag = tagged_rcpt[1].options[1]
|
||||||
|
rspamd_logger.infox("found tag: %s", tag)
|
||||||
|
local action = task:get_metric_action('default')
|
||||||
|
rspamd_logger.infox("metric action now: %s", action)
|
||||||
|
|
||||||
|
if action ~= 'no action' and action ~= 'greylist' then
|
||||||
|
rspamd_logger.infox("skipping tag handler for action: %s", action)
|
||||||
|
remove_moo_tag()
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local function http_callback(err_message, code, body, headers)
|
||||||
|
if body ~= nil and body ~= "" then
|
||||||
|
rspamd_logger.infox(rspamd_config, "expanding rcpt to \"%s\"", body)
|
||||||
|
|
||||||
|
local function tag_callback_subject(err, data)
|
||||||
|
if err or type(data) ~= 'string' then
|
||||||
|
rspamd_logger.infox(rspamd_config, "subject tag handler rcpt %s returned invalid or empty data (\"%s\") or error (\"%s\") - trying subfolder tag handler...", body, data, err)
|
||||||
|
|
||||||
|
local function tag_callback_subfolder(err, data)
|
||||||
|
if err or type(data) ~= 'string' then
|
||||||
|
rspamd_logger.infox(rspamd_config, "subfolder tag handler for rcpt %s returned invalid or empty data (\"%s\") or error (\"%s\")", body, data, err)
|
||||||
|
remove_moo_tag()
|
||||||
|
else
|
||||||
|
rspamd_logger.infox("Add X-Moo-Tag header")
|
||||||
|
task:set_milter_reply({
|
||||||
|
add_headers = {['X-Moo-Tag'] = 'YES'}
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local redis_ret_subfolder = rspamd_redis_make_request(task,
|
||||||
|
redis_params, -- connect params
|
||||||
|
body, -- hash key
|
||||||
|
false, -- is write
|
||||||
|
tag_callback_subfolder, --callback
|
||||||
|
'HGET', -- command
|
||||||
|
{'RCPT_WANTS_SUBFOLDER_TAG', body} -- arguments
|
||||||
|
)
|
||||||
|
if not redis_ret_subfolder then
|
||||||
|
rspamd_logger.infox(rspamd_config, "cannot make request to load tag handler for rcpt")
|
||||||
|
remove_moo_tag()
|
||||||
|
end
|
||||||
|
|
||||||
|
else
|
||||||
|
rspamd_logger.infox("user wants subject modified for tagged mail")
|
||||||
|
local sbj = task:get_header('Subject')
|
||||||
|
new_sbj = '=?UTF-8?B?' .. tostring(util.encode_base64('[' .. tag .. '] ' .. sbj)) .. '?='
|
||||||
|
task:set_milter_reply({
|
||||||
|
remove_headers = {
|
||||||
|
['Subject'] = 1,
|
||||||
|
['X-Moo-Tag'] = 0
|
||||||
|
},
|
||||||
|
add_headers = {['Subject'] = new_sbj}
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local redis_ret_subject = rspamd_redis_make_request(task,
|
||||||
|
redis_params, -- connect params
|
||||||
|
body, -- hash key
|
||||||
|
false, -- is write
|
||||||
|
tag_callback_subject, --callback
|
||||||
|
'HGET', -- command
|
||||||
|
{'RCPT_WANTS_SUBJECT_TAG', body} -- arguments
|
||||||
|
)
|
||||||
|
if not redis_ret_subject then
|
||||||
|
rspamd_logger.infox(rspamd_config, "cannot make request to load tag handler for rcpt")
|
||||||
|
remove_moo_tag()
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if rcpts and #rcpts == 1 then
|
||||||
|
for _,rcpt in ipairs(rcpts) do
|
||||||
|
local rcpt_split = rspamd_str_split(rcpt['addr'], '@')
|
||||||
|
if #rcpt_split == 2 then
|
||||||
|
if rcpt_split[1] == 'postmaster' then
|
||||||
|
rspamd_logger.infox(rspamd_config, "not expanding postmaster alias")
|
||||||
|
remove_moo_tag()
|
||||||
|
else
|
||||||
|
rspamd_http.request({
|
||||||
|
task=task,
|
||||||
|
url='http://nginx:8081/aliasexp.php',
|
||||||
|
body='',
|
||||||
|
callback=http_callback,
|
||||||
|
headers={Rcpt=rcpt['addr']},
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
remove_moo_tag()
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
priority = 19
|
||||||
|
})
|
||||||
|
|
||||||
|
rspamd_config:register_symbol({
|
||||||
|
name = 'BCC',
|
||||||
|
type = 'postfilter',
|
||||||
|
callback = function(task)
|
||||||
|
local util = require("rspamd_util")
|
||||||
|
local rspamd_http = require "rspamd_http"
|
||||||
|
local rspamd_logger = require "rspamd_logger"
|
||||||
|
|
||||||
|
local from_table = {}
|
||||||
|
local rcpt_table = {}
|
||||||
|
|
||||||
|
if task:has_symbol('ENCRYPTED_CHAT') then
|
||||||
|
return -- stop
|
||||||
|
end
|
||||||
|
|
||||||
|
local send_mail = function(task, bcc_dest)
|
||||||
|
local lua_smtp = require "lua_smtp"
|
||||||
|
local function sendmail_cb(ret, err)
|
||||||
|
if not ret then
|
||||||
|
rspamd_logger.errx(task, 'BCC SMTP ERROR: %s', err)
|
||||||
|
else
|
||||||
|
rspamd_logger.infox(rspamd_config, "BCC SMTP SUCCESS TO %s", bcc_dest)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if not bcc_dest then
|
||||||
|
return -- stop
|
||||||
|
end
|
||||||
|
-- dot stuff content before sending
|
||||||
|
local email_content = tostring(task:get_content())
|
||||||
|
email_content = string.gsub(email_content, "\r\n%.", "\r\n..")
|
||||||
|
-- send mail
|
||||||
|
lua_smtp.sendmail({
|
||||||
|
task = task,
|
||||||
|
host = os.getenv("IPV4_NETWORK") .. '.253',
|
||||||
|
port = 591,
|
||||||
|
from = task:get_from(stp)[1].addr,
|
||||||
|
recipients = bcc_dest,
|
||||||
|
helo = 'bcc',
|
||||||
|
timeout = 20,
|
||||||
|
}, email_content, sendmail_cb)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- determine from
|
||||||
|
local from = task:get_from('smtp')
|
||||||
|
if from then
|
||||||
|
for _, a in ipairs(from) do
|
||||||
|
table.insert(from_table, a['addr']) -- add this rcpt to table
|
||||||
|
table.insert(from_table, '@' .. a['domain']) -- add this rcpts domain to table
|
||||||
|
end
|
||||||
|
else
|
||||||
|
return -- stop
|
||||||
|
end
|
||||||
|
|
||||||
|
-- determine rcpts
|
||||||
|
local rcpts = task:get_recipients('smtp')
|
||||||
|
if rcpts then
|
||||||
|
for _, a in ipairs(rcpts) do
|
||||||
|
table.insert(rcpt_table, a['addr']) -- add this rcpt to table
|
||||||
|
table.insert(rcpt_table, '@' .. a['domain']) -- add this rcpts domain to table
|
||||||
|
end
|
||||||
|
else
|
||||||
|
return -- stop
|
||||||
|
end
|
||||||
|
|
||||||
|
local action = task:get_metric_action('default')
|
||||||
|
rspamd_logger.infox("metric action now: %s", action)
|
||||||
|
|
||||||
|
local function rcpt_callback(err_message, code, body, headers)
|
||||||
|
if err_message == nil and code == 201 and body ~= nil then
|
||||||
|
if action == 'no action' or action == 'add header' or action == 'rewrite subject' then
|
||||||
|
send_mail(task, body)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function from_callback(err_message, code, body, headers)
|
||||||
|
if err_message == nil and code == 201 and body ~= nil then
|
||||||
|
if action == 'no action' or action == 'add header' or action == 'rewrite subject' then
|
||||||
|
send_mail(task, body)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if rcpt_table then
|
||||||
|
for _,e in ipairs(rcpt_table) do
|
||||||
|
rspamd_logger.infox(rspamd_config, "checking bcc for rcpt address %s", e)
|
||||||
|
rspamd_http.request({
|
||||||
|
task=task,
|
||||||
|
url='http://nginx:8081/bcc.php',
|
||||||
|
body='',
|
||||||
|
callback=rcpt_callback,
|
||||||
|
headers={Rcpt=e}
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if from_table then
|
||||||
|
for _,e in ipairs(from_table) do
|
||||||
|
rspamd_logger.infox(rspamd_config, "checking bcc for from address %s", e)
|
||||||
|
rspamd_http.request({
|
||||||
|
task=task,
|
||||||
|
url='http://nginx:8081/bcc.php',
|
||||||
|
body='',
|
||||||
|
callback=from_callback,
|
||||||
|
headers={From=e}
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
|
end,
|
||||||
|
priority = 20
|
||||||
|
})
|
||||||
|
|
||||||
|
rspamd_config:register_symbol({
|
||||||
|
name = 'DYN_RL_CHECK',
|
||||||
|
type = 'prefilter',
|
||||||
|
callback = function(task)
|
||||||
|
local util = require("rspamd_util")
|
||||||
|
local redis_params = rspamd_parse_redis_server('dyn_rl')
|
||||||
|
local rspamd_logger = require "rspamd_logger"
|
||||||
|
local envfrom = task:get_from(1)
|
||||||
|
local uname = task:get_user()
|
||||||
|
if not envfrom or not uname then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
local uname = uname:lower()
|
||||||
|
|
||||||
|
local env_from_domain = envfrom[1].domain:lower() -- get smtp from domain in lower case
|
||||||
|
|
||||||
|
local function redis_cb_user(err, data)
|
||||||
|
|
||||||
|
if err or type(data) ~= 'string' then
|
||||||
|
rspamd_logger.infox(rspamd_config, "dynamic ratelimit request for user %s returned invalid or empty data (\"%s\") or error (\"%s\") - trying dynamic ratelimit for domain...", uname, data, err)
|
||||||
|
|
||||||
|
local function redis_key_cb_domain(err, data)
|
||||||
|
if err or type(data) ~= 'string' then
|
||||||
|
rspamd_logger.infox(rspamd_config, "dynamic ratelimit request for domain %s returned invalid or empty data (\"%s\") or error (\"%s\")", env_from_domain, data, err)
|
||||||
|
else
|
||||||
|
rspamd_logger.infox(rspamd_config, "found dynamic ratelimit in redis for domain %s with value %s", env_from_domain, data)
|
||||||
|
task:insert_result('DYN_RL', 0.0, data, env_from_domain)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local redis_ret_domain = rspamd_redis_make_request(task,
|
||||||
|
redis_params, -- connect params
|
||||||
|
env_from_domain, -- hash key
|
||||||
|
false, -- is write
|
||||||
|
redis_key_cb_domain, --callback
|
||||||
|
'HGET', -- command
|
||||||
|
{'RL_VALUE', env_from_domain} -- arguments
|
||||||
|
)
|
||||||
|
if not redis_ret_domain then
|
||||||
|
rspamd_logger.infox(rspamd_config, "cannot make request to load ratelimit for domain")
|
||||||
|
end
|
||||||
|
else
|
||||||
|
rspamd_logger.infox(rspamd_config, "found dynamic ratelimit in redis for user %s with value %s", uname, data)
|
||||||
|
task:insert_result('DYN_RL', 0.0, data, uname)
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
local redis_ret_user = rspamd_redis_make_request(task,
|
||||||
|
redis_params, -- connect params
|
||||||
|
uname, -- hash key
|
||||||
|
false, -- is write
|
||||||
|
redis_cb_user, --callback
|
||||||
|
'HGET', -- command
|
||||||
|
{'RL_VALUE', uname} -- arguments
|
||||||
|
)
|
||||||
|
if not redis_ret_user then
|
||||||
|
rspamd_logger.infox(rspamd_config, "cannot make request to load ratelimit for user")
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end,
|
||||||
|
flags = 'empty',
|
||||||
|
priority = 20
|
||||||
|
})
|
||||||
|
|
||||||
|
rspamd_config:register_symbol({
|
||||||
|
name = 'NO_LOG_STAT',
|
||||||
|
type = 'postfilter',
|
||||||
|
callback = function(task)
|
||||||
|
local from = task:get_header('From')
|
||||||
|
if from and (monitoring_hosts:get_key(from) or from == "watchdog@localhost") then
|
||||||
|
task:set_flag('no_log')
|
||||||
|
task:set_flag('no_stat')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
})
|
||||||
|
|
||||||
|
rspamd_config:register_symbol({
|
||||||
|
name = 'MOO_FOOTER',
|
||||||
|
type = 'prefilter',
|
||||||
|
callback = function(task)
|
||||||
|
local cjson = require "cjson"
|
||||||
|
local lua_mime = require "lua_mime"
|
||||||
|
local lua_util = require "lua_util"
|
||||||
|
local rspamd_logger = require "rspamd_logger"
|
||||||
|
local rspamd_http = require "rspamd_http"
|
||||||
|
local envfrom = task:get_from(1)
|
||||||
|
local uname = task:get_user()
|
||||||
|
if not envfrom or not uname then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
local uname = uname:lower()
|
||||||
|
local env_from_domain = envfrom[1].domain:lower()
|
||||||
|
local env_from_addr = envfrom[1].addr:lower()
|
||||||
|
|
||||||
|
-- determine newline type
|
||||||
|
local function newline(task)
|
||||||
|
local t = task:get_newlines_type()
|
||||||
|
|
||||||
|
if t == 'cr' then
|
||||||
|
return '\r'
|
||||||
|
elseif t == 'lf' then
|
||||||
|
return '\n'
|
||||||
|
end
|
||||||
|
|
||||||
|
return '\r\n'
|
||||||
|
end
|
||||||
|
-- retrieve footer
|
||||||
|
local function footer_cb(err_message, code, data, headers)
|
||||||
|
if err or type(data) ~= 'string' then
|
||||||
|
rspamd_logger.infox(rspamd_config, "domain wide footer request for user %s returned invalid or empty data (\"%s\") or error (\"%s\")", uname, data, err)
|
||||||
|
else
|
||||||
|
|
||||||
|
-- parse json string
|
||||||
|
local footer = cjson.decode(data)
|
||||||
|
if not footer then
|
||||||
|
rspamd_logger.infox(rspamd_config, "parsing domain wide footer for user %s returned invalid or empty data (\"%s\") or error (\"%s\")", uname, data, err)
|
||||||
|
else
|
||||||
|
if footer and type(footer) == "table" and (footer.html and footer.html ~= "" or footer.plain and footer.plain ~= "") then
|
||||||
|
rspamd_logger.infox(rspamd_config, "found domain wide footer for user %s: html=%s, plain=%s, vars=%s", uname, footer.html, footer.plain, footer.vars)
|
||||||
|
|
||||||
|
if footer.skip_replies ~= 0 then
|
||||||
|
in_reply_to = task:get_header_raw('in-reply-to')
|
||||||
|
if in_reply_to then
|
||||||
|
rspamd_logger.infox(rspamd_config, "mail is a reply - skip footer")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local envfrom_mime = task:get_from(2)
|
||||||
|
local from_name = ""
|
||||||
|
if envfrom_mime and envfrom_mime[1].name then
|
||||||
|
from_name = envfrom_mime[1].name
|
||||||
|
elseif envfrom and envfrom[1].name then
|
||||||
|
from_name = envfrom[1].name
|
||||||
|
end
|
||||||
|
|
||||||
|
-- default replacements
|
||||||
|
local replacements = {
|
||||||
|
auth_user = uname,
|
||||||
|
from_user = envfrom[1].user,
|
||||||
|
from_name = from_name,
|
||||||
|
from_addr = envfrom[1].addr,
|
||||||
|
from_domain = envfrom[1].domain:lower()
|
||||||
|
}
|
||||||
|
-- add custom mailbox attributes
|
||||||
|
if footer.vars and type(footer.vars) == "string" then
|
||||||
|
local footer_vars = cjson.decode(footer.vars)
|
||||||
|
|
||||||
|
if type(footer_vars) == "table" then
|
||||||
|
for key, value in pairs(footer_vars) do
|
||||||
|
replacements[key] = value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if footer.html and footer.html ~= "" then
|
||||||
|
footer.html = lua_util.jinja_template(footer.html, replacements, true)
|
||||||
|
end
|
||||||
|
if footer.plain and footer.plain ~= "" then
|
||||||
|
footer.plain = lua_util.jinja_template(footer.plain, replacements, true)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- add footer
|
||||||
|
local out = {}
|
||||||
|
local rewrite = lua_mime.add_text_footer(task, footer.html, footer.plain) or {}
|
||||||
|
|
||||||
|
local seen_cte
|
||||||
|
local newline_s = newline(task)
|
||||||
|
|
||||||
|
local function rewrite_ct_cb(name, hdr)
|
||||||
|
if rewrite.need_rewrite_ct then
|
||||||
|
if name:lower() == 'content-type' then
|
||||||
|
local nct = string.format('%s: %s/%s; charset=utf-8',
|
||||||
|
'Content-Type', rewrite.new_ct.type, rewrite.new_ct.subtype)
|
||||||
|
out[#out + 1] = nct
|
||||||
|
-- update Content-Type header
|
||||||
|
task:set_milter_reply({
|
||||||
|
remove_headers = {['Content-Type'] = 0},
|
||||||
|
})
|
||||||
|
task:set_milter_reply({
|
||||||
|
add_headers = {['Content-Type'] = string.format('%s/%s; charset=utf-8', rewrite.new_ct.type, rewrite.new_ct.subtype)}
|
||||||
|
})
|
||||||
|
return
|
||||||
|
elseif name:lower() == 'content-transfer-encoding' then
|
||||||
|
out[#out + 1] = string.format('%s: %s',
|
||||||
|
'Content-Transfer-Encoding', 'quoted-printable')
|
||||||
|
-- update Content-Transfer-Encoding header
|
||||||
|
task:set_milter_reply({
|
||||||
|
remove_headers = {['Content-Transfer-Encoding'] = 0},
|
||||||
|
})
|
||||||
|
task:set_milter_reply({
|
||||||
|
add_headers = {['Content-Transfer-Encoding'] = 'quoted-printable'}
|
||||||
|
})
|
||||||
|
seen_cte = true
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
out[#out + 1] = hdr.raw:gsub('\r?\n?$', '')
|
||||||
|
end
|
||||||
|
|
||||||
|
task:headers_foreach(rewrite_ct_cb, {full = true})
|
||||||
|
|
||||||
|
if not seen_cte and rewrite.need_rewrite_ct then
|
||||||
|
out[#out + 1] = string.format('%s: %s', 'Content-Transfer-Encoding', 'quoted-printable')
|
||||||
|
end
|
||||||
|
|
||||||
|
-- End of headers
|
||||||
|
out[#out + 1] = newline_s
|
||||||
|
|
||||||
|
if rewrite.out then
|
||||||
|
for _,o in ipairs(rewrite.out) do
|
||||||
|
out[#out + 1] = o
|
||||||
|
end
|
||||||
|
else
|
||||||
|
out[#out + 1] = task:get_rawbody()
|
||||||
|
end
|
||||||
|
local out_parts = {}
|
||||||
|
for _,o in ipairs(out) do
|
||||||
|
if type(o) ~= 'table' then
|
||||||
|
out_parts[#out_parts + 1] = o
|
||||||
|
out_parts[#out_parts + 1] = newline_s
|
||||||
|
else
|
||||||
|
local removePrefix = "--\x0D\x0AContent-Type"
|
||||||
|
if string.lower(string.sub(tostring(o[1]), 1, string.len(removePrefix))) == string.lower(removePrefix) then
|
||||||
|
o[1] = string.sub(tostring(o[1]), string.len("--\x0D\x0A") + 1)
|
||||||
|
end
|
||||||
|
out_parts[#out_parts + 1] = o[1]
|
||||||
|
if o[2] then
|
||||||
|
out_parts[#out_parts + 1] = newline_s
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
task:set_message(out_parts)
|
||||||
|
else
|
||||||
|
rspamd_logger.infox(rspamd_config, "domain wide footer request for user %s returned invalid or empty data (\"%s\")", uname, data)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- fetch footer
|
||||||
|
rspamd_http.request({
|
||||||
|
task=task,
|
||||||
|
url='http://nginx:8081/footer.php',
|
||||||
|
body='',
|
||||||
|
callback=footer_cb,
|
||||||
|
headers={Domain=env_from_domain,Username=uname,From=env_from_addr},
|
||||||
|
})
|
||||||
|
|
||||||
|
return true
|
||||||
|
end,
|
||||||
|
priority = 1
|
||||||
|
})
|
260
Stable2.0/conf/meta_exporter/pipe.php
Normal file
260
Stable2.0/conf/meta_exporter/pipe.php
Normal file
@ -0,0 +1,260 @@
|
|||||||
|
<?php
|
||||||
|
// File size is limited by Nginx site to 10M
|
||||||
|
// To speed things up, we do not include prerequisites
|
||||||
|
header('Content-Type: text/plain');
|
||||||
|
require_once "vars.inc.php";
|
||||||
|
// Do not show errors, we log to using error_log
|
||||||
|
ini_set('error_reporting', 0);
|
||||||
|
// Init database
|
||||||
|
//$dsn = $database_type . ':host=' . $database_host . ';dbname=' . $database_name;
|
||||||
|
$dsn = $database_type . ":unix_socket=" . $database_sock . ";dbname=" . $database_name;
|
||||||
|
$opt = [
|
||||||
|
PDO::ATTR_ERRMODE => 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("QUARANTINE: " . $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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
48
Stable2.0/conf/meta_exporter/pipe_rl.php
Normal file
48
Stable2.0/conf/meta_exporter/pipe_rl.php
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
// File size is limited by Nginx site to 10M
|
||||||
|
// To speed things up, we do not include prerequisites
|
||||||
|
header('Content-Type: text/plain');
|
||||||
|
require_once "vars.inc.php";
|
||||||
|
// Do not show errors, we log to using error_log
|
||||||
|
ini_set('error_reporting', 0);
|
||||||
|
// Init Redis
|
||||||
|
$redis = new Redis();
|
||||||
|
try {
|
||||||
|
if (!empty(getenv('REDIS_SLAVEOF_IP'))) {
|
||||||
|
$redis->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;
|
||||||
|
|
275
Stable2.0/conf/meta_exporter/pushover.php
Normal file
275
Stable2.0/conf/meta_exporter/pushover.php
Normal file
@ -0,0 +1,275 @@
|
|||||||
|
<?php
|
||||||
|
// File size is limited by Nginx site to 10M
|
||||||
|
// To speed things up, we do not include prerequisites
|
||||||
|
header('Content-Type: text/plain');
|
||||||
|
require_once "vars.inc.php";
|
||||||
|
// Do not show errors, we log to using error_log
|
||||||
|
ini_set('error_reporting', 0);
|
||||||
|
// Init database
|
||||||
|
//$dsn = $database_type . ':host=' . $database_host . ';dbname=' . $database_name;
|
||||||
|
$dsn = $database_type . ":unix_socket=" . $database_sock . ";dbname=" . $database_name;
|
||||||
|
$opt = [
|
||||||
|
PDO::ATTR_ERRMODE => 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('/(?<name>.*?)<(?<address>.*?)>/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('/(?<name>.*?)<(?<address>.*?)>/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);
|
||||||
|
}
|
||||||
|
}
|
6
Stable2.0/conf/meta_exporter/vars.inc.php
Normal file
6
Stable2.0/conf/meta_exporter/vars.inc.php
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?php
|
||||||
|
require_once('../../../web/inc/vars.inc.php');
|
||||||
|
if (file_exists('../../../web/inc/vars.local.inc.php')) {
|
||||||
|
include_once('../../../web/inc/vars.local.inc.php');
|
||||||
|
}
|
||||||
|
?>
|
5
Stable2.0/conf/override.d/logging.inc
Normal file
5
Stable2.0/conf/override.d/logging.inc
Normal file
@ -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"
|
4
Stable2.0/conf/override.d/ratelimit.conf
Normal file
4
Stable2.0/conf/override.d/ratelimit.conf
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
whitelisted_rcpts = "postmaster,mailer-daemon";
|
||||||
|
max_rcpt = 25;
|
||||||
|
custom_keywords = "/etc/rspamd/lua/ratelimit.lua";
|
||||||
|
info_symbol = "RATELIMITED";
|
7
Stable2.0/conf/override.d/worker-controller.inc
Normal file
7
Stable2.0/conf/override.d/worker-controller.inc
Normal file
@ -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"
|
12
Stable2.0/conf/override.d/worker-fuzzy.inc
Normal file
12
Stable2.0/conf/override.d/worker-fuzzy.inc
Normal file
@ -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;
|
||||||
|
|
4
Stable2.0/conf/override.d/worker-normal.inc
Normal file
4
Stable2.0/conf/override.d/worker-normal.inc
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
bind_socket = "*:11333";
|
||||||
|
task_timeout = 25s;
|
||||||
|
count = 1;
|
||||||
|
.include(try=true; priority=30) "$CONFDIR/override.d/worker-normal.custom.inc"
|
9
Stable2.0/conf/override.d/worker-proxy.inc
Normal file
9
Stable2.0/conf/override.d/worker-proxy.inc
Normal file
@ -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"
|
1
Stable2.0/conf/plugins.d/README.md
Normal file
1
Stable2.0/conf/plugins.d/README.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
This is where you should copy any rspamd custom module
|
1
Stable2.0/conf/rspamd.conf.local
Normal file
1
Stable2.0/conf/rspamd.conf.local
Normal file
@ -0,0 +1 @@
|
|||||||
|
# rspamd.conf.local
|
2
Stable2.0/conf/rspamd.conf.override
Normal file
2
Stable2.0/conf/rspamd.conf.override
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# rspamd.conf.override
|
||||||
|
|
17
Stable2.0/hooks/build
Normal file
17
Stable2.0/hooks/build
Normal file
@ -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} .
|
9
Stable2.0/hooks/post_push
Normal file
9
Stable2.0/hooks/post_push
Normal file
@ -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}"
|
13
Stable2.0/rspamd.conf.local.override
Normal file
13
Stable2.0/rspamd.conf.local.override
Normal file
@ -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"
|
||||||
|
}
|
1
Stable2.0/worker-controller.inc
Normal file
1
Stable2.0/worker-controller.inc
Normal file
@ -0,0 +1 @@
|
|||||||
|
bind_socket = "*:11334";
|
1
Stable2.0/worker-proxy.inc
Normal file
1
Stable2.0/worker-proxy.inc
Normal file
@ -0,0 +1 @@
|
|||||||
|
bind_socket = *:11332;
|
Reference in New Issue
Block a user