restructure code and fixes

This commit is contained in:
2021-03-09 15:09:56 +01:00
parent b4986af1c2
commit d053851e73
9 changed files with 548 additions and 476 deletions

View File

@@ -14,10 +14,11 @@
__all__ = [ __all__ = [
"actions", "actions",
"base",
"conditions", "conditions",
"rules",
"run", "run",
"CustomLogger", "ModifyMilterConfig",
"Rule",
"ModifyMilter"] "ModifyMilter"]
__version__ = "1.1.4" __version__ = "1.1.4"
@@ -26,130 +27,93 @@ from pymodmilter import _runtime_patches
import Milter import Milter
import logging import logging
import re
import json
from Milter.utils import parse_addr from Milter.utils import parse_addr
from collections import defaultdict from collections import defaultdict
from email.header import Header from email.header import Header
from email.message import MIMEPart
from email.parser import BytesFeedParser from email.parser import BytesFeedParser
from email.policy import default as default_policy, SMTP from email.policy import default as default_policy, SMTP
from pymodmilter.actions import Action from pymodmilter.base import CustomLogger, BaseConfig, MilterMessage
from pymodmilter.conditions import Conditions from pymodmilter.base import replace_illegal_chars
from pymodmilter.rules import RuleConfig, Rule
class CustomLogger(logging.LoggerAdapter): class ModifyMilterConfig(BaseConfig):
def process(self, msg, kwargs): def __init__(self, cfgfile, debug=False):
if "name" in self.extra: try:
msg = "{}: {}".format(self.extra["name"], msg) with open(cfgfile, "r") as fh:
# remove lines with leading # (comments), they
# are not allowed in json
cfg = re.sub(r"(?m)^\s*#.*\n?", "", fh.read())
except IOError as e:
raise RuntimeError(f"unable to open/read config file: {e}")
if "qid" in self.extra: try:
msg = "{}: {}".format(self.extra["qid"], msg) cfg = json.loads(cfg)
except json.JSONDecodeError as e:
cfg_text = [f"{n+1}: {l}" for n, l in enumerate(cfg.splitlines())]
msg = "\n".join(cfg_text)
e.msg = f"{msg}\n{e.msg}"
raise e
if self.logger.getEffectiveLevel() != logging.DEBUG: if "global" in cfg:
msg = msg.replace("\n", "").replace("\r", "") assert isinstance(cfg["global"], dict), \
"global: invalid type, should be dict"
return msg, kwargs cfg["global"]["name"] = "global"
super().__init__(cfg["global"], debug)
self.logger.debug("initialize config")
class Rule: if "pretend" in cfg["global"]:
""" pretend = cfg["global"]["pretend"]
Rule to implement multiple actions on emails. assert isinstance(pretend, bool), \
""" "global: pretend: invalid value, should be bool"
self["pretend"] = pretend
def __init__(self, milter_cfg, cfg):
logger = logging.getLogger(cfg["name"])
self.logger = CustomLogger(logger, {"name": cfg["name"]})
self.logger.setLevel(cfg["loglevel"])
if cfg["conditions"] is None:
self.conditions = None
else: else:
self.conditions = Conditions(milter_cfg, cfg["conditions"]) self["pretend"] = False
self._need_body = False
self.actions = []
for action_cfg in cfg["actions"]:
action = Action(milter_cfg, action_cfg)
self.actions.append(action)
if action.need_body():
self._need_body = True
self.pretend = cfg["pretend"]
def need_body(self):
"""Return True if this rule needs the message body."""
return self._need_body
def ignores(self, host=None, envfrom=None, envto=None):
args = {}
if host is not None:
args["host"] = host
if envfrom is not None:
args["envfrom"] = envfrom
if envto is not None:
args["envto"] = envto
if self.conditions is None or self.conditions.match(args):
for action in self.actions:
if action.conditions is None or action.conditions.match(args):
return False
return True
def execute(self, milter, pretend=None):
"""Execute all actions of this rule."""
if pretend is None:
pretend = self.pretend
for action in self.actions:
milter_action = action.execute(milter, pretend=pretend)
if milter_action is not None:
return milter_action
class MilterMessage(MIMEPart):
def replace_header(self, _name, _value, idx=None):
_name = _name.lower()
counter = 0
for i, (k, v) in zip(range(len(self._headers)), self._headers):
if k.lower() == _name:
counter += 1
if not idx or counter == idx:
self._headers[i] = self.policy.header_store_parse(
k, _value)
break
if "socket" in cfg["global"]:
socket = cfg["global"]["socket"]
assert isinstance(socket, str), \
"global: socket: invalid value, should be string"
self["socket"] = socket
else: else:
raise KeyError(_name) self["socket"] = None
def remove_header(self, name, idx=None): if "local_addrs" in cfg["global"]:
name = name.lower() local_addrs = cfg["global"]["local_addrs"]
newheaders = [] assert isinstance(local_addrs, list) and all(
counter = 0 [isinstance(addr, str) for addr in local_addrs]), \
for k, v in self._headers: "global: local_addrs: invalid value, " \
if k.lower() == name: "should be list of strings"
counter += 1 self["local_addrs"] = local_addrs
if counter != idx:
newheaders.append((k, v))
else: else:
newheaders.append((k, v)) self["local_addrs"] = [
"::1/128",
"127.0.0.0/8",
"10.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16"]
self._headers = newheaders self.logger.debug(f"socket={self['socket']}, "
f"local_addrs={self['local_addrs']}, "
f"pretend={self['pretend']}, "
f"loglevel={self['loglevel']}")
assert "rules" in cfg, \
"mandatory parameter 'rules' not found"
assert isinstance(cfg["rules"], list), \
"rules: invalid value, should be list"
def replace_illegal_chars(string): self.logger.debug("initialize rules config")
"""Replace illegal characters in header values.""" self["rules"] = []
return string.replace( for idx, rule_cfg in enumerate(cfg["rules"]):
"\x00", "").replace( self["rules"].append(
"\r", "").replace( RuleConfig(idx, self, rule_cfg, debug))
"\n", "")
class ModifyMilter(Milter.Base): class ModifyMilter(Milter.Base):

