git subrepo commit (merge) mailcow/src/mailcow-dockerized

subrepo: subdir:   "mailcow/src/mailcow-dockerized"
  merged:   "c7b1dc37"
upstream: origin:   "https://github.com/mailcow/mailcow-dockerized.git"
  branch:   "master"
  commit:   "a366494c"
git-subrepo: version:  "0.4.6"
  origin:   "???"
  commit:   "???"
Change-Id: Id574ecd4e02e3c4fbf8a1efd49be11c0b6d19a3f
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/custom/bad_asn.map b/mailcow/src/mailcow-dockerized/data/conf/rspamd/custom/bad_asn.map
index 1858c55..a8d49cf 100644
--- a/mailcow/src/mailcow-dockerized/data/conf/rspamd/custom/bad_asn.map
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/custom/bad_asn.map
@@ -27,4 +27,5 @@
 #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
\ No newline at end of file
+#201942 5 #Soltia Consulting SL, Spain
+#213373 4 #IP Connect Inc
\ No newline at end of file
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/composites.conf b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/composites.conf
index 337a2eb..e6fa24c 100644
--- a/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/composites.conf
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/composites.conf
@@ -8,7 +8,7 @@
 }
 # Bad policy from free mail providers
 FREEMAIL_POLICY_FAILURE {
-  expression = "-g+:policies & !DMARC_POLICY_ALLOW & !MAILLIST & ( FREEMAIL_ENVFROM | FREEMAIL_FROM ) & !WHITELISTED_FWD_HOST";
+  expression = "FREEMAIL_FROM & !DMARC_POLICY_ALLOW & !MAILLIST& !WHITELISTED_FWD_HOST & -g+:policies";
   score = 16.0;
 }
 # Applies to freemail with undisclosed recipients
@@ -68,3 +68,53 @@
 ENCRYPTED_CHAT {
   expression = "CHAT_VERSION_HEADER & ENCRYPTED_PGP";
 }
+# 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;
+}
\ No newline at end of file
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/multimap.conf b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/multimap.conf
index 17ada99..888bf36 100644
--- a/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/multimap.conf
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/multimap.conf
@@ -159,8 +159,8 @@
 }
 
 URLHAUS_ABUSE_CH {
-  type = "url";
-  filter = "full";
+  type = "selector";
+  selector = "urls";
   map = "https://urlhaus.abuse.ch/downloads/text_online/";
   score = 10.0;
 }
@@ -175,7 +175,7 @@
   type = "header";
   header = "subject";
   regexp = true;
-  map = "http://nullnull.org/bad-subject-regex.txt";
+  map = "http://fuzzy.mailcow.email/bad-subject-regex.txt";
   score = 6.0;
   symbols_set = ["BAD_SUBJECT_00"];
 }
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/lua/rspamd.local.lua b/mailcow/src/mailcow-dockerized/data/conf/rspamd/lua/rspamd.local.lua
index 6318bd2..acc4055 100644
--- a/mailcow/src/mailcow-dockerized/data/conf/rspamd/lua/rspamd.local.lua
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/lua/rspamd.local.lua
@@ -221,6 +221,16 @@
     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)
@@ -229,6 +239,7 @@
 
       if action ~= 'no action' and action ~= 'greylist' then
         rspamd_logger.infox("skipping tag handler for action: %s", action)
+        remove_moo_tag()
         return true
       end
 
@@ -243,6 +254,7 @@
               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({
@@ -261,6 +273,7 @@
               )
               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
@@ -268,7 +281,10 @@
               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},
+                remove_headers = {
+                  ['Subject'] = 1,
+                  ['X-Moo-Tag'] = 0
+                },
                 add_headers = {['Subject'] = new_sbj}
               })
             end
@@ -284,6 +300,7 @@
           )
           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
@@ -295,6 +312,7 @@
           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,
@@ -307,7 +325,8 @@
           end
         end
       end
-
+    else
+      remove_moo_tag()
     end
   end,
   priority = 19
@@ -340,6 +359,10 @@
       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',
@@ -347,8 +370,8 @@
         from = task:get_from(stp)[1].addr,
         recipients = bcc_dest,
         helo = 'bcc',
-        timeout = 10,
-      }, task:get_content(), sendmail_cb)
+        timeout = 20,
+      }, email_content, sendmail_cb)
     end
 
     -- determine from
@@ -499,3 +522,146 @@
     end
   end
 })
+
+rspamd_config:register_symbol({
+  name = 'MOO_FOOTER',
+  type = 'prefilter',
+  callback = function(task)
+    local lua_mime = require "lua_mime"
+    local lua_util = require "lua_util"
+    local rspamd_logger = require "rspamd_logger"
+    local rspamd_redis = require "rspamd_redis"
+    local ucl = require "ucl"
+    local redis_params = rspamd_parse_redis_server('footer')
+    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 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
+    local function redis_cb_footer(err, data)
+      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 parser = ucl.parser()
+        local res,err = parser:parse_string(data)
+        if not res 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
+          local footer = parser:get_object()
+
+          if footer and type(footer) == "table" and (footer.html or footer.plain) then
+            rspamd_logger.infox(rspamd_config, "found domain wide footer for user %s: html=%s, plain=%s", uname, footer.html, footer.plain)
+
+            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
+
+            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()
+            }
+            if footer.html then
+              footer.html = lua_util.jinja_template(footer.html, replacements, true)
+            end
+            if 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
+                  return
+                elseif name:lower() == 'content-transfer-encoding' then
+                  out[#out + 1] = string.format('%s: %s',
+                      '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
+                 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
+
+    local redis_ret_footer = rspamd_redis_make_request(task,
+      redis_params, -- connect params
+      env_from_domain, -- hash key
+      false, -- is write
+      redis_cb_footer, --callback
+      'HGET', -- command
+      {"DOMAIN_WIDE_FOOTER", env_from_domain} -- arguments
+    )
+    if not redis_ret_footer then
+      rspamd_logger.infox(rspamd_config, "cannot make request to load footer for domain")
+    end
+
+    return true
+  end,
+  priority = 1
+})