change config structure

This commit is contained in:
2024-01-03 19:54:34 +01:00
parent 5d21b81530
commit 4da1a0e9b3
7 changed files with 194 additions and 99 deletions

View File

@@ -27,7 +27,7 @@ __all__ = [
"list", "list",
"QuarantineMilter"] "QuarantineMilter"]
__version__ = "2.0.8" __version__ = "2.1.0"
from pyquarantine import _runtime_patches from pyquarantine import _runtime_patches

View File

@@ -39,8 +39,7 @@ class Action:
self.conditions["loglevel"] = cfg["loglevel"] self.conditions["loglevel"] = cfg["loglevel"]
self.conditions = Conditions(self.conditions, local_addrs, debug) self.conditions = Conditions(self.conditions, local_addrs, debug)
action_type = cfg["type"] self.action = self.ACTION_TYPES[cfg["type"]](
self.action = self.ACTION_TYPES[action_type](
cfg, local_addrs, debug) cfg, local_addrs, debug)
def __str__(self): def __str__(self):

View File

@@ -21,28 +21,31 @@ import logging.handlers
import sys import sys
import time import time
from pyquarantine.config import get_milter_config, ActionConfig, ListConfig from pyquarantine.action import Action
from pyquarantine.config import get_milter_config, ActionConfig, StorageConfig, NotificationConfig, ListConfig
from pyquarantine.storage import Quarantine from pyquarantine.storage import Quarantine
from pyquarantine.list import DatabaseList from pyquarantine.list import DatabaseList
from pyquarantine import __version__ as version from pyquarantine import __version__ as version
def _get_quarantines(cfg): def _get_quarantines(milter_cfg):
quarantines = [] quarantines = []
for rule in cfg["rules"]: for rule in milter_cfg["rules"]:
for action in rule["actions"]: for action in rule["actions"]:
if action["type"] == "quarantine": if action["type"] == "quarantine":
quarantines.append(action) quarantines.append(action)
return quarantines return quarantines
def _get_quarantine(cfg, name, debug): def _get_quarantine(milter_cfg, name, debug):
try: try:
quarantine = next( quarantine = next(
(q for q in _get_quarantines(cfg) if q["name"] == name)) (q for q in _get_quarantines(milter_cfg) if q["name"] == name))
except StopIteration: except StopIteration:
raise RuntimeError(f"invalid quarantine '{name}'") raise RuntimeError(f"invalid quarantine '{name}'")
return Quarantine(ActionConfig(quarantine, cfg["lists"]), [], debug)
cfg = ActionConfig(quarantine, milter_cfg)
return Quarantine(cfg, [], debug)
def _get_notification(cfg, name, debug): def _get_notification(cfg, name, debug):
@@ -55,7 +58,7 @@ def _get_notification(cfg, name, debug):
def _get_list(cfg, name, debug): def _get_list(cfg, name, debug):
try: try:
list_cfg = ListConfig(cfg["lists"][name], {}) list_cfg = cfg["lists"][name]
except KeyError: except KeyError:
raise RuntimeError(f"list '{name}' is not configured") raise RuntimeError(f"list '{name}' is not configured")
@@ -107,7 +110,7 @@ def print_table(columns, rows):
print(row_format.format(*row)) print(row_format.format(*row))
def list_quarantines(cfg, args): def llist(cfg, args):
quarantines = _get_quarantines(cfg) quarantines = _get_quarantines(cfg)
if args.batch: if args.batch:
print("\n".join([q["name"] for q in quarantines])) print("\n".join([q["name"] for q in quarantines]))
@@ -115,32 +118,33 @@ def list_quarantines(cfg, args):
qlist = [] qlist = []
for q in quarantines: for q in quarantines:
qcfg = q["options"] qcfg = q["options"]
storage_type = qcfg["store"]["type"]
if "notify" in cfg: if "notify" in qcfg:
notification_type = qcfg["notify"]["type"] notification = cfg["notifications"][qcfg["notify"]]["name"]
else: else:
notification_type = "NONE" notification = "NONE"
if "lists" in qcfg: if "allowlist" in qcfg:
lists_type = qcfg["lists"] allowlist = qcfg["allowlist"]
else: else:
lists_type = "NONE" allowlist = "NONE"
if "milter_action" in qcfg: if "milter_action" in qcfg:
milter_action = qcfg["milter_action"] milter_action = qcfg["milter_action"]
else: else:
milter_action = "NONE" milter_action = "NONE"
storage_name = cfg["storages"][qcfg["store"]]["name"]
qlist.append({ qlist.append({
"name": q["name"], "name": q["name"],
"storage": storage_type, "storage": storage_name,
"notification": notification_type, "notification": notification,
"lists": lists_type, "lists": allowlist,
"action": milter_action}) "action": milter_action})
print_table( print_table(
[("Name", "name"), [("Quarantine", "name"),
("Storage", "storage"), ("Storage", "storage"),
("Notification", "notification"), ("Notification", "notification"),
("Allowlist", "lists"), ("Allowlist", "lists"),
@@ -148,6 +152,48 @@ def list_quarantines(cfg, args):
qlist qlist
) )
if "storages" in cfg:
storages = []
for name, options in cfg["storages"].items():
storages.append({
"name": name,
"type": options["type"]})
print("\n")
print_table(
[("Storage", "name"),
("Type", "type")],
storages
)
if "notifications" in cfg:
notifications = []
for name, options in cfg["notifications"].items():
notifications.append({
"name": name,
"type": options["type"]})
print("\n")
print_table(
[("Notification", "name"),
("Type", "type")],
notifications
)
if "lists" in cfg:
lst_list = []
for name, options in cfg["lists"].items():
lst_list.append({
"name": name,
"type": options["type"]})
print("\n")
print_table(
[("List", "name"),
("Type", "type")],
lst_list
)
def list_quarantine_emails(cfg, args): def list_quarantine_emails(cfg, args):
storage = _get_quarantine(cfg, args.quarantine, args.debug).storage storage = _get_quarantine(cfg, args.quarantine, args.debug).storage
@@ -364,7 +410,7 @@ def main():
"-b", "--batch", "-b", "--batch",
help="Print results using only quarantine names, each on a new line.", help="Print results using only quarantine names, each on a new line.",
action="store_true") action="store_true")
list_parser.set_defaults(func=list_quarantines) list_parser.set_defaults(func=llist)
# quarantine command group # quarantine command group
quar_parser = subparsers.add_parser( quar_parser = subparsers.add_parser(
@@ -638,7 +684,7 @@ def main():
try: try:
logger.debug("read milter configuration") logger.debug("read milter configuration")
cfg = get_milter_config(args.config, raw=True) cfg = get_milter_config(args.config, rec=False)
if "rules" not in cfg or not cfg["rules"]: if "rules" not in cfg or not cfg["rules"]:
raise RuntimeError("no rules configured") raise RuntimeError("no rules configured")

View File

@@ -20,7 +20,9 @@ __all__ = [
"DelHeaderConfig", "DelHeaderConfig",
"AddDisclaimerConfig", "AddDisclaimerConfig",
"RewriteLinksConfig", "RewriteLinksConfig",
"StorageConfig",
"StoreConfig", "StoreConfig",
"NotificationConfig",
"NotifyConfig", "NotifyConfig",
"ListConfig", "ListConfig",
"QuarantineConfig", "QuarantineConfig",
@@ -43,7 +45,7 @@ class BaseConfig:
"properties": { "properties": {
"loglevel": {"type": "string", "default": "info"}}} "loglevel": {"type": "string", "default": "info"}}}
def __init__(self, config, lists): def __init__(self, config, *args, **kwargs):
required = self.JSON_SCHEMA["required"] required = self.JSON_SCHEMA["required"]
properties = self.JSON_SCHEMA["properties"] properties = self.JSON_SCHEMA["properties"]
for p in properties.keys(): for p in properties.keys():
@@ -126,15 +128,13 @@ class ConditionsConfig(BaseConfig):
"list": {"type": "string"}}} "list": {"type": "string"}}}
def __init__(self, config, lists, rec=True): def __init__(self, config, lists, rec=True):
super().__init__(config, lists) super().__init__(config)
if "list" in self: if "list" in self:
lst = self["list"] lst = self["list"]
try: try:
self["list"] = lists[lst] self["list"] = lists[lst]
except KeyError: except KeyError:
raise RuntimeError(f"list '{lst}' is not configured") raise RuntimeError(f"list '{lst}' not found in config")
if not rec:
return
class AddHeaderConfig(BaseConfig): class AddHeaderConfig(BaseConfig):
@@ -190,7 +190,7 @@ class RewriteLinksConfig(BaseConfig):
"repl": {"type": "string"}}} "repl": {"type": "string"}}}
class StoreConfig(BaseConfig): class StorageConfig(BaseConfig):
JSON_SCHEMA = { JSON_SCHEMA = {
"type": "object", "type": "object",
"required": ["type"], "required": ["type"],
@@ -203,6 +203,7 @@ class StoreConfig(BaseConfig):
"additionalProperties": False, "additionalProperties": False,
"properties": { "properties": {
"type": {"type": "string"}, "type": {"type": "string"},
"name": {"type": "string"},
"directory": {"type": "string"}, "directory": {"type": "string"},
"mode": {"type": "string"}, "mode": {"type": "string"},
"metavar": {"type": "string"}, "metavar": {"type": "string"},
@@ -210,7 +211,16 @@ class StoreConfig(BaseConfig):
"original": {"type": "boolean", "default": False}}}} "original": {"type": "boolean", "default": False}}}}
class NotifyConfig(BaseConfig): class StoreConfig(BaseConfig):
JSON_SCHEMA = {
"type": "object",
"required": ["storage"],
"additionalProperties": False,
"properties": {
"storage": {"type": "string"}}}
class NotificationConfig(BaseConfig):
JSON_SCHEMA = { JSON_SCHEMA = {
"type": "object", "type": "object",
"required": ["type"], "required": ["type"],
@@ -224,6 +234,7 @@ class NotifyConfig(BaseConfig):
"additionalProperties": False, "additionalProperties": False,
"properties": { "properties": {
"type": {"type": "string"}, "type": {"type": "string"},
"name": {"type": "string"},
"smtp_host": {"type": "string"}, "smtp_host": {"type": "string"},
"smtp_port": {"type": "number"}, "smtp_port": {"type": "number"},
"envelope_from": {"type": "string"}, "envelope_from": {"type": "string"},
@@ -238,6 +249,15 @@ class NotifyConfig(BaseConfig):
"default": []}}}} "default": []}}}}
class NotifyConfig(BaseConfig):
JSON_SCHEMA = {
"type": "object",
"required": ["notification"],
"additionalProperties": False,
"properties": {
"notification": {"type": "string"}}}
class QuarantineConfig(BaseConfig): class QuarantineConfig(BaseConfig):
JSON_SCHEMA = { JSON_SCHEMA = {
"type": "object", "type": "object",
@@ -245,29 +265,38 @@ class QuarantineConfig(BaseConfig):
"additionalProperties": False, "additionalProperties": False,
"properties": { "properties": {
"name": {"type": "string"}, "name": {"type": "string"},
"notify": {"type": "object"}, "notify": {"type": "string"},
"milter_action": {"type": "string"}, "milter_action": {"type": "string"},
"reject_reason": {"type": "string"}, "reject_reason": {"type": "string"},
"allowlist": {"type": "string"}, "allowlist": {"type": "string"},
"store": {"type": "object"}, "store": {"type": "string"},
"smtp_host": {"type": "string"}, "smtp_host": {"type": "string"},
"smtp_port": {"type": "number"}}} "smtp_port": {"type": "number"}}}
def __init__(self, config, lists, rec=True): def __init__(self, config, milter_config, rec=True):
super().__init__(config, lists) super().__init__(config)
if not rec: storage = self["store"]
return try:
self["store"] = milter_config["storages"][storage]
except KeyError:
raise RuntimeError(f"storage '{storage}' not found")
if "metadata" not in self["store"]: if "metadata" not in self["store"]:
self["store"]["metadata"] = True self["store"]["metadata"] = True
self["store"] = StoreConfig(self["store"], lists)
if "notify" in self: if "notify" in self:
self["notify"] = NotifyConfig(self["notify"], lists) notify = self["notify"]
try:
self["notify"] = milter_config["notifications"][notify]
except KeyError:
raise RuntimeError(f"notification '{notify}' not found")
if "allowlist" in self: if "allowlist" in self:
allowlist = self["allowlist"] allowlist = self["allowlist"]
try: try:
self["allowlist"] = lists[allowlist] self["allowlist"] = milter_config["lists"][allowlist]
except KeyError: except KeyError:
raise RuntimeError(f"list '{allowlist}' is not configured") raise RuntimeError(f"list '{allowlist}' not found")
if not rec:
return
class ActionConfig(BaseConfig): class ActionConfig(BaseConfig):
@@ -293,14 +322,31 @@ class ActionConfig(BaseConfig):
"type": {"enum": list(ACTION_TYPES.keys())}, "type": {"enum": list(ACTION_TYPES.keys())},
"options": {"type": "object"}}} "options": {"type": "object"}}}
def __init__(self, config, lists, rec=True): def __init__(self, config, milter_config, rec=True):
super().__init__(config, lists) super().__init__(config)
if not rec: if not rec:
return return
lists = milter_config["lists"]
if "conditions" in self: if "conditions" in self:
self["conditions"] = ConditionsConfig(self["conditions"], lists) self["conditions"] = ConditionsConfig(self["conditions"], lists)
self["action"] = self.ACTION_TYPES[self["type"]](
self["options"], lists) if self["type"] == "store":
storage = StoreConfig(self["options"])["storage"]
try:
self["action"] = milter_config["storages"][storage]
except KeyError:
raise RuntimeError(f"storage '{storage}' not found")
elif self["type"] == "notify":
notify = NotifyConfig(self["options"])["notification"]
try:
self["action"] = milter_config["notifications"][notify]
except KeyError:
raise RuntimeError(f"notification '{notify}' not found")
else:
self["action"] = self.ACTION_TYPES[self["type"]](
self["options"], milter_config)
class RuleConfig(BaseConfig): class RuleConfig(BaseConfig):
@@ -315,10 +361,11 @@ class RuleConfig(BaseConfig):
"conditions": {"type": "object"}, "conditions": {"type": "object"},
"actions": {"type": "array"}}} "actions": {"type": "array"}}}
def __init__(self, config, lists, rec=True): def __init__(self, config, milter_config, rec=True):
super().__init__(config, lists) super().__init__(config)
if not rec: if not rec:
return return
lists = milter_config["lists"]
if "conditions" in self: if "conditions" in self:
self["conditions"] = ConditionsConfig(self["conditions"], lists) self["conditions"] = ConditionsConfig(self["conditions"], lists)
@@ -328,7 +375,7 @@ class RuleConfig(BaseConfig):
action["loglevel"] = config["loglevel"] action["loglevel"] = config["loglevel"]
if "pretend" not in action: if "pretend" not in action:
action["pretend"] = config["pretend"] action["pretend"] = config["pretend"]
actions.append(ActionConfig(action, lists, rec)) actions.append(ActionConfig(action, milter_config, rec))
self["actions"] = actions self["actions"] = actions
@@ -350,29 +397,52 @@ class QuarantineMilterConfig(BaseConfig):
"192.168.0.0/16"]}, "192.168.0.0/16"]},
"loglevel": {"type": "string", "default": "info"}, "loglevel": {"type": "string", "default": "info"},
"pretend": {"type": "boolean", "default": False}, "pretend": {"type": "boolean", "default": False},
"rules": {"type": "array"}, "lists": {
"lists": {"type": "array", "type": "object",
"default": []}}} "patternProperties": {"^(.+)$": {"type": "object"}},
"additionalProperties": False,
"default": {}},
"storages": {
"type": "object",
"patternProperties": {"^(.+)$": {"type": "object"}},
"additionalProperties": False,
"default": {}},
"notifications": {
"type": "object",
"patternProperties": {"^(.+)$": {"type": "object"}},
"additionalProperties": False,
"default": {}},
"rules": {"type": "array"}}}
def __init__(self, config, rec=True): def __init__(self, config, rec=True):
super().__init__(config, {}) super().__init__(config)
for param in ["lists", "storages", "notifications"]:
for name, cfg in self[param].items():
if "name" not in cfg:
cfg["name"] = name
for name, cfg in self["lists"].items():
self["lists"][name] = ListConfig(cfg)
for name, cfg in self["storages"].items():
self["storages"][name] = StorageConfig(cfg)
for name, cfg in self["notifications"].items():
self["notifications"][name] = NotificationConfig(cfg)
if not rec: if not rec:
return return
lists = {}
for lst in self["lists"]:
lists[lst["name"]] = ListConfig(lst, rec)
self["lists"] = lists
rules = [] rules = []
for rule in self["rules"]: for rule in self["rules"]:
if "loglevel" not in rule: if "loglevel" not in rule:
rule["loglevel"] = config["loglevel"] rule["loglevel"] = config["loglevel"]
if "pretend" not in rule: if "pretend" not in rule:
rule["pretend"] = config["pretend"] rule["pretend"] = config["pretend"]
rules.append(RuleConfig(rule, lists, rec)) rules.append(RuleConfig(rule, self, rec))
self["rules"] = rules self["rules"] = rules
def get_milter_config(cfgfile, raw=False): def get_milter_config(cfgfile, rec=True):
try: try:
with open(cfgfile, "r") as fh: with open(cfgfile, "r") as fh:
# remove lines with leading # (comments), they # remove lines with leading # (comments), they
@@ -387,10 +457,4 @@ def get_milter_config(cfgfile, raw=False):
cfg_text = [f"{n+1}: {l}" for n, l in enumerate(cfg.splitlines())] cfg_text = [f"{n+1}: {l}" for n, l in enumerate(cfg.splitlines())]
msg = "\n".join(cfg_text) msg = "\n".join(cfg_text)
raise RuntimeError(f"{e}\n{msg}") raise RuntimeError(f"{e}\n{msg}")
if raw: return QuarantineMilterConfig(cfg, rec)
lists = {}
for lst in cfg["lists"]:
lists[lst["name"]] = lst
cfg["lists"] = lists
return cfg
return QuarantineMilterConfig(cfg)

