CK_beats
I have an update to the script I did, I was capturing all the email content including the attachments in the a mixed, multipart. text printable MIME mess. Some times attachments get though other times they don’t on Outlook and Mailbird, but thunderbird is forgiving and sends the correct structure, as with bluemail and emClient. So the mess was hidden from me.
What the code now does is when an attachment is added it silently stops the domain wide footer and cleanly sends the attachments pdf where not going as base64 so become corrupt along the way now they do. When no attachments are in the email the domain wide footer is displayed once again.
Here is my commit https://github.com/shaneodoo/mailcow-dockerized/blob/master/data/conf/rspamd/lua/rspamd.local.lua
https://github.com/shaneodoo/mailcow-dockerized/commit/ff454876c9916531916b032c0b69223aed1b3faa
Replace everything from line 562 to the end.
Or just before –fetch footer this should get rid of the unknown attachment issue you have as MIME was formatting the file incorrectly and RSPAMD knew there was an attachment but didn’t know how to process it, its now untouched and cleanly moving off as it should be…
-- retrieve footer
local function footer_cb(err_message, code, data, headers)
-- CHANGE 1: Fixed error variable name from 'err' to 'err_message' and added early return
if err_message 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_message)
return
end
-- parse json string
local footer = cjson.decode(data)
-- CHANGE 2: Restructured validation with early returns instead of nested if/else
if not footer then
rspamd_logger.infox(rspamd_config, "parsing domain wide footer for user %s returned invalid or empty data (\"%s\")", uname, data)
return
end
if not (footer and type(footer) == "table" and (footer.html and footer.html ~= "" or footer.plain and footer.plain ~= "")) then
rspamd_logger.infox(rspamd_config, "domain wide footer request for user %s returned invalid or empty data (\"%s\")", uname, data)
return
end
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)
-- Check if mail is a reply and skip if needed
if footer.skip_replies ~= 0 then
local 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
-- CHANGE 3: NEW SAFETY CHECK - Skip multipart/mixed messages (likely have attachments)
local ct = task:get_header('Content-Type')
if ct and ct:lower():match('multipart/mixed') then
rspamd_logger.infox(rspamd_config, "skipping footer for multipart/mixed message (has attachments)")
return
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
-- include boundary if present
local boundary_part = rewrite.new_ct.boundary and
string.format('; boundary="%s"', rewrite.new_ct.boundary) or ''
local nct = string.format('%s: %s/%s; charset=utf-8%s',
'Content-Type', rewrite.new_ct.type, rewrite.new_ct.subtype, boundary_part)
out[#out + 1] = nct
-- update Content-Type header (include boundary if present)
task:set_milter_reply({
remove_headers = {['Content-Type'] = 0},
})
task:set_milter_reply({
add_headers = {['Content-Type'] = string.format('%s/%s; charset=utf-8%s',
rewrite.new_ct.type, rewrite.new_ct.subtype, boundary_part)}
})
return
elseif name:lower() == 'content-transfer-encoding' then
-- CHANGE 4: Only change encoding for text parts, preserve base64 for attachments
if rewrite.new_ct and rewrite.new_ct.type == 'text' then
out[#out + 1] = string.format('%s: %s',
'Content-Transfer-Encoding', 'quoted-printable')
task:set_milter_reply({
remove_headers = {['Content-Transfer-Encoding'] = 0},
})
task:set_milter_reply({
add_headers = {['Content-Transfer-Encoding'] = 'quoted-printable'}
})
seen_cte = true
else
-- Preserve original encoding for non-text parts
out[#out + 1] = hdr.raw:gsub('\r?\n?$', '')
end
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
-- CHANGE 5: REMOVED boundary stripping code that was destroying MIME structure
-- Just add the part as-is without modification
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)
end
-- fetch footer