View File

@@ -12,6 +12,16 @@
# along with PyMod-Milter. If not, see <http://www.gnu.org/licenses/>. # along with PyMod-Milter. If not, see <http://www.gnu.org/licenses/>.
# #
__all__ = [
"add_header",
"mod_header",
"del_header",
"add_disclaimer",
"rewrite_links",
"store",
"ActionConfig",
"Action"]
import logging import logging
import os import os
import re import re
@@ -23,7 +33,9 @@ from copy import copy
from datetime import datetime from datetime import datetime
from email.message import MIMEPart from email.message import MIMEPart
from pymodmilter import CustomLogger, Conditions, replace_illegal_chars from pymodmilter import CustomLogger, BaseConfig
from pymodmilter.conditions import ConditionsConfig, Conditions
from pymodmilter import replace_illegal_chars
def add_header(milter, field, value, pretend=False, def add_header(milter, field, value, pretend=False,
@@ -316,13 +328,145 @@ def store(milter, directory, pretend=False,
raise RuntimeError(f"unable to store message: {e}") raise RuntimeError(f"unable to store message: {e}")
class ActionConfig(BaseConfig):
def __init__(self, idx, rule_cfg, cfg, debug):
if "name" in cfg:
cfg["name"] = f"{rule_cfg['name']}: {cfg['name']}"
else:
cfg["name"] = f"{rule_cfg['name']}: Action #{idx}"
if "loglevel" not in cfg:
cfg["loglevel"] = rule_cfg["loglevel"]
super().__init__(cfg, debug)
self["pretend"] = rule_cfg["pretend"]
self["conditions"] = None
self["type"] = ""
if "pretend" in cfg:
pretend = cfg["pretend"]
assert isinstance(pretend, bool), \
f"{self['name']}: pretend: invalid value, should be bool"
self["pretend"] = pretend
assert "type" in cfg, \
f"{self['name']}: mandatory parameter 'type' not found"
assert isinstance(cfg["type"], str), \
f"{self['name']}: invalid value, should be string"
self["type"] = cfg["type"]
if self["type"] == "add_header":
self["func"] = add_header
self["need_body"] = False
self.add_string_arg(cfg, ("field", "value"))
elif self["type"] == "mod_header":
self["func"] = mod_header
self["need_body"] = False
args = ["field", "value"]
if "search" in cfg:
args.append("search")
for arg in args:
self.add_string_arg(cfg, arg)
if arg in ("field", "search"):
try:
self["args"][arg] = re.compile(
self["args"][arg],
re.MULTILINE + re.DOTALL + re.IGNORECASE)
except re.error as e:
raise ValueError(f"{self['name']}: {arg}: {e}")
elif self["type"] == "del_header":
self["func"] = del_header
self["need_body"] = False
args = ["field"]
if "value" in cfg:
args.append("value")
for arg in args:
self.add_string_arg(cfg, arg)
try:
self["args"][arg] = re.compile(
self["args"][arg],
re.MULTILINE + re.DOTALL + re.IGNORECASE)
except re.error as e:
raise ValueError(f"{self['name']}: {arg}: {e}")
elif self["type"] == "add_disclaimer":
self["func"] = add_disclaimer
self["need_body"] = True
if "error_policy" not in cfg:
cfg["error_policy"] = "wrap"
self.add_string_arg(
cfg, ("action", "html_template", "text_template",
"error_policy"))
assert self["args"]["action"] in ("append", "prepend"), \
f"{self['name']}: action: invalid value, " \
f"should be 'append' or 'prepend'"
assert self["args"]["error_policy"] in ("wrap",
"ignore",
"reject"), \
f"{self['name']}: error_policy: invalid value, " \
f"should be 'wrap', 'ignore' or 'reject'"
try:
with open(self["args"]["html_template"], "r") as f:
html = BeautifulSoup(f.read(), "html.parser")
body = html.find('body')
if body:
# just use content within the body tag if present
html = body
self["args"]["html_template"] = html
with open(self["args"]["text_template"], "r") as f:
self["args"]["text_template"] = f.read()
except IOError as e:
raise RuntimeError(
f"{self['name']}: unable to open/read template file: {e}")
elif self["type"] == "rewrite_links":
self["func"] = rewrite_links
self["need_body"] = True
self.add_string_arg(cfg, "repl")
elif self["type"] == "store":
self["func"] = store
self["need_body"] = True
self.add_string_arg(cfg, "storage_type")
assert self["args"]["storage_type"] in ("file"), \
f"{self['name']}: storage_type: invalid value, " \
f"should be 'file'"
if self["args"]["storage_type"] == "file":
self.add_string_arg(cfg, "directory")
else:
raise RuntimeError(f"{self['name']}: type: invalid action type")
if "conditions" in cfg:
conditions = cfg["conditions"]
assert isinstance(conditions, dict), \
f"{self['name']}: conditions: invalid value, should be dict"
self["conditions"] = ConditionsConfig(self, conditions, debug)
self.logger.debug(f"pretend={self['pretend']}, "
f"loglevel={self['loglevel']}, "
f"type={self['type']}, "
f"args={self['args']}")
class Action: class Action:
"""Action to implement a pre-configured action to perform on e-mails.""" """Action to implement a pre-configured action to perform on e-mails."""
def __init__(self, milter_cfg, cfg): def __init__(self, milter_cfg, cfg):
logger = logging.getLogger(cfg["name"]) self.logger = cfg.logger
self.logger = CustomLogger(logger, {"name": cfg["name"]}) #logger = logging.getLogger(cfg["name"])
self.logger.setLevel(cfg["loglevel"]) #self.logger = CustomLogger(logger, {"name": cfg["name"]})
#self.logger.setLevel(cfg["loglevel"])
if cfg["conditions"] is None: if cfg["conditions"] is None:
self.conditions = None self.conditions = None
@@ -330,6 +474,7 @@ class Action:
self.conditions = Conditions(milter_cfg, cfg["conditions"]) self.conditions = Conditions(milter_cfg, cfg["conditions"])
self.pretend = cfg["pretend"] self.pretend = cfg["pretend"]
self._func = cfg["func"]
self._args = cfg["args"] self._args = cfg["args"]
action_type = cfg["type"] action_type = cfg["type"]

143
pymodmilter/base.py Normal file
View File

@@ -0,0 +1,143 @@
# PyMod-Milter is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# PyMod-Milter is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with PyMod-Milter. If not, see <http://www.gnu.org/licenses/>.
#
__all__ = [
"CustomLogger",
"BaseConfig",
"MilterMessage",
"replace_illegal_chars"]
import logging
from email.message import MIMEPart
class CustomLogger(logging.LoggerAdapter):
def process(self, msg, kwargs):
if "name" in self.extra:
msg = "{}: {}".format(self.extra["name"], msg)
if "qid" in self.extra:
msg = "{}: {}".format(self.extra["qid"], msg)
if self.logger.getEffectiveLevel() != logging.DEBUG:
msg = msg.replace("\n", "").replace("\r", "")
return msg, kwargs
class BaseConfig:
def __init__(self, cfg={}, debug=False, logger=None):
self._cfg = {}
if "name" in cfg:
assert isinstance(cfg["name"], str), \
"rule: name: invalid value, should be string"
self["name"] = cfg["name"]
else:
self["name"] = __name__
if debug:
self["loglevel"] = logging.DEBUG
elif "loglevel" in cfg:
if isinstance(cfg["loglevel"], int):
self["loglevel"] = cfg["loglevel"]
else:
level = getattr(logging, cfg["loglevel"].upper(), None)
assert isinstance(level, int), \
f"{self['name']}: loglevel: invalid value"
self["loglevel"] = level
else:
self["loglevel"] = logging.INFO
if logger is None:
logger = logging.getLogger(self["name"])
logger.setLevel(self["loglevel"])
self.logger = CustomLogger(logger, {"name": self["name"]})
# the keys/values of args are used as parameters
# to functions
self["args"] = {}
def __setitem__(self, key, value):
self._cfg[key] = value
def __getitem__(self, key):
return self._cfg[key]
def __delitem__(self, key):
del self._cfg[key]
def __contains__(self, key):
return key in self._cfg
def add_string_arg(self, cfg, args):
if isinstance(args, str):
args = [args]
for arg in args:
assert arg in cfg, \
f"{self['name']}: mandatory parameter '{arg}' not found"
assert isinstance(cfg[arg], str), \
f"{self['name']}: {arg}: invalid value, should be string"
self["args"][arg] = cfg[arg]
def add_bool_arg(self, cfg, args):
if isinstance(args, str):
args = [args]
for arg in args:
assert arg in cfg, \
f"{self['name']}: mandatory parameter '{arg}' not found"
assert isinstance(cfg[arg], bool), \
f"{self['name']}: {arg}: invalid value, should be bool"
self["args"][arg] = cfg[arg]
class MilterMessage(MIMEPart):
def replace_header(self, _name, _value, idx=None):
_name = _name.lower()
counter = 0
for i, (k, v) in zip(range(len(self._headers)), self._headers):
if k.lower() == _name:
counter += 1
if not idx or counter == idx:
self._headers[i] = self.policy.header_store_parse(
k, _value)
break
else:
raise KeyError(_name)
def remove_header(self, name, idx=None):
name = name.lower()
newheaders = []
counter = 0
for k, v in self._headers:
if k.lower() == name:
counter += 1
if counter != idx:
newheaders.append((k, v))
else:
newheaders.append((k, v))
self._headers = newheaders
def replace_illegal_chars(string):
"""Replace illegal characters in header values."""
return string.replace(
"\x00", "").replace(
"\r", "").replace(
"\n", "")

View File

@@ -12,19 +12,66 @@
# along with PyMod-Milter. If not, see <http://www.gnu.org/licenses/>. # along with PyMod-Milter. If not, see <http://www.gnu.org/licenses/>.
# #
import logging __all__ = [
"ConditionsConfig",
"Conditions"]
from netaddr import IPAddress import logging
from pymodmilter import CustomLogger import re
from netaddr import IPAddress, IPNetwork, AddrFormatError
from pymodmilter import CustomLogger, BaseConfig
class ConditionsConfig(BaseConfig):
def __init__(self, parent_cfg, cfg, debug):
if "loglevel" not in cfg:
cfg["loglevel"] = parent_cfg["loglevel"]
cfg["name"] = f"{parent_cfg['name']}: condition"
super().__init__(cfg, debug)
if "local" in cfg:
self.add_bool_arg(cfg, "local")
if "hosts" in cfg:
hosts = cfg["hosts"]
assert isinstance(hosts, list) and all(
[isinstance(host, str) for host in hosts]), \
f"{self['name']}: hosts: invalid value, " \
f"should be list of strings"
self["args"]["hosts"] = []
try:
for host in cfg["hosts"]:
self["args"]["hosts"].append(IPNetwork(host))
except AddrFormatError as e:
raise ValueError(f"{self['name']}: hosts: {e}")
for arg in ("envfrom", "envto"):
if arg in cfg:
self.add_string_arg(cfg, arg)
try:
self["args"][arg] = re.compile(
self["args"][arg],
re.IGNORECASE)
except re.error as e:
raise ValueError(f"{self['name']}: {arg}: {e}")
self.logger.debug(f"{self['name']}: "
f"loglevel={self['loglevel']}, "
f"args={self['args']}")
class Conditions: class Conditions:
"""Conditions to implement conditions for rules and actions.""" """Conditions to implement conditions for rules and actions."""
def __init__(self, milter_cfg, cfg): def __init__(self, milter_cfg, cfg):
logger = logging.getLogger(cfg["name"]) self.logger = cfg.logger
self.logger = CustomLogger(logger, {"name": cfg["name"]}) #logger = logging.getLogger(cfg["name"])
self.logger.setLevel(cfg["loglevel"]) #self.logger = CustomLogger(logger, {"name": cfg["name"]})
#self.logger.setLevel(cfg["loglevel"])
self._local_addrs = milter_cfg["local_addrs"] self._local_addrs = milter_cfg["local_addrs"]
self._args = cfg["args"] self._args = cfg["args"]

View File

@@ -1,346 +0,0 @@
# PyMod-Milter is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# PyMod-Milter is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with PyMod-Milter. If not, see <http://www.gnu.org/licenses/>.
#
__all__ = [
"BaseConfig",
"ActionConfig",
"RuleConfig",
"ModifyMilterConfig"]
import json
import logging
import re
from bs4 import BeautifulSoup
from netaddr import IPNetwork, AddrFormatError
class BaseConfig:
def __init__(self, cfg={}, debug=False, logger=None):
if logger is None:
logger = logging.getLogger(__name__)
self.logger = logger
self._cfg = {}
if "name" in cfg:
assert isinstance(cfg["name"], str), \
"rule: name: invalid value, should be string"
self["name"] = cfg["name"]
else:
self["name"] = ""
if debug:
self["loglevel"] = logging.DEBUG
elif "loglevel" in cfg:
if isinstance(cfg["loglevel"], int):
self["loglevel"] = cfg["loglevel"]
else:
level = getattr(logging, cfg["loglevel"].upper(), None)
assert isinstance(level, int), \
f"{self['name']}: loglevel: invalid value"
self["loglevel"] = level
else:
self["loglevel"] = logging.INFO
# the keys/values of args are used as parameters
# to functions
self["args"] = {}
def __setitem__(self, key, value):
self._cfg[key] = value
def __getitem__(self, key):
return self._cfg[key]
def __delitem__(self, key):
del self._cfg[key]
def __contains__(self, key):
return key in self._cfg
def add_string_arg(self, cfg, args):
if isinstance(args, str):
args = [args]
for arg in args:
assert arg in cfg, \
f"{self['name']}: mandatory parameter '{arg}' not found"
assert isinstance(cfg[arg], str), \
f"{self['name']}: {arg}: invalid value, should be string"
self["args"][arg] = cfg[arg]
def add_bool_arg(self, cfg, args):
if isinstance(args, str):
args = [args]
for arg in args:
assert arg in cfg, \
f"{self['name']}: mandatory parameter '{arg}' not found"
assert isinstance(cfg[arg], bool), \
f"{self['name']}: {arg}: invalid value, should be bool"
self["args"][arg] = cfg[arg]
class ConditionsConfig(BaseConfig):
def __init__(self, parent_cfg, cfg, debug):
if "loglevel" not in cfg:
cfg["loglevel"] = parent_cfg["loglevel"]
cfg["name"] = f"{parent_cfg['name']}: condition"
super().__init__(cfg, debug)
if "local" in cfg:
self.add_bool_arg(cfg, "local")
if "hosts" in cfg:
hosts = cfg["hosts"]
assert isinstance(hosts, list) and all(
[isinstance(host, str) for host in hosts]), \
f"{self['name']}: hosts: invalid value, " \
f"should be list of strings"
self["args"]["hosts"] = []
try:
for host in cfg["hosts"]:
self["args"]["hosts"].append(IPNetwork(host))
except AddrFormatError as e:
raise ValueError(f"{self['name']}: hosts: {e}")
for arg in ("envfrom", "envto"):
if arg in cfg:
self.add_string_arg(cfg, arg)
try:
self["args"][arg] = re.compile(
self["args"][arg],
re.IGNORECASE)
except re.error as e:
raise ValueError(f"{self['name']}: {arg}: {e}")
class ActionConfig(BaseConfig):
def __init__(self, idx, rule_cfg, cfg, debug):
if "name" in cfg:
cfg["name"] = f"{rule_cfg['name']}: {cfg['name']}"
else:
cfg["name"] = f"{rule_cfg['name']}: Action #{idx}"
if "loglevel" not in cfg:
cfg["loglevel"] = rule_cfg["loglevel"]
super().__init__(cfg, debug)
self["pretend"] = rule_cfg["pretend"]
self["conditions"] = None
self["type"] = ""
if "pretend" in cfg:
pretend = cfg["pretend"]
assert isinstance(pretend, bool), \
f"{self['name']}: pretend: invalid value, should be bool"
self["pretend"] = pretend
assert "type" in cfg, \
f"{self['name']}: type: invalid value, should be string"
assert cfg["type"] in \
("add_header", "del_header", "mod_header", "add_disclaimer",
"rewrite_links", "store"), \
f"{self['name']}: type: invalid action type"
self["type"] = cfg["type"]
if self["type"] == "add_header":
self.add_string_arg(cfg, ("field", "value"))
elif self["type"] == "mod_header":
args = ["field", "value"]
if "search" in cfg:
args.append("search")
for arg in args:
self.add_string_arg(cfg, arg)
if arg in ("field", "search"):
try:
self["args"][arg] = re.compile(
self["args"][arg],
re.MULTILINE + re.DOTALL + re.IGNORECASE)
except re.error as e:
raise ValueError(f"{self['name']}: {arg}: {e}")
elif self["type"] == "del_header":
args = ["field"]
if "value" in cfg:
args.append("value")
for arg in args:
self.add_string_arg(cfg, arg)
try:
self["args"][arg] = re.compile(
self["args"][arg],
re.MULTILINE + re.DOTALL + re.IGNORECASE)
except re.error as e:
raise ValueError(f"{self['name']}: {arg}: {e}")
elif self["type"] == "add_disclaimer":
if "error_policy" not in cfg:
cfg["error_policy"] = "wrap"
self.add_string_arg(
cfg, ("action", "html_template", "text_template",
"error_policy"))
assert self["args"]["action"] in ("append", "prepend"), \
f"{self['name']}: action: invalid value, " \
f"should be 'append' or 'prepend'"
assert self["args"]["error_policy"] in ("wrap",
"ignore",
"reject"), \
f"{self['name']}: error_policy: invalid value, " \
f"should be 'wrap', 'ignore' or 'reject'"
try:
with open(self["args"]["html_template"], "r") as f:
html = BeautifulSoup(f.read(), "html.parser")
body = html.find('body')
if body:
# just use content within the body tag if present
html = body
self["args"]["html_template"] = html
with open(self["args"]["text_template"], "r") as f:
self["args"]["text_template"] = f.read()
except IOError as e:
raise RuntimeError(
f"{self['name']}: unable to open/read template file: {e}")
elif self["type"] == "rewrite_links":
self.add_string_arg(cfg, "repl")
elif self["type"] == "store":
self.add_string_arg(cfg, "storage_type")
assert self["args"]["storage_type"] in ("file"), \
f"{self['name']}: storage_type: invalid value, " \
f"should be 'file'"
if self["args"]["storage_type"] == "file":
self.add_string_arg(cfg, "directory")
if "conditions" in cfg:
conditions = cfg["conditions"]
assert isinstance(conditions, dict), \
f"{self['name']}: conditions: invalid value, should be dict"
self["conditions"] = ConditionsConfig(self, conditions, debug)
class RuleConfig(BaseConfig):
def __init__(self, idx, milter_cfg, cfg, debug=False):
if "name" not in cfg:
cfg["name"] = f"Rule #{idx}"
if "loglevel" not in cfg:
cfg["loglevel"] = milter_cfg["loglevel"]
super().__init__(cfg, debug)
self["pretend"] = milter_cfg["pretend"]
self["conditions"] = None
self["actions"] = []
if "pretend" in cfg:
pretend = cfg["pretend"]
assert isinstance(pretend, bool), \
f"{self['name']}: pretend: invalid value, should be bool"
self["pretend"] = pretend
assert "actions" in cfg, \
f"{self['name']}: mandatory parameter 'actions' not found"
actions = cfg["actions"]
assert isinstance(actions, list), \
f"{self['name']}: actions: invalid value, should be list"
for idx, action_cfg in enumerate(cfg["actions"]):
self["actions"].append(
ActionConfig(idx, self, action_cfg, debug))
if "conditions" in cfg:
conditions = cfg["conditions"]
assert isinstance(conditions, dict), \
f"{self['name']}: conditions: invalid value, should be dict"
self["conditions"] = ConditionsConfig(self, conditions, debug)
class ModifyMilterConfig(BaseConfig):
def __init__(self, cfgfile, debug=False):
try:
with open(cfgfile, "r") as fh:
# remove lines with leading # (comments), they
# are not allowed in json
cfg = re.sub(r"(?m)^\s*#.*\n?", "", fh.read())
except IOError as e:
raise RuntimeError(f"unable to open/read config file: {e}")
try:
cfg = json.loads(cfg)
except json.JSONDecodeError as e:
cfg_text = [f"{n+1}: {l}" for n, l in enumerate(cfg.splitlines())]
msg = "\n".join(cfg_text)
e.msg = f"{msg}\n{e.msg}"
raise e
if "global" in cfg:
assert isinstance(cfg["global"], dict), \
"global: invalid type, should be dict"
super().__init__(cfg["global"], debug)
if "pretend" in cfg["global"]:
pretend = cfg["global"]["pretend"]
assert isinstance(pretend, bool), \
"global: pretend: invalid value, should be bool"
self["pretend"] = pretend
else:
self["pretend"] = False
if "socket" in cfg["global"]:
socket = cfg["global"]["socket"]
assert isinstance(socket, str), \
"global: socket: invalid value, should be string"
self["socket"] = socket
else:
self["socket"] = None
if "local_addrs" in cfg["global"]:
local_addrs = cfg["global"]["local_addrs"]
assert isinstance(local_addrs, list) and all(
[isinstance(addr, str) for addr in local_addrs]), \
"global: local_addrs: invalid value, " \
"should be list of strings"
self["local_addrs"] = local_addrs
else:
self["local_addrs"] = [
"::1/128",
"127.0.0.0/8",
"10.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16"]
assert "rules" in cfg, \
"mandatory parameter 'rules' not found"
assert isinstance(cfg["rules"], list), \
"rules: invalid value, should be list"
self["rules"] = []
for idx, rule_cfg in enumerate(cfg["rules"]):
self["rules"].append(
RuleConfig(idx, self, rule_cfg, debug))

124
pymodmilter/rules.py Normal file
View File

@@ -0,0 +1,124 @@
# PyMod-Milter is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# PyMod-Milter is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with PyMod-Milter. If not, see <http://www.gnu.org/licenses/>.
#
__all__ = [
"RuleConfig",
"Rule"]
import logging
from pymodmilter import CustomLogger, BaseConfig
from pymodmilter.actions import ActionConfig, Action
from pymodmilter.conditions import ConditionsConfig, Conditions
class RuleConfig(BaseConfig):
def __init__(self, idx, milter_cfg, cfg, debug=False):
if "name" not in cfg:
cfg["name"] = f"Rule #{idx}"
if "loglevel" not in cfg:
cfg["loglevel"] = milter_cfg["loglevel"]
super().__init__(cfg, debug)
self["pretend"] = milter_cfg["pretend"]
self["conditions"] = None
self["actions"] = []
if "pretend" in cfg:
pretend = cfg["pretend"]
assert isinstance(pretend, bool), \
f"{self['name']}: pretend: invalid value, should be bool"
self["pretend"] = pretend
assert "actions" in cfg, \
f"{self['name']}: mandatory parameter 'actions' not found"
actions = cfg["actions"]
assert isinstance(actions, list), \
f"{self['name']}: actions: invalid value, should be list"
self.logger.debug(f"pretend={self['pretend']}, "
f"loglevel={self['loglevel']}")
if "conditions" in cfg:
conditions = cfg["conditions"]
assert isinstance(conditions, dict), \
f"{self['name']}: conditions: invalid value, should be dict"
self["conditions"] = ConditionsConfig(self, conditions, debug)
for idx, action_cfg in enumerate(cfg["actions"]):
self["actions"].append(
ActionConfig(idx, self, action_cfg, debug))
class Rule:
"""
Rule to implement multiple actions on emails.
"""
def __init__(self, milter_cfg, cfg):
self.logger = cfg.logger
#logger = logging.getLogger(cfg["name"])
#self.logger = CustomLogger(logger, {"name": cfg["name"]})
#self.logger.setLevel(cfg["loglevel"])
if cfg["conditions"] is None:
self.conditions = None
else:
self.conditions = Conditions(milter_cfg, cfg["conditions"])
self._need_body = False
self.actions = []
for action_cfg in cfg["actions"]:
action = Action(milter_cfg, action_cfg)
self.actions.append(action)
if action.need_body():
self._need_body = True
self.pretend = cfg["pretend"]
def need_body(self):
"""Return True if this rule needs the message body."""
return self._need_body
def ignores(self, host=None, envfrom=None, envto=None):
args = {}
if host is not None:
args["host"] = host
if envfrom is not None:
args["envfrom"] = envfrom
if envto is not None:
args["envto"] = envto
if self.conditions is None or self.conditions.match(args):
for action in self.actions:
if action.conditions is None or action.conditions.match(args):
return False
return True
def execute(self, milter, pretend=None):
"""Execute all actions of this rule."""
if pretend is None:
pretend = self.pretend
for action in self.actions:
milter_action = action.execute(milter, pretend=pretend)
if milter_action is not None:
return milter_action

View File

@@ -12,6 +12,8 @@
# along with PyMod-Milter. If not, see <http://www.gnu.org/licenses/>. # along with PyMod-Milter. If not, see <http://www.gnu.org/licenses/>.
# #
__all__ = ["main"]
import Milter import Milter
import argparse import argparse
import logging import logging
@@ -20,7 +22,7 @@ import sys
from pymodmilter import ModifyMilter from pymodmilter import ModifyMilter
from pymodmilter import __version__ as version from pymodmilter import __version__ as version
from pymodmilter.config import ModifyMilterConfig from pymodmilter import ModifyMilterConfig
def main(): def main():
@@ -83,7 +85,7 @@ def main():
try: try:
logger.debug("prepar milter configuration") logger.debug("prepar milter configuration")
cfg = ModifyMilterConfig(args.cfgfile, args.debug) cfg = ModifyMilterConfig(args.config, args.debug)
if not args.debug: if not args.debug:
logger.setLevel(cfg["loglevel"]) logger.setLevel(cfg["loglevel"])
@@ -100,8 +102,6 @@ def main():
if not cfg["rules"]: if not cfg["rules"]:
raise RuntimeError("no rules configured") raise RuntimeError("no rules configured")
logger.debug("initializing rules ...")
for rule_cfg in cfg["rules"]: for rule_cfg in cfg["rules"]:
if not rule_cfg["actions"]: if not rule_cfg["actions"]:
raise RuntimeError( raise RuntimeError(
@@ -112,7 +112,7 @@ def main():
sys.exit(255) sys.exit(255)
if args.test: if args.test:
print("Configuration ok") print("Configuration OK")
sys.exit(0) sys.exit(0)
# setup console log for runtime # setup console log for runtime

View File

@@ -1,5 +0,0 @@
#!/usr/bin/env python3
import config
a = config.ModifyMilterConfig("test.conf", debug=True)

View File

@@ -176,14 +176,14 @@
# Notes: Path to a file which contains the html representation of the disclaimer. # Notes: Path to a file which contains the html representation of the disclaimer.
# Value: [ FILE_PATH ] # Value: [ FILE_PATH ]
# #
"html_template": "docs/templates/disclaimer_html.template", "html_template": "pymodmilter/docs/templates/disclaimer_html.template",
# Option: text_template # Option: text_template
# Type: String # Type: String
# Notes: Path to a file which contains the text representation of the disclaimer. # Notes: Path to a file which contains the text representation of the disclaimer.
# Value: [ FILE_PATH ] # Value: [ FILE_PATH ]
# #
"text_template": "docs/templates/disclaimer_text.template", "text_template": "pymodmilter/docs/templates/disclaimer_text.template",
# Option: error_policy # Option: error_policy
# Type: String # Type: String