diff --git a/pymodmilter/__init__.py b/pymodmilter/__init__.py
index 03e7e7e..255b889 100644
--- a/pymodmilter/__init__.py
+++ b/pymodmilter/__init__.py
@@ -14,10 +14,11 @@
__all__ = [
"actions",
+ "base",
"conditions",
+ "rules",
"run",
- "CustomLogger",
- "Rule",
+ "ModifyMilterConfig",
"ModifyMilter"]
__version__ = "1.1.4"
@@ -26,130 +27,93 @@ from pymodmilter import _runtime_patches
import Milter
import logging
+import re
+import json
from Milter.utils import parse_addr
-
from collections import defaultdict
-
from email.header import Header
-from email.message import MIMEPart
from email.parser import BytesFeedParser
from email.policy import default as default_policy, SMTP
-from pymodmilter.actions import Action
-from pymodmilter.conditions import Conditions
+from pymodmilter.base import CustomLogger, BaseConfig, MilterMessage
+from pymodmilter.base import replace_illegal_chars
+from pymodmilter.rules import RuleConfig, Rule
-class CustomLogger(logging.LoggerAdapter):
- def process(self, msg, kwargs):
- if "name" in self.extra:
- msg = "{}: {}".format(self.extra["name"], msg)
+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}")
- if "qid" in self.extra:
- msg = "{}: {}".format(self.extra["qid"], msg)
+ 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 self.logger.getEffectiveLevel() != logging.DEBUG:
- msg = msg.replace("\n", "").replace("\r", "")
+ if "global" in cfg:
+ 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:
- """
- Rule to implement multiple actions on emails.
- """
-
- 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:
- 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
-
-
-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))
+ if "pretend" in cfg["global"]:
+ pretend = cfg["global"]["pretend"]
+ assert isinstance(pretend, bool), \
+ "global: pretend: invalid value, should be bool"
+ self["pretend"] = pretend
else:
- newheaders.append((k, v))
+ self["pretend"] = False
- self._headers = newheaders
+ 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"]
-def replace_illegal_chars(string):
- """Replace illegal characters in header values."""
- return string.replace(
- "\x00", "").replace(
- "\r", "").replace(
- "\n", "")
+ 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"
+
+ self.logger.debug("initialize rules config")
+ self["rules"] = []
+ for idx, rule_cfg in enumerate(cfg["rules"]):
+ self["rules"].append(
+ RuleConfig(idx, self, rule_cfg, debug))
class ModifyMilter(Milter.Base):
diff --git a/pymodmilter/actions.py b/pymodmilter/actions.py
index 1aa26a5..1ad4e6f 100644
--- a/pymodmilter/actions.py
+++ b/pymodmilter/actions.py
@@ -12,6 +12,16 @@
# along with PyMod-Milter. If not, see .
#
+__all__ = [
+ "add_header",
+ "mod_header",
+ "del_header",
+ "add_disclaimer",
+ "rewrite_links",
+ "store",
+ "ActionConfig",
+ "Action"]
+
import logging
import os
import re
@@ -23,7 +33,9 @@ from copy import copy
from datetime import datetime
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,
@@ -316,13 +328,145 @@ def store(milter, directory, pretend=False,
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:
"""Action to implement a pre-configured action to perform on e-mails."""
def __init__(self, milter_cfg, cfg):
- logger = logging.getLogger(cfg["name"])
- self.logger = CustomLogger(logger, {"name": cfg["name"]})
- self.logger.setLevel(cfg["loglevel"])
+ 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
@@ -330,6 +474,7 @@ class Action:
self.conditions = Conditions(milter_cfg, cfg["conditions"])
self.pretend = cfg["pretend"]
+ self._func = cfg["func"]
self._args = cfg["args"]
action_type = cfg["type"]
diff --git a/pymodmilter/base.py b/pymodmilter/base.py
new file mode 100644
index 0000000..3235eae
--- /dev/null
+++ b/pymodmilter/base.py
@@ -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 .
+#
+
+__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", "")
diff --git a/pymodmilter/conditions.py b/pymodmilter/conditions.py
index 335baf5..6548862 100644
--- a/pymodmilter/conditions.py
+++ b/pymodmilter/conditions.py
@@ -12,19 +12,66 @@
# along with PyMod-Milter. If not, see .
#
-import logging
+__all__ = [
+ "ConditionsConfig",
+ "Conditions"]
-from netaddr import IPAddress
-from pymodmilter import CustomLogger
+import logging
+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:
"""Conditions to implement conditions for rules and actions."""
def __init__(self, milter_cfg, cfg):
- logger = logging.getLogger(cfg["name"])
- self.logger = CustomLogger(logger, {"name": cfg["name"]})
- self.logger.setLevel(cfg["loglevel"])
+ self.logger = cfg.logger
+ #logger = logging.getLogger(cfg["name"])
+ #self.logger = CustomLogger(logger, {"name": cfg["name"]})
+ #self.logger.setLevel(cfg["loglevel"])
self._local_addrs = milter_cfg["local_addrs"]
self._args = cfg["args"]
diff --git a/pymodmilter/config.py b/pymodmilter/config.py
deleted file mode 100644
index 3d1b651..0000000
--- a/pymodmilter/config.py
+++ /dev/null
@@ -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 .
-#
-
-__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))
diff --git a/pymodmilter/rules.py b/pymodmilter/rules.py
new file mode 100644
index 0000000..2f71c55
--- /dev/null
+++ b/pymodmilter/rules.py
@@ -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 .
+#
+
+__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
diff --git a/pymodmilter/run.py b/pymodmilter/run.py
index b89bebe..4da8f7a 100644
--- a/pymodmilter/run.py
+++ b/pymodmilter/run.py
@@ -12,6 +12,8 @@
# along with PyMod-Milter. If not, see .
#
+__all__ = ["main"]
+
import Milter
import argparse
import logging
@@ -20,7 +22,7 @@ import sys
from pymodmilter import ModifyMilter
from pymodmilter import __version__ as version
-from pymodmilter.config import ModifyMilterConfig
+from pymodmilter import ModifyMilterConfig
def main():
@@ -83,7 +85,7 @@ def main():
try:
logger.debug("prepar milter configuration")
- cfg = ModifyMilterConfig(args.cfgfile, args.debug)
+ cfg = ModifyMilterConfig(args.config, args.debug)
if not args.debug:
logger.setLevel(cfg["loglevel"])
@@ -100,8 +102,6 @@ def main():
if not cfg["rules"]:
raise RuntimeError("no rules configured")
- logger.debug("initializing rules ...")
-
for rule_cfg in cfg["rules"]:
if not rule_cfg["actions"]:
raise RuntimeError(
@@ -112,7 +112,7 @@ def main():
sys.exit(255)
if args.test:
- print("Configuration ok")
+ print("Configuration OK")
sys.exit(0)
# setup console log for runtime
diff --git a/pymodmilter/test.py b/pymodmilter/test.py
deleted file mode 100755
index dbd784f..0000000
--- a/pymodmilter/test.py
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/usr/bin/env python3
-
-import config
-
-a = config.ModifyMilterConfig("test.conf", debug=True)
diff --git a/pymodmilter/test.conf b/test.conf
similarity index 97%
rename from pymodmilter/test.conf
rename to test.conf
index 998e537..bcbbfde 100644
--- a/pymodmilter/test.conf
+++ b/test.conf
@@ -176,14 +176,14 @@
# Notes: Path to a file which contains the html representation of the disclaimer.
# Value: [ FILE_PATH ]
#
- "html_template": "docs/templates/disclaimer_html.template",
+ "html_template": "pymodmilter/docs/templates/disclaimer_html.template",
# Option: text_template
# Type: String
# Notes: Path to a file which contains the text representation of the disclaimer.
# Value: [ FILE_PATH ]
#
- "text_template": "docs/templates/disclaimer_text.template",
+ "text_template": "pymodmilter/docs/templates/disclaimer_text.template",
# Option: error_policy
# Type: String