View File

@@ -331,18 +331,17 @@ class Notify:
def __init__(self, cfg, local_addrs, debug): def __init__(self, cfg, local_addrs, debug):
self.cfg = cfg self.cfg = cfg
self.logger = logging.getLogger(cfg["name"]) self.logger = logging.getLogger(cfg["name"])
del cfg["name"]
self.logger.setLevel(cfg.get_loglevel(debug)) self.logger.setLevel(cfg.get_loglevel(debug))
del cfg["loglevel"]
nodification_type = cfg["options"]["type"] nodification_type = cfg["type"]
del cfg["options"]["type"] del cfg["type"]
cfg["options"]["pretend"] = cfg["pretend"] self._notification = self.NOTIFICATION_TYPES[nodification_type](**cfg)
self._notification = self.NOTIFICATION_TYPES[nodification_type](
**cfg["options"])
self._headersonly = self._notification._headersonly self._headersonly = self._notification._headersonly
def __str__(self): def __str__(self):
cfg = [] cfg = []
for key, value in self.cfg["options"].items(): for key, value in self.cfg.items():
cfg.append(f"{key}={value}") cfg.append(f"{key}={value}")
class_name = type(self._notification).__name__ class_name = type(self._notification).__name__
return f"{class_name}(" + ", ".join(cfg) + ")" return f"{class_name}(" + ", ".join(cfg) + ")"

View File

@@ -46,8 +46,9 @@ class Rule:
actions = [] actions = []
for action in self.actions: for action in self.actions:
actions.append(str(action)) actions.append(str(action))
cfg.append("actions=[" + ", ".join(actions) + "]") cfg.append("actions=[\n " +
return "Rule(" + ", ".join(cfg) + ")" ",\n ".join(actions) + "\n ]")
return "Rule(\n " + ",\n ".join(cfg) + "\n)"
def execute(self, milter): def execute(self, milter):
"""Execute all actions of this rule.""" """Execute all actions of this rule."""

View File

@@ -31,7 +31,6 @@ from time import gmtime
from pyquarantine import mailer from pyquarantine import mailer
from pyquarantine.base import CustomLogger, MilterMessage from pyquarantine.base import CustomLogger, MilterMessage
from pyquarantine.config import ActionConfig
from pyquarantine.list import DatabaseList from pyquarantine.list import DatabaseList
from pyquarantine.notify import Notify from pyquarantine.notify import Notify
@@ -373,18 +372,17 @@ class Store:
def __init__(self, cfg, local_addrs, debug): def __init__(self, cfg, local_addrs, debug):
self.cfg = cfg self.cfg = cfg
self.logger = logging.getLogger(cfg["name"]) self.logger = logging.getLogger(cfg["name"])
del cfg["name"]
self.logger.setLevel(cfg.get_loglevel(debug)) self.logger.setLevel(cfg.get_loglevel(debug))
del cfg["loglevel"]
storage_type = cfg["options"]["type"] storage_type = cfg["type"]
del cfg["options"]["type"] del cfg["type"]
cfg["options"]["pretend"] = cfg["pretend"] self._storage = self.STORAGE_TYPES[storage_type](**cfg)
self._storage = self.STORAGE_TYPES[storage_type](
**cfg["options"])
self._headersonly = self._storage._headersonly self._headersonly = self._storage._headersonly
def __str__(self): def __str__(self):
cfg = [] cfg = []
for key, value in self.cfg["options"].items(): for key, value in self.cfg.items():
cfg.append(f"{key}={value}") cfg.append(f"{key}={value}")
class_name = type(self._storage).__name__ class_name = type(self._storage).__name__
return f"{class_name}(" + ", ".join(cfg) + ")" return f"{class_name}(" + ", ".join(cfg) + ")"
@@ -407,29 +405,17 @@ class Quarantine:
self.logger = logging.getLogger(cfg["name"]) self.logger = logging.getLogger(cfg["name"])
self.logger.setLevel(cfg.get_loglevel(debug)) self.logger.setLevel(cfg.get_loglevel(debug))
storage_cfg = ActionConfig( cfg["options"]["store"]["loglevel"] = cfg["loglevel"]
{ self._storage = Store(cfg["options"]["store"], local_addrs, debug)
"name": cfg["name"],
"loglevel": cfg["loglevel"],
"pretend": cfg["pretend"],
"type": "store",
"options": cfg["options"]["store"].get_config()},
{})
self._storage = Store(storage_cfg, local_addrs, debug)
self.smtp_host = cfg["options"]["smtp_host"] self.smtp_host = cfg["options"]["smtp_host"]
self.smtp_port = cfg["options"]["smtp_port"] self.smtp_port = cfg["options"]["smtp_port"]
self._notification = None self._notification = None
if "notify" in cfg["options"]: if "notify" in cfg["options"]:
notify_cfg = ActionConfig({ cfg["options"]["notify"]["loglevel"] = cfg["loglevel"]
"name": cfg["name"], self._notification = Notify(
"loglevel": cfg["loglevel"], cfg["options"]["notify"], local_addrs, debug)
"pretend": cfg["pretend"],
"type": "notify",
"options": cfg["options"]["notify"].get_config()},
{})
self._notification = Notify(notify_cfg, local_addrs, debug)
self._allowlist = None self._allowlist = None
if "allowlist" in cfg["options"]: if "allowlist" in cfg["options"]: