Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
1130ec8e95
|
|||
|
5dd76e327c
|
|||
|
d5f030151f
|
|||
|
d7f8f40e03
|
|||
|
ed5575bd2d
|
|||
|
0f4da248e7
|
|||
|
9e7106ff0b
|
|||
|
91144643f3
|
|||
|
a4c2ec3952
|
|||
|
375728e452
|
|||
|
7c2bfda126
|
|||
|
0b6724e656
|
|||
|
3bedae77e1
|
|||
|
1e33e57cb3
|
|||
|
023a8412e8
|
|||
|
018e87f51f
|
|||
|
6146d377a1
|
|||
|
c4d4d2c5e7
|
@@ -120,7 +120,7 @@ Config options for conditions objects:
|
|||||||
* **headers** (optional)
|
* **headers** (optional)
|
||||||
Matches e-mails for which all regular expressions in the given list are matching at least one e-mail header.
|
Matches e-mails for which all regular expressions in the given list are matching at least one e-mail header.
|
||||||
* **whitelist** (optional)
|
* **whitelist** (optional)
|
||||||
Matches e-mails for which the whitelist has no entry for the envelope-from and envelope-to address combination, see section [Whitelist](#Whitelist).
|
Matches e-mails for which the whitelist has no entry for the envelope-from and envelope-to address combination, see section [Whitelist](#Whitelist) for details.
|
||||||
* **var** (optional)
|
* **var** (optional)
|
||||||
Matches e-mails for which a previous action or condition has set the given metavariable.
|
Matches e-mails for which a previous action or condition has set the given metavariable.
|
||||||
* **metavar** (optional)
|
* **metavar** (optional)
|
||||||
@@ -210,14 +210,14 @@ Options:
|
|||||||
Quarantine e-mail.
|
Quarantine e-mail.
|
||||||
Options:
|
Options:
|
||||||
* **store**
|
* **store**
|
||||||
Options for e-mail storage, see action **store** in section [Action types](#Action-types).
|
Options for e-mail storage, see action [store](#store).
|
||||||
If the option **metadata** is not specificall set for this storage, it will be set to true.
|
If the option **metadata** is not specificall set for this storage, it will be set to true.
|
||||||
* **smtp_host**
|
* **smtp_host**
|
||||||
SMTP host used to release e-mails from quarantine.
|
SMTP host used to release e-mails from quarantine.
|
||||||
* **smtp_port**
|
* **smtp_port**
|
||||||
SMTP port used to release e-mails from quarantine.
|
SMTP port used to release e-mails from quarantine.
|
||||||
* **notify** (optional)
|
* **notify** (optional)
|
||||||
Options for e-mail notifications, see action **notify** in section [Action types](#Action-types).
|
Options for e-mail notifications, see action [notify](#notify).
|
||||||
* **milter_action** (optional)
|
* **milter_action** (optional)
|
||||||
Milter action to perform. If set, no further rules or actions will be processed.
|
Milter action to perform. If set, no further rules or actions will be processed.
|
||||||
Please think carefully what you set here or your MTA may do something you do not want it to do.
|
Please think carefully what you set here or your MTA may do something you do not want it to do.
|
||||||
@@ -231,7 +231,7 @@ Options:
|
|||||||
* **reject_reason** (optional, default: "Message rejected")
|
* **reject_reason** (optional, default: "Message rejected")
|
||||||
Reject message sent to MTA if milter_action is set to reject.
|
Reject message sent to MTA if milter_action is set to reject.
|
||||||
* **whitelist** (optional)
|
* **whitelist** (optional)
|
||||||
Options for a whitelist, see **whitelist** in section [Conditions](#Conditions).
|
Options for a whitelist object, see section [Whitelist](#Whitelist).
|
||||||
|
|
||||||
### Storage types
|
### Storage types
|
||||||
Available storage types:
|
Available storage types:
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# Distributed under the terms of the GNU General Public License v2
|
# Distributed under the terms of the GNU General Public License v2
|
||||||
|
|
||||||
EAPI=7
|
EAPI=7
|
||||||
PYTHON_COMPAT=( python3_{9,10} )
|
PYTHON_COMPAT=( python3_{9..10} )
|
||||||
DISTUTILS_USE_SETUPTOOLS=rdepend
|
DISTUTILS_USE_SETUPTOOLS=rdepend
|
||||||
|
|
||||||
SCM=""
|
SCM=""
|
||||||
@@ -37,7 +37,7 @@ RDEPEND="
|
|||||||
lxml? ( dev-python/lxml[${PYTHON_USEDEP}] )
|
lxml? ( dev-python/lxml[${PYTHON_USEDEP}] )
|
||||||
dev-python/netaddr[${PYTHON_USEDEP}]
|
dev-python/netaddr[${PYTHON_USEDEP}]
|
||||||
dev-python/peewee[${PYTHON_USEDEP}]
|
dev-python/peewee[${PYTHON_USEDEP}]
|
||||||
dev-python/pymilter[${PYTHON_USEDEP}]"
|
>=dev-python/pymilter-1.5[${PYTHON_USEDEP}]"
|
||||||
|
|
||||||
python_install_all() {
|
python_install_all() {
|
||||||
distutils-r1_python_install_all
|
distutils-r1_python_install_all
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
|
set -x
|
||||||
PYTHON=$(which python)
|
PYTHON=$(which python)
|
||||||
|
|
||||||
script_dir=$(dirname "$(readlink -f -- "$BASH_SOURCE")")
|
script_dir=$(dirname "$(readlink -f -- "$BASH_SOURCE")")
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ __all__ = [
|
|||||||
"whitelist",
|
"whitelist",
|
||||||
"QuarantineMilter"]
|
"QuarantineMilter"]
|
||||||
|
|
||||||
__version__ = "2.0.0"
|
__version__ = "2.0.5"
|
||||||
|
|
||||||
from pyquarantine import _runtime_patches
|
from pyquarantine import _runtime_patches
|
||||||
|
|
||||||
@@ -100,7 +100,7 @@ class QuarantineMilter(Milter.Base):
|
|||||||
self.logger.warning(f"unable to serialize message as bytes: {e}")
|
self.logger.warning(f"unable to serialize message as bytes: {e}")
|
||||||
try:
|
try:
|
||||||
self.logger.warning("try to serialize as str and encode")
|
self.logger.warning("try to serialize as str and encode")
|
||||||
data = self.msg.as_string().encode("ascii", errors="replace")
|
data = self.msg.as_string().encode(errors="replace")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(
|
self.logger.error(
|
||||||
f"unable to serialize message, giving up: {e}")
|
f"unable to serialize message, giving up: {e}")
|
||||||
@@ -208,6 +208,7 @@ class QuarantineMilter(Milter.Base):
|
|||||||
|
|
||||||
return Milter.CONTINUE
|
return Milter.CONTINUE
|
||||||
|
|
||||||
|
@Milter.decode("replace")
|
||||||
def envfrom(self, mailfrom, *str):
|
def envfrom(self, mailfrom, *str):
|
||||||
try:
|
try:
|
||||||
self.mailfrom = "@".join(parse_addr(mailfrom)).lower()
|
self.mailfrom = "@".join(parse_addr(mailfrom)).lower()
|
||||||
@@ -219,6 +220,7 @@ class QuarantineMilter(Milter.Base):
|
|||||||
|
|
||||||
return Milter.CONTINUE
|
return Milter.CONTINUE
|
||||||
|
|
||||||
|
@Milter.decode("replace")
|
||||||
def envrcpt(self, to, *str):
|
def envrcpt(self, to, *str):
|
||||||
try:
|
try:
|
||||||
self.rcpts.add("@".join(parse_addr(to)).lower())
|
self.rcpts.add("@".join(parse_addr(to)).lower())
|
||||||
@@ -243,6 +245,7 @@ class QuarantineMilter(Milter.Base):
|
|||||||
|
|
||||||
return Milter.CONTINUE
|
return Milter.CONTINUE
|
||||||
|
|
||||||
|
@Milter.decode("replace")
|
||||||
def header(self, field, value):
|
def header(self, field, value):
|
||||||
try:
|
try:
|
||||||
# remove CR and LF from address fields, otherwise pythons
|
# remove CR and LF from address fields, otherwise pythons
|
||||||
@@ -258,11 +261,7 @@ class QuarantineMilter(Milter.Base):
|
|||||||
v = v.replace("\r", "").replace("\n", "")
|
v = v.replace("\r", "").replace("\n", "")
|
||||||
value = Header(s=v).encode()
|
value = Header(s=v).encode()
|
||||||
|
|
||||||
# remove surrogates
|
self.fp.write(field.encode() + b": " + value.encode() + b"\r\n")
|
||||||
field = field.encode("ascii", errors="replace")
|
|
||||||
value = value.encode("ascii", errors="replace")
|
|
||||||
|
|
||||||
self.fp.write(field + b": " + value + b"\r\n")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.exception(
|
self.logger.exception(
|
||||||
f"an exception occured in header method: {e}")
|
f"an exception occured in header method: {e}")
|
||||||
|
|||||||
@@ -21,14 +21,18 @@ import shutil
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
|
||||||
SYSTEMD_PATH = "/lib/systemd/system"
|
SYSTEMD_PATHS = ["/lib/systemd/system", "/usr/lib/systemd/system"]
|
||||||
OPENRC = "/sbin/openrc"
|
OPENRC = "/sbin/openrc"
|
||||||
|
|
||||||
|
|
||||||
def _systemd_files(pkg_dir, name):
|
def _systemd_files(pkg_dir, name):
|
||||||
|
for path in SYSTEMD_PATHS:
|
||||||
|
if os.path.isdir(path):
|
||||||
|
break
|
||||||
|
|
||||||
return [
|
return [
|
||||||
(f"{pkg_dir}/misc/systemd/{name}-milter.service",
|
(f"{pkg_dir}/misc/systemd/{name}-milter.service",
|
||||||
f"{SYSTEMD_PATH}/{name}-milter.service", True)]
|
f"{path}/{name}-milter.service", True)]
|
||||||
|
|
||||||
|
|
||||||
def _openrc_files(pkg_dir, name):
|
def _openrc_files(pkg_dir, name):
|
||||||
@@ -117,7 +121,11 @@ def _check_root():
|
|||||||
|
|
||||||
|
|
||||||
def _check_systemd():
|
def _check_systemd():
|
||||||
systemd = os.path.isdir(SYSTEMD_PATH)
|
for path in SYSTEMD_PATHS:
|
||||||
|
systemd = os.path.isdir(path)
|
||||||
|
if systemd:
|
||||||
|
break
|
||||||
|
|
||||||
if systemd:
|
if systemd:
|
||||||
logging.info("systemd detected")
|
logging.info("systemd detected")
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
# along with pyquarantine. If not, see <http://www.gnu.org/licenses/>.
|
# along with pyquarantine. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
|
||||||
|
from sys import version_info
|
||||||
import encodings
|
import encodings
|
||||||
|
|
||||||
|
|
||||||
@@ -150,68 +151,6 @@ def get_obs_local_part(value):
|
|||||||
setattr(email._header_value_parser, "get_obs_local_part", get_obs_local_part)
|
setattr(email._header_value_parser, "get_obs_local_part", get_obs_local_part)
|
||||||
|
|
||||||
|
|
||||||
# https://bugs.python.org/issue30681
|
|
||||||
#
|
|
||||||
# fix: https://github.com/python/cpython/pull/22090
|
|
||||||
|
|
||||||
import email.errors
|
|
||||||
from email.errors import HeaderDefect
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidDateDefect(HeaderDefect):
|
|
||||||
"""Header has unparseable or invalid date"""
|
|
||||||
|
|
||||||
|
|
||||||
setattr(email.errors, "InvalidDateDefect", InvalidDateDefect)
|
|
||||||
|
|
||||||
|
|
||||||
import email.utils
|
|
||||||
from email.utils import _parsedate_tz
|
|
||||||
import datetime
|
|
||||||
|
|
||||||
|
|
||||||
def parsedate_to_datetime(data):
|
|
||||||
parsed_date_tz = _parsedate_tz(data)
|
|
||||||
if parsed_date_tz is None:
|
|
||||||
raise ValueError('Invalid date value or format "%s"' % str(data))
|
|
||||||
*dtuple, tz = parsed_date_tz
|
|
||||||
if tz is None:
|
|
||||||
return datetime.datetime(*dtuple[:6])
|
|
||||||
return datetime.datetime(*dtuple[:6],
|
|
||||||
tzinfo=datetime.timezone(datetime.timedelta(seconds=tz)))
|
|
||||||
|
|
||||||
|
|
||||||
setattr(email.utils, "parsedate_to_datetime", parsedate_to_datetime)
|
|
||||||
|
|
||||||
|
|
||||||
import email.headerregistry
|
|
||||||
from email import utils, _header_value_parser as parser
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def parse(cls, value, kwds):
|
|
||||||
if not value:
|
|
||||||
kwds['defects'].append(errors.HeaderMissingRequiredValue())
|
|
||||||
kwds['datetime'] = None
|
|
||||||
kwds['decoded'] = ''
|
|
||||||
kwds['parse_tree'] = parser.TokenList()
|
|
||||||
return
|
|
||||||
if isinstance(value, str):
|
|
||||||
kwds['decoded'] = value
|
|
||||||
try:
|
|
||||||
value = utils.parsedate_to_datetime(value)
|
|
||||||
except ValueError:
|
|
||||||
kwds['defects'].append(errors.InvalidDateDefect('Invalid date value or format'))
|
|
||||||
kwds['datetime'] = None
|
|
||||||
kwds['parse_tree'] = parser.TokenList()
|
|
||||||
return
|
|
||||||
kwds['datetime'] = value
|
|
||||||
kwds['decoded'] = utils.format_datetime(kwds['datetime'])
|
|
||||||
kwds['parse_tree'] = cls.value_parser(kwds['decoded'])
|
|
||||||
|
|
||||||
|
|
||||||
setattr(email.headerregistry.DateHeader, "parse", parse)
|
|
||||||
|
|
||||||
|
|
||||||
#######################################
|
#######################################
|
||||||
# add charset alias for windows-874 #
|
# add charset alias for windows-874 #
|
||||||
#######################################
|
#######################################
|
||||||
@@ -227,3 +166,66 @@ for alias in ["windows-874", "windows_874"]:
|
|||||||
aliases[alias] = "cp874"
|
aliases[alias] = "cp874"
|
||||||
|
|
||||||
setattr(encodings.aliases, "aliases", aliases)
|
setattr(encodings.aliases, "aliases", aliases)
|
||||||
|
|
||||||
|
|
||||||
|
if version_info.major == 3 and version_info.minor < 10:
|
||||||
|
# https://bugs.python.org/issue30681
|
||||||
|
#
|
||||||
|
# fix: https://github.com/python/cpython/pull/22090
|
||||||
|
|
||||||
|
import email.errors
|
||||||
|
from email.errors import HeaderDefect
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidDateDefect(HeaderDefect):
|
||||||
|
"""Header has unparseable or invalid date"""
|
||||||
|
|
||||||
|
|
||||||
|
setattr(email.errors, "InvalidDateDefect", InvalidDateDefect)
|
||||||
|
|
||||||
|
|
||||||
|
import email.utils
|
||||||
|
from email.utils import _parsedate_tz
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
|
||||||
|
def parsedate_to_datetime(data):
|
||||||
|
parsed_date_tz = _parsedate_tz(data)
|
||||||
|
if parsed_date_tz is None:
|
||||||
|
raise ValueError('Invalid date value or format "%s"' % str(data))
|
||||||
|
*dtuple, tz = parsed_date_tz
|
||||||
|
if tz is None:
|
||||||
|
return datetime.datetime(*dtuple[:6])
|
||||||
|
return datetime.datetime(*dtuple[:6],
|
||||||
|
tzinfo=datetime.timezone(datetime.timedelta(seconds=tz)))
|
||||||
|
|
||||||
|
|
||||||
|
setattr(email.utils, "parsedate_to_datetime", parsedate_to_datetime)
|
||||||
|
|
||||||
|
|
||||||
|
import email.headerregistry
|
||||||
|
from email import utils, _header_value_parser as parser
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def parse(cls, value, kwds):
|
||||||
|
if not value:
|
||||||
|
kwds['defects'].append(errors.HeaderMissingRequiredValue())
|
||||||
|
kwds['datetime'] = None
|
||||||
|
kwds['decoded'] = ''
|
||||||
|
kwds['parse_tree'] = parser.TokenList()
|
||||||
|
return
|
||||||
|
if isinstance(value, str):
|
||||||
|
kwds['decoded'] = value
|
||||||
|
try:
|
||||||
|
value = utils.parsedate_to_datetime(value)
|
||||||
|
except ValueError:
|
||||||
|
kwds['defects'].append(errors.InvalidDateDefect('Invalid date value or format'))
|
||||||
|
kwds['datetime'] = None
|
||||||
|
kwds['parse_tree'] = parser.TokenList()
|
||||||
|
return
|
||||||
|
kwds['datetime'] = value
|
||||||
|
kwds['decoded'] = utils.format_datetime(kwds['datetime'])
|
||||||
|
kwds['parse_tree'] = cls.value_parser(kwds['decoded'])
|
||||||
|
|
||||||
|
|
||||||
|
setattr(email.headerregistry.DateHeader, "parse", parse)
|
||||||
|
|||||||
@@ -65,6 +65,134 @@ class MilterMessage(MIMEPart):
|
|||||||
|
|
||||||
self._headers = newheaders
|
self._headers = newheaders
|
||||||
|
|
||||||
|
def _find_body_parent(self, part, preferencelist, parent=None):
|
||||||
|
if part.is_attachment():
|
||||||
|
return
|
||||||
|
maintype, subtype = part.get_content_type().split("/")
|
||||||
|
if maintype == "text":
|
||||||
|
if subtype in preferencelist:
|
||||||
|
yield(preferencelist.index(subtype), parent)
|
||||||
|
return
|
||||||
|
if maintype != "multipart" or not self.is_multipart():
|
||||||
|
return
|
||||||
|
if subtype != "related":
|
||||||
|
for subpart in part.iter_parts():
|
||||||
|
yield from self._find_body_parent(
|
||||||
|
subpart, preferencelist, part)
|
||||||
|
return
|
||||||
|
if 'related' in preferencelist:
|
||||||
|
yield(preferencelist.index('related'), parent)
|
||||||
|
candidate = None
|
||||||
|
start = part.get_param('start')
|
||||||
|
if start:
|
||||||
|
for subpart in part.iter_parts():
|
||||||
|
if subpart['content-id'] == start:
|
||||||
|
candidate = subpart
|
||||||
|
break
|
||||||
|
if candidate is None:
|
||||||
|
subparts = part.get_payload()
|
||||||
|
candidate = subparts[0] if subparts else None
|
||||||
|
if candidate is not None:
|
||||||
|
yield from self._find_body_parent(candidate, preferencelist, part)
|
||||||
|
|
||||||
|
def get_body_parent(self, preferencelist=("related", "html", "plain")):
|
||||||
|
best_prio = len(preferencelist)
|
||||||
|
body_parent = None
|
||||||
|
for prio, parent in self._find_body_parent(self, preferencelist):
|
||||||
|
if prio < best_prio:
|
||||||
|
best_prio = prio
|
||||||
|
body_parent = parent
|
||||||
|
if prio == 0:
|
||||||
|
break
|
||||||
|
return body_parent
|
||||||
|
|
||||||
|
def get_body_content(self, pref):
|
||||||
|
part = None
|
||||||
|
content = None
|
||||||
|
if not self.is_multipart() and \
|
||||||
|
self.get_content_type() == f"text/{pref}":
|
||||||
|
part = self
|
||||||
|
else:
|
||||||
|
part = self.get_body(preferencelist=(pref))
|
||||||
|
|
||||||
|
if part is not None:
|
||||||
|
content = part.get_content()
|
||||||
|
|
||||||
|
return (part, content)
|
||||||
|
|
||||||
|
def set_body(self, text_content=None, html_content=None):
|
||||||
|
parent = self.get_body_parent() or self
|
||||||
|
if "Content-Type" not in parent:
|
||||||
|
# set Content-Type header if not present, otherwise
|
||||||
|
# make_alternative and make_mixed skip the payload
|
||||||
|
parent["Content-Type"] = parent.get_content_type()
|
||||||
|
|
||||||
|
maintype, subtype = parent.get_content_type().split("/")
|
||||||
|
if not parent.is_multipart() or maintype != "multipart":
|
||||||
|
if maintype == "text" and subtype in ("html", "plain"):
|
||||||
|
parent.make_alternative()
|
||||||
|
maintype, subtype = ("multipart", "alternative")
|
||||||
|
else:
|
||||||
|
parent.make_mixed()
|
||||||
|
maintype, subtype = ("multipart", "mixed")
|
||||||
|
|
||||||
|
text_body = parent.get_body(preferencelist=("plain"))
|
||||||
|
html_body = parent.get_body(preferencelist=("html"))
|
||||||
|
|
||||||
|
if text_content is not None:
|
||||||
|
if text_body:
|
||||||
|
text_body.set_content(text_content)
|
||||||
|
else:
|
||||||
|
if not html_body or subtype == "alternative":
|
||||||
|
inject_body_part(parent, text_content)
|
||||||
|
else:
|
||||||
|
html_body.add_alternative(text_content)
|
||||||
|
text_body = parent.get_body(preferencelist=("plain"))
|
||||||
|
|
||||||
|
if html_content is not None:
|
||||||
|
if html_body:
|
||||||
|
html_body.set_content(html_content, subtype="html")
|
||||||
|
else:
|
||||||
|
if not text_body or subtype == "alternative":
|
||||||
|
inject_body_part(parent, html_content, subtype="html")
|
||||||
|
else:
|
||||||
|
text_body.add_alternative(html_content, subtype="html")
|
||||||
|
|
||||||
|
|
||||||
|
def inject_body_part(part, content, subtype="plain"):
|
||||||
|
parts = []
|
||||||
|
text_body = None
|
||||||
|
text_content = None
|
||||||
|
if subtype == "html":
|
||||||
|
text_body, text_content = part.get_body_content("plain")
|
||||||
|
|
||||||
|
for p in part.iter_parts():
|
||||||
|
if text_body and p == text_body:
|
||||||
|
continue
|
||||||
|
parts.append(p)
|
||||||
|
|
||||||
|
boundary = part.get_boundary()
|
||||||
|
p_subtype = part.get_content_subtype()
|
||||||
|
part.clear_content()
|
||||||
|
if text_content != None:
|
||||||
|
part.set_content(text_content)
|
||||||
|
part.add_alternative(content, subtype=subtype)
|
||||||
|
else:
|
||||||
|
part.set_content(content, subtype=subtype)
|
||||||
|
|
||||||
|
if part.get_content_subtype() != p_subtype:
|
||||||
|
if p_subtype == "alternative":
|
||||||
|
part.make_alternative()
|
||||||
|
elif p_subtype == "related":
|
||||||
|
part.make_related()
|
||||||
|
else:
|
||||||
|
part.make_mixed()
|
||||||
|
|
||||||
|
if boundary:
|
||||||
|
part.set_boundary(boundary)
|
||||||
|
for p in parts:
|
||||||
|
part.attach(p)
|
||||||
|
|
||||||
|
|
||||||
def replace_illegal_chars(string):
|
def replace_illegal_chars(string):
|
||||||
"""Remove illegal characters from header values."""
|
"""Remove illegal characters from header values."""
|
||||||
|
|||||||
@@ -183,26 +183,6 @@ def _has_content_before_body_tag(soup):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _inject_body(milter, text_content="", html_content=None):
|
|
||||||
if not milter.msg.is_multipart():
|
|
||||||
milter.msg.make_mixed()
|
|
||||||
|
|
||||||
attachments = []
|
|
||||||
for attachment in milter.msg.iter_attachments():
|
|
||||||
if "content-disposition" not in attachment:
|
|
||||||
attachment["Content-Disposition"] = "attachment"
|
|
||||||
attachments.append(attachment)
|
|
||||||
|
|
||||||
milter.msg.clear_content()
|
|
||||||
milter.msg.set_content(text_content)
|
|
||||||
if html_content is not None:
|
|
||||||
milter.msg.add_alternative(html_content, subtype="html")
|
|
||||||
milter.msg.make_mixed()
|
|
||||||
|
|
||||||
for attachment in attachments:
|
|
||||||
milter.msg.attach(attachment)
|
|
||||||
|
|
||||||
|
|
||||||
def _wrap_message(milter):
|
def _wrap_message(milter):
|
||||||
attachment = MIMEPart(policy=SMTP)
|
attachment = MIMEPart(policy=SMTP)
|
||||||
attachment.set_content(milter.msg_as_bytes(),
|
attachment.set_content(milter.msg_as_bytes(),
|
||||||
@@ -248,29 +228,28 @@ class AddDisclaimer:
|
|||||||
self.pretend = pretend
|
self.pretend = pretend
|
||||||
|
|
||||||
def patch_message_body(self, milter, logger):
|
def patch_message_body(self, milter, logger):
|
||||||
text_body, text_content = _get_body_content(milter.msg, "plain")
|
text_body, text_content = milter.msg.get_body_content("plain")
|
||||||
html_body, html_content = _get_body_content(milter.msg, "html")
|
html_body, html_content = milter.msg.get_body_content("html")
|
||||||
|
|
||||||
if text_content is None and html_content is None:
|
if text_content is None and html_content is None:
|
||||||
logger.info("message contains only attachment(s), "
|
logger.info("message contains no body, inject it")
|
||||||
"inject empty body")
|
|
||||||
if self.add_html_body:
|
if self.add_html_body:
|
||||||
_inject_body(milter, "", "")
|
milter.msg.set_body("", "")
|
||||||
html_body, html_content = _get_body_content(milter.msg, "html")
|
html_body, html_content = milter.msg.get_body_content("html")
|
||||||
else:
|
else:
|
||||||
_inject_body(milter, "")
|
milter.msg.set_body("")
|
||||||
text_body, text_content = _get_body_content(milter.msg, "plain")
|
text_body, text_content = milter.msg.get_body_content("plain")
|
||||||
|
|
||||||
if html_content is None and self.add_html_body:
|
if html_content is None and self.add_html_body:
|
||||||
logger.info("inject html body part generated from plain body")
|
logger.info("inject html body based on plain body")
|
||||||
header = '<meta http-equiv="Content-Type" content="text/html; ' \
|
header = '<meta http-equiv="Content-Type" content="text/html; ' \
|
||||||
'charset=utf-8">'
|
'charset=utf-8">'
|
||||||
html_text = re.sub(r"^(.*)$", r"\1<br/>",
|
html_text = re.sub(r"^(.*)$", r"\1<br/>",
|
||||||
escape(text_content, quote=False),
|
escape(text_content, quote=False),
|
||||||
flags=re.MULTILINE)
|
flags=re.MULTILINE)
|
||||||
_inject_body(milter, text_content, f"{header}{html_text}")
|
milter.msg.set_body(None, f"{header}{html_text}")
|
||||||
text_body, text_content = _get_body_content(milter.msg, "plain")
|
text_body, text_content = milter.msg.get_body_content("plain")
|
||||||
html_body, html_content = _get_body_content(milter.msg, "html")
|
html_body, html_content = milter.msg.get_body_content("html")
|
||||||
|
|
||||||
variables = defaultdict(str, milter.msginfo["vars"])
|
variables = defaultdict(str, milter.msginfo["vars"])
|
||||||
variables["ENVELOPE_FROM"] = escape(
|
variables["ENVELOPE_FROM"] = escape(
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ def main():
|
|||||||
sysloghandler = logging.handlers.SysLogHandler(
|
sysloghandler = logging.handlers.SysLogHandler(
|
||||||
address="/dev/log", facility=logging.handlers.SysLogHandler.LOG_MAIL)
|
address="/dev/log", facility=logging.handlers.SysLogHandler.LOG_MAIL)
|
||||||
sysloghandler.setFormatter(
|
sysloghandler.setFormatter(
|
||||||
logging.Formatter("pyquarantine: %(message)s"))
|
logging.Formatter(f"{name}[%(process)d]: %(message)s"))
|
||||||
root_logger.addHandler(sysloghandler)
|
root_logger.addHandler(sysloghandler)
|
||||||
|
|
||||||
logger.info("milter starting")
|
logger.info("milter starting")
|
||||||
@@ -149,7 +149,7 @@ def main():
|
|||||||
|
|
||||||
# increase the recursion limit so that BeautifulSoup can
|
# increase the recursion limit so that BeautifulSoup can
|
||||||
# parse larger html content
|
# parse larger html content
|
||||||
sys.setrecursionlimit(2000)
|
sys.setrecursionlimit(4000)
|
||||||
|
|
||||||
rc = 0
|
rc = 0
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -549,7 +549,7 @@ class Quarantine:
|
|||||||
if not rcpts:
|
if not rcpts:
|
||||||
# all recipients whitelisted
|
# all recipients whitelisted
|
||||||
return
|
return
|
||||||
milter.msginfo["rcpts"] = rcpts
|
milter.msginfo["rcpts"] = rcpts.copy()
|
||||||
|
|
||||||
if self._milter_action in ["REJECT", "DISCARD"]:
|
if self._milter_action in ["REJECT", "DISCARD"]:
|
||||||
logger.info(f"quarantine message for {rcpts}")
|
logger.info(f"quarantine message for {rcpts}")
|
||||||
@@ -563,6 +563,5 @@ class Quarantine:
|
|||||||
|
|
||||||
if self._milter_action is not None:
|
if self._milter_action is not None:
|
||||||
milter.delrcpt(rcpts)
|
milter.delrcpt(rcpts)
|
||||||
if self._milter_action in ["ACCEPT", "REJECT"] and \
|
if not milter.msginfo["rcpts"]:
|
||||||
not milter.msginfo["rcpts"]:
|
|
||||||
return (self._milter_action, self._reason)
|
return (self._milter_action, self._reason)
|
||||||
|
|||||||
6
setup.py
6
setup.py
@@ -18,7 +18,7 @@ setup(name = "pyquarantine",
|
|||||||
# 3 - Alpha
|
# 3 - Alpha
|
||||||
# 4 - Beta
|
# 4 - Beta
|
||||||
# 5 - Production/Stable
|
# 5 - Production/Stable
|
||||||
"Development Status :: 4 - Beta",
|
"Development Status :: 5 - Production/Stable",
|
||||||
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
|
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
|
||||||
"Operating System :: OS Independent",
|
"Operating System :: OS Independent",
|
||||||
"Programming Language :: Python",
|
"Programming Language :: Python",
|
||||||
@@ -48,6 +48,6 @@ setup(name = "pyquarantine",
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
install_requires = ["pymilter", "jsonschema", "netaddr", "beautifulsoup4[lxml]", "peewee"],
|
install_requires = ["pymilter >= 1.5", "jsonschema", "netaddr", "beautifulsoup4[lxml]", "peewee"],
|
||||||
python_requires = ">=3.8"
|
python_requires = ">=3.9"
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user