From 42e65848c4d213271b530e595594e47e078b127c Mon Sep 17 00:00:00 2001 From: Thomas Oettli Date: Mon, 20 Sep 2021 18:08:56 +0200 Subject: [PATCH] refactor config structure --- pymodmilter/__init__.py | 39 +++--- pymodmilter/action.py | 270 ++++++++++++++++++-------------------- pymodmilter/base.py | 62 ++++----- pymodmilter/conditions.py | 34 ++--- pymodmilter/rule.py | 72 +++++----- pymodmilter/run.py | 14 +- pymodmilter/storage.py | 6 + 7 files changed, 240 insertions(+), 257 deletions(-) diff --git a/pymodmilter/__init__.py b/pymodmilter/__init__.py index bbc3e08..cc5f5d2 100644 --- a/pymodmilter/__init__.py +++ b/pymodmilter/__init__.py @@ -79,17 +79,17 @@ class ModifyMilterConfig(BaseConfig): pretend = cfg["global"]["pretend"] assert isinstance(pretend, bool), \ "global: pretend: invalid value, should be bool" - self["pretend"] = pretend + self.pretend = pretend else: - self["pretend"] = False + 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 + self.socket = socket else: - self["socket"] = None + self.socket = None if "local_addrs" in cfg["global"]: local_addrs = cfg["global"]["local_addrs"] @@ -106,17 +106,17 @@ class ModifyMilterConfig(BaseConfig): "172.16.0.0/12", "192.168.0.0/16"] - self["local_addrs"] = [] + self.local_addrs = [] try: for addr in local_addrs: - self["local_addrs"].append(IPNetwork(addr)) + self.local_addrs.append(IPNetwork(addr)) except AddrFormatError as e: - raise ValueError(f"{self['name']}: local_addrs: {e}") + raise ValueError(f"{self.name}: local_addrs: {e}") - self.logger.debug(f"socket={self['socket']}, " - f"local_addrs={self['local_addrs']}, " - f"pretend={self['pretend']}, " - f"loglevel={self['loglevel']}") + 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" @@ -124,10 +124,15 @@ class ModifyMilterConfig(BaseConfig): "rules: invalid value, should be list" self.logger.debug("initialize rules config") - self["rules"] = [] + self.rules = [] for idx, rule_cfg in enumerate(cfg["rules"]): - self["rules"].append( - RuleConfig(idx, self, rule_cfg, debug)) + if "name" not in rule_cfg: + rule_cfg["name"] = "Rule #{idx}" + if "loglevel" not in rule_cfg: + rule_cfg["loglevel"] = self.loglevel + if "pretend" not in rule_cfg: + rule_cfg["pretend"] = self.pretend + self.rules.append(RuleConfig(rule_cfg, debug)) class ModifyMilter(Milter.Base): @@ -140,10 +145,10 @@ class ModifyMilter(Milter.Base): @staticmethod def set_config(cfg): - ModifyMilter._loglevel = cfg["loglevel"] - for rule_cfg in cfg["rules"]: + ModifyMilter._loglevel = cfg.loglevel + for rule_cfg in cfg.rules: ModifyMilter._rules.append( - Rule(cfg, rule_cfg)) + Rule(rule_cfg, cfg.local_addrs)) def __init__(self): self.logger = logging.getLogger(__name__) diff --git a/pymodmilter/action.py b/pymodmilter/action.py index c45620d..569acdf 100644 --- a/pymodmilter/action.py +++ b/pymodmilter/action.py @@ -16,8 +16,6 @@ __all__ = [ "ActionConfig", "Action"] -import os - from pymodmilter import BaseConfig from pymodmilter import modify, notify, storage from pymodmilter.base import CustomLogger @@ -25,163 +23,153 @@ from pymodmilter.conditions import ConditionsConfig, Conditions class ActionConfig(BaseConfig): - def __init__(self, idx, rule_cfg, cfg, debug): - if "name" in cfg: - assert isinstance(cfg["name"], str), \ - f"{rule_cfg['name']}: Action #{idx}: name: invalid value, " \ - f"should be string" - 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"] + TYPES = {"add_header": "_add_header", + "mod_header": "_mod_header", + "del_header": "_del_header", + "add_disclaimer": "_add_disclaimer", + "rewrite_links": "_rewrite_links", + "store": "_store", + "notify": "_notify"} + def __init__(self, cfg, debug): super().__init__(cfg, debug) - self["pretend"] = rule_cfg["pretend"] - self["conditions"] = None - + self.pretend = False if "pretend" in cfg: - pretend = cfg["pretend"] - assert isinstance(pretend, bool), \ - f"{self['name']}: pretend: invalid value, should be bool" - self["pretend"] = pretend + assert isinstance(cfg["pretend"], bool), \ + f"{self.name}: pretend: invalid value, should be bool" + self.pretend = cfg["pretend"] assert "type" in cfg, \ - f"{self['name']}: mandatory parameter 'type' not found" + f"{self.name}: mandatory parameter 'type' not found" assert isinstance(cfg["type"], str), \ - f"{self['name']}: type: invalid value, should be string" + f"{self.name}: type: invalid value, should be string" + assert cfg["type"] in ActionConfig.TYPES, \ + f"{self.name}: type: invalid action type" - if cfg["type"] == "add_header": - self["class"] = modify.AddHeader - self["headersonly"] = True - self.add_string_arg(cfg, ["field", "value"]) - elif cfg["type"] == "mod_header": - self["class"] = modify.ModHeader - self["headersonly"] = True - args = ["field", "value"] - if "search" in cfg: - args.append("search") - - self.add_string_arg(cfg, args) - elif cfg["type"] == "del_header": - self["class"] = modify.DelHeader - self["headersonly"] = True - args = ["field"] - if "value" in cfg: - args.append("value") - - self.add_string_arg(cfg, args) - elif cfg["type"] == "add_disclaimer": - self["class"] = modify.AddDisclaimer - self["headersonly"] = False - 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'" - - elif cfg["type"] == "rewrite_links": - self["class"] = modify.RewriteLinks - self["headersonly"] = False - self.add_string_arg(cfg, "repl") - - elif cfg["type"] == "store": - self["headersonly"] = False - - assert "storage_type" in cfg, \ - f"{self['name']}: mandatory parameter 'storage_type' not found" - assert isinstance(cfg["storage_type"], str), \ - f"{self['name']}: storage_type: invalid value, " \ - f"should be string" - self["storage_type"] = cfg["storage_type"] - - if "original" in cfg: - self.add_bool_arg(cfg, "original") - - if self["storage_type"] == "file": - self["class"] = storage.FileMailStorage - self.add_string_arg(cfg, "directory") - # check if directory exists and is writable - if not os.path.isdir(self["args"]["directory"]) or \ - not os.access(self["args"]["directory"], os.W_OK): - raise RuntimeError( - f"{self['name']}: file quarantine directory " - f"'{self['directory']}' does not exist or is " - f"not writable") - - if "skip_metadata" in cfg: - self.add_bool_arg(cfg, "skip_metadata") - - if "metavar" in cfg: - self.add_string_arg(cfg, "metavar") - - else: - raise RuntimeError( - f"{self['name']}: storage_type: invalid storage type") - - elif cfg["type"] == "notify": - self["headersonly"] = False - self["class"] = notify.EMailNotification - - args = ["smtp_host", "envelope_from", "from_header", "subject", - "template"] - if "repl_img" in cfg: - args.append("repl_img") - - self.add_string_arg(cfg, args) - self.add_int_arg(cfg, "smtp_port") - - if "embed_imgs" in cfg: - assert isinstance(cfg["embed_imgs"], list), \ - f"{self['name']}: embed_imgs: invalid value, " \ - f"should be list" - for img in cfg["embed_imgs"]: - assert isinstance(img, str), \ - f"{self['name']}: embed_imgs: invalid entry, " \ - f"should be string" - - self["args"]["embed_imgs"] = cfg["embed_imgs"] - - else: - raise RuntimeError(f"{self['name']}: type: invalid action type") + getattr(self, cfg["type"])(cfg) 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) + assert isinstance(cfg["conditions"], dict), \ + f"{self.name}: conditions: invalid value, should be dict" + cfg["conditions"]["name"] = f"{self.name}: condition" + if "loglevel" not in cfg["conditions"]: + cfg["conditions"]["loglevel"] = self.loglevel + self.conditions = ConditionsConfig(cfg["conditions"], debug) + else: + self.conditions = None - self.logger.debug(f"{self['name']}: pretend={self['pretend']}, " - f"loglevel={self['loglevel']}, " + self.logger.debug(f"{self.name}: pretend={self.pretend}, " + f"loglevel={self.loglevel}, " f"type={cfg['type']}, " - f"args={self['args']}") + f"args={self.args}") + + def add_header(self, cfg): + self.action = modify.AddHeader + self.headersonly = True + self.add_string_arg(cfg, ["field", "value"]) + + def mod_header(self, cfg): + self.action = modify.ModHeader + self.headersonly = True + args = ["field", "value"] + if "search" in cfg: + args.append("search") + + self.add_string_arg(cfg, args) + + def del_header(self, cfg): + self.action = modify.DelHeader + self.headersonly = True + args = ["field"] + if "value" in cfg: + args.append("value") + + self.add_string_arg(cfg, args) + + def add_disclaimer(self, cfg): + self.action = modify.AddDisclaimer + self.headersonly = False + 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'" + + def rewrite_links(self, cfg): + self.action = modify.RewriteLinks + self.headersonly = False + self.add_string_arg(cfg, "repl") + + def store(self, cfg): + self.headersonly = False + + assert "storage_type" in cfg, \ + f"{self.name}: mandatory parameter 'storage_type' not found" + assert isinstance(cfg["storage_type"], str), \ + f"{self.name}: storage_type: invalid value, " \ + f"should be string" + + if "original" in cfg: + self.add_bool_arg(cfg, "original") + + if cfg["storage_type"] == "file": + self.action = storage.FileMailStorage + self.add_string_arg(cfg, "directory") + + if "skip_metadata" in cfg: + self.add_bool_arg(cfg, "skip_metadata") + + if "metavar" in cfg: + self.add_string_arg(cfg, "metavar") + + else: + raise RuntimeError( + f"{self.name}: storage_type: invalid storage type") + + def notify(self, cfg): + self.headersonly = False + self.action = notify.EMailNotification + + args = ["smtp_host", "envelope_from", "from_header", "subject", + "template"] + if "repl_img" in cfg: + args.append("repl_img") + + self.add_string_arg(cfg, args) + self.add_int_arg(cfg, "smtp_port") + + if "embed_imgs" in cfg: + assert isinstance(cfg["embed_imgs"], list) and all( + [isinstance(img, str) for img in cfg["embed_imgs"]]), \ + f"{self.name}: embed_imgs: invalid value, " \ + f"should be list of strings" + self.args["embed_imgs"] = cfg["embed_imgs"] class Action: """Action to implement a pre-configured action to perform on e-mails.""" - def __init__(self, milter_cfg, cfg): - if cfg["conditions"] is None: + def __init__(self, cfg, local_addrs): + self.logger = cfg.logger + if cfg.conditions is None: self.conditions = None else: - self.conditions = Conditions(milter_cfg, cfg["conditions"]) + self.conditions = Conditions(cfg.conditions, local_addrs) - self.pretend = cfg["pretend"] - self._name = cfg["name"] - self._class = cfg["class"](**cfg["args"]) - self._headersonly = cfg["headersonly"] - self.logger = cfg.logger + self.pretend = cfg.pretend + self.name = cfg.name + self.action = cfg.action(**cfg.args) + self._headersonly = cfg.headersonly def headersonly(self): """Return the needs of this action.""" @@ -190,8 +178,8 @@ class Action: def execute(self, milter): """Execute configured action.""" logger = CustomLogger( - self.logger, {"qid": milter.qid, "name": self._name}) + self.logger, {"qid": milter.qid, "name": self.name}) if self.conditions is None or \ self.conditions.match(milter): - return self._class.execute( + return self.action.execute( milter=milter, pretend=self.pretend, logger=logger) diff --git a/pymodmilter/base.py b/pymodmilter/base.py index 2ec6a6f..004826b 100644 --- a/pymodmilter/base.py +++ b/pymodmilter/base.py @@ -38,49 +38,33 @@ class CustomLogger(logging.LoggerAdapter): class BaseConfig: - def __init__(self, cfg={}, debug=False, logger=None): - self._cfg = {} + def __init__(self, cfg={}, debug=False): if "name" in cfg: assert isinstance(cfg["name"], str), \ - "rule: name: invalid value, should be string" - self["name"] = cfg["name"] + "name: invalid value, should be string" + self.name = cfg["name"] else: - self["name"] = __name__ + self.name = __name__ + self.logger = logging.getLogger(self.name) if debug: - self["loglevel"] = logging.DEBUG + self.loglevel = logging.DEBUG elif "loglevel" in cfg: if isinstance(cfg["loglevel"], int): - self["loglevel"] = cfg["loglevel"] + 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 + f"{self.name}: loglevel: invalid value" + self.loglevel = level else: - self["loglevel"] = logging.INFO + self.loglevel = logging.INFO - if logger is None: - logger = logging.getLogger(self["name"]) - logger.setLevel(self["loglevel"]) + self.logger.setLevel(self.loglevel) - self.logger = logger - - # 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 + # the keys/values in args are used as parameters + # to initialize action classes + self.args = {} def add_string_arg(self, cfg, args): if isinstance(args, str): @@ -88,10 +72,10 @@ class BaseConfig: for arg in args: assert arg in cfg, \ - f"{self['name']}: mandatory parameter '{arg}' not found" + 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] + 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): @@ -99,10 +83,10 @@ class BaseConfig: for arg in args: assert arg in cfg, \ - f"{self['name']}: mandatory parameter '{arg}' not found" + 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] + f"{self.name}: {arg}: invalid value, should be bool" + self.args[arg] = cfg[arg] def add_int_arg(self, cfg, args): if isinstance(args, str): @@ -110,10 +94,10 @@ class BaseConfig: for arg in args: assert arg in cfg, \ - f"{self['name']}: mandatory parameter '{arg}' not found" + f"{self.name}: mandatory parameter '{arg}' not found" assert isinstance(cfg[arg], int), \ - f"{self['name']}: {arg}: invalid value, should be integer" - self["args"][arg] = cfg[arg] + f"{self.name}: {arg}: invalid value, should be integer" + self.args[arg] = cfg[arg] class MilterMessage(MIMEPart): diff --git a/pymodmilter/conditions.py b/pymodmilter/conditions.py index 10b2ff8..7df41b5 100644 --- a/pymodmilter/conditions.py +++ b/pymodmilter/conditions.py @@ -23,12 +23,7 @@ from pymodmilter import BaseConfig, CustomLogger 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" - + def __init__(self, cfg, debug): super().__init__(cfg, debug) if "local" in cfg: @@ -37,10 +32,10 @@ class ConditionsConfig(BaseConfig): if "hosts" in cfg: assert isinstance(cfg["hosts"], list) and all( [isinstance(host, str) for host in cfg["hosts"]]), \ - f"{self['name']}: hosts: invalid value, " \ + f"{self.name}: hosts: invalid value, " \ f"should be list of strings" - self["args"]["hosts"] = cfg["hosts"] + self.args["hosts"] = cfg["hosts"] for arg in ("envfrom", "envto"): if arg in cfg: @@ -55,21 +50,22 @@ class ConditionsConfig(BaseConfig): if "metavar" in cfg: self.add_string_arg(cfg, "metavar") - self.logger.debug(f"{self['name']}: " - f"loglevel={self['loglevel']}, " - f"args={self['args']}") + 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): - self._local_addrs = milter_cfg["local_addrs"] - self._name = cfg["name"] + def __init__(self, cfg, local_addrs): + self.logger = cfg.logger + self.name = cfg.name + self.local_addrs = local_addrs for arg in ("local", "hosts", "envfrom", "envto", "header", "metavar", "var"): - value = cfg["args"][arg] if arg in cfg["args"] else None + value = cfg.args[arg] if arg in cfg.args else None setattr(self, arg, value) if value is None: continue @@ -96,17 +92,15 @@ class Conditions: except re.error as e: raise RuntimeError(e) - self.logger = cfg.logger - def match_host(self, host): logger = CustomLogger( - self.logger, {"name": self._name}) + self.logger, {"name": self.name}) ip = IPAddress(host) if self.local is not None: is_local = False - for addr in self._local_addrs: + for addr in self.local_addrs: if ip in addr: is_local = True break @@ -140,7 +134,7 @@ class Conditions: def match(self, milter): logger = CustomLogger( - self.logger, {"qid": milter.qid, "name": self._name}) + self.logger, {"qid": milter.qid, "name": self.name}) if self.envfrom is not None: envfrom = milter.msginfo["mailfrom"] diff --git a/pymodmilter/rule.py b/pymodmilter/rule.py index c6a342e..3341ae2 100644 --- a/pymodmilter/rule.py +++ b/pymodmilter/rule.py @@ -22,46 +22,52 @@ from pymodmilter.conditions import ConditionsConfig, Conditions class RuleConfig(BaseConfig): - def __init__(self, idx, milter_cfg, cfg, debug=False): - if "name" in cfg: - assert isinstance(cfg["name"], str), \ - f"Rule #{idx}: name: invalid value, should be string" - else: - cfg["name"] = f"Rule #{idx}" - - if "loglevel" not in cfg: - cfg["loglevel"] = milter_cfg["loglevel"] - + def __init__(self, cfg, debug=False): super().__init__(cfg, debug) - self["pretend"] = milter_cfg["pretend"] - self["conditions"] = None - self["actions"] = [] + self.conditions = None + self.actions = [] + self.pretend = False if "pretend" in cfg: - pretend = cfg["pretend"] - assert isinstance(pretend, bool), \ - f"{self['name']}: pretend: invalid value, should be bool" - self["pretend"] = pretend + assert isinstance(cfg["pretend"], bool), \ + f"{self.name}: pretend: invalid value, should be bool" + self.pretend = cfg["pretend"] assert "actions" in cfg, \ - f"{self['name']}: mandatory parameter 'actions' not found" + f"{self.name}: mandatory parameter 'actions' not found" actions = cfg["actions"] assert isinstance(actions, list), \ - f"{self['name']}: actions: invalid value, should be list" + f"{self.name}: actions: invalid value, should be list" - self.logger.debug(f"{self['name']}: pretend={self['pretend']}, " - f"loglevel={self['loglevel']}") + self.logger.debug(f"{self.name}: 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) + assert isinstance(cfg["conditions"], dict), \ + f"{self.name}: conditions: invalid value, should be dict" + cfg["conditions"]["name"] = f"{self.name}: condition" + if "loglevel" not in cfg["conditions"]: + cfg["conditions"]["loglevel"] = self.loglevel + self.conditions = ConditionsConfig(cfg["conditions"], debug) + else: + self.conditions = None for idx, action_cfg in enumerate(cfg["actions"]): - self["actions"].append( - ActionConfig(idx, self, action_cfg, debug)) + if "name" in action_cfg: + assert isinstance(action_cfg["name"], str), \ + f"{self.name}: Action #{idx}: name: invalid value, " \ + f"should be string" + action_cfg["name"] = f"{self.name}: {action_cfg['name']}" + else: + action_cfg["name"] = f"{self.name}: Action #{idx}" + + if "loglevel" not in action_cfg: + action_cfg["loglevel"] = self.loglevel + if "pretend" not in action_cfg: + action_cfg["pretend"] = self.pretend + self.actions.append( + ActionConfig(action_cfg, debug)) class Rule: @@ -69,19 +75,19 @@ class Rule: Rule to implement multiple actions on emails. """ - def __init__(self, milter_cfg, cfg): + def __init__(self, cfg, local_addrs): self.logger = cfg.logger - if cfg["conditions"] is None: + if cfg.conditions is None: self.conditions = None else: - self.conditions = Conditions(milter_cfg, cfg["conditions"]) + self.conditions = Conditions(cfg.conditions, local_addrs) self.actions = [] - for action_cfg in cfg["actions"]: - self.actions.append(Action(milter_cfg, action_cfg)) + for action_cfg in cfg.actions: + self.actions.append(Action(action_cfg, local_addrs)) - self.pretend = cfg["pretend"] + self.pretend = cfg.pretend def execute(self, milter): """Execute all actions of this rule.""" diff --git a/pymodmilter/run.py b/pymodmilter/run.py index a3a64d8..6bd6304 100644 --- a/pymodmilter/run.py +++ b/pymodmilter/run.py @@ -84,24 +84,24 @@ def main(): cfg = ModifyMilterConfig(args.config, args.debug) if not args.debug: - logger.setLevel(cfg["loglevel"]) + logger.setLevel(cfg.loglevel) if args.socket: socket = args.socket - elif "socket" in cfg: - socket = cfg["socket"] + elif cfg.socket: + socket = cfg.socket else: raise RuntimeError( "listening socket is neither specified on the command line " "nor in the configuration file") - if not cfg["rules"]: + if not cfg.rules: raise RuntimeError("no rules configured") - for rule_cfg in cfg["rules"]: - if not rule_cfg["actions"]: + for rule_cfg in cfg.rules: + if not rule_cfg.actions: raise RuntimeError( - f"{rule_cfg['name']}: no actions configured") + f"{rule_cfg.name}: no actions configured") except (RuntimeError, AssertionError) as e: logger.error(e) diff --git a/pymodmilter/storage.py b/pymodmilter/storage.py index 93fb518..7a058f3 100644 --- a/pymodmilter/storage.py +++ b/pymodmilter/storage.py @@ -57,6 +57,12 @@ class FileMailStorage(BaseMailStorage): def __init__(self, directory, original=False, skip_metadata=False, metavar=None): super().__init__() + # check if directory exists and is writable + if not os.path.isdir(directory) or \ + not os.access(directory, os.W_OK): + raise RuntimeError( + f"directory '{directory}' does not exist or is " + f"not writable") self.directory = directory self.original = original self.skip_metadata = skip_metadata