718 lines
27 KiB
Python
718 lines
27 KiB
Python
# PyQuarantine-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.
|
|
#
|
|
# PyQuarantine-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 PyQuarantineMilter. If not, see <http://www.gnu.org/licenses/>.
|
|
#
|
|
|
|
import Milter
|
|
import configparser
|
|
import logging
|
|
import os
|
|
import re
|
|
import encodings
|
|
|
|
from Milter.utils import parse_addr
|
|
from collections import defaultdict
|
|
from email.charset import Charset
|
|
from email.header import Header, decode_header
|
|
from io import BytesIO
|
|
from itertools import groupby
|
|
from netaddr import IPAddress, IPNetwork, AddrFormatError
|
|
from pyquarantine import mailer
|
|
from pyquarantine import notifications
|
|
from pyquarantine import storages
|
|
from pyquarantine import whitelists
|
|
|
|
__all__ = [
|
|
"make_header",
|
|
"Quarantine",
|
|
"QuarantineMilter",
|
|
"setup_milter",
|
|
"reload_config",
|
|
"cli",
|
|
"mailer",
|
|
"notifications",
|
|
"storages",
|
|
"run",
|
|
"whitelists"]
|
|
|
|
__version__ = "1.0.7"
|
|
|
|
|
|
################################################
|
|
# add charset alias for windows-874 encoding #
|
|
################################################
|
|
|
|
aliases = encodings.aliases.aliases
|
|
|
|
for alias in ["windows-874", "windows_874"]:
|
|
if alias not in aliases:
|
|
aliases[alias] = "cp874"
|
|
|
|
setattr(encodings.aliases, "aliases", aliases)
|
|
|
|
################################################
|
|
|
|
|
|
def make_header(decoded_seq, maxlinelen=None, header_name=None,
|
|
continuation_ws=' ', errors='strict'):
|
|
"""Create a Header from a sequence of pairs as returned by decode_header()
|
|
|
|
decode_header() takes a header value string and returns a sequence of
|
|
pairs of the format (decoded_string, charset) where charset is the string
|
|
name of the character set.
|
|
|
|
This function takes one of those sequence of pairs and returns a Header
|
|
instance. Optional maxlinelen, header_name, and continuation_ws are as in
|
|
the Header constructor.
|
|
"""
|
|
h = Header(maxlinelen=maxlinelen, header_name=header_name,
|
|
continuation_ws=continuation_ws)
|
|
for s, charset in decoded_seq:
|
|
# None means us-ascii but we can simply pass it on to h.append()
|
|
if charset is not None and not isinstance(charset, Charset):
|
|
charset = Charset(charset)
|
|
h.append(s, charset, errors=errors)
|
|
return h
|
|
|
|
|
|
class Quarantine(object):
|
|
"""Quarantine class suitable for QuarantineMilter
|
|
|
|
The class holds all the objects and functions needed
|
|
for QuarantineMilter quarantine.
|
|
|
|
"""
|
|
|
|
# list of possible actions
|
|
_actions = {
|
|
"ACCEPT": Milter.ACCEPT,
|
|
"REJECT": Milter.REJECT,
|
|
"DISCARD": Milter.DISCARD}
|
|
|
|
def __init__(self, name, index=0, regex=None, storage=None, whitelist=None,
|
|
host_whitelist=[], notification=None, action="ACCEPT",
|
|
reject_reason=None):
|
|
self.logger = logging.getLogger(__name__)
|
|
self.name = name
|
|
self.index = index
|
|
self.regex = regex
|
|
if regex:
|
|
self.re = re.compile(
|
|
regex, re.MULTILINE + re.DOTALL + re.IGNORECASE)
|
|
self.storage = storage
|
|
self.whitelist = whitelist
|
|
self.host_whitelist = host_whitelist
|
|
self.notification = notification
|
|
action = action.upper()
|
|
assert action in self._actions
|
|
self.action = action
|
|
self.milter_action = self._actions[action]
|
|
self.reject_reason = reject_reason
|
|
|
|
def setup_from_cfg(self, global_cfg, cfg, test=False):
|
|
defaults = {
|
|
"action": "accept",
|
|
"reject_reason": "Message rejected",
|
|
"storage_type": "none",
|
|
"notification_type": "none",
|
|
"whitelist_type": "none",
|
|
"host_whitelist": ""
|
|
}
|
|
# check config
|
|
for opt in ["regex", "smtp_host", "smtp_port"] + list(defaults.keys()):
|
|
if opt in cfg:
|
|
continue
|
|
if opt in global_cfg:
|
|
cfg[opt] = global_cfg[opt]
|
|
elif opt in defaults:
|
|
cfg[opt] = defaults[opt]
|
|
else:
|
|
raise RuntimeError(
|
|
f"mandatory option '{opt}' not present in "
|
|
f"config section '{self.name}' or 'global'")
|
|
|
|
# pre-compile regex
|
|
self.logger.debug(
|
|
f"{self.name}: compiling regex '{cfg['regex']}'")
|
|
self.re = re.compile(
|
|
cfg["regex"], re.MULTILINE + re.DOTALL + re.IGNORECASE)
|
|
|
|
self.smtp_host = cfg["smtp_host"]
|
|
self.smtp_port = cfg["smtp_port"]
|
|
|
|
# create storage instance
|
|
storage_type = cfg["storage_type"].lower()
|
|
if storage_type in storages.TYPES:
|
|
self.logger.debug(
|
|
f"{self.name}: initializing storage "
|
|
f"type '{storage_type.upper()}'")
|
|
self.storage = storages.TYPES[storage_type](
|
|
self.name, global_cfg, cfg, test)
|
|
elif storage_type == "none":
|
|
self.logger.debug(f"{self.name}: storage is NONE")
|
|
self.storage = None
|
|
else:
|
|
raise RuntimeError(
|
|
f"{self.name}: unknown storage type '{storage_type}'")
|
|
|
|
# create whitelist instance
|
|
whitelist_type = cfg["whitelist_type"].lower()
|
|
if whitelist_type in whitelists.TYPES:
|
|
self.logger.debug(
|
|
f"{self.name}: initializing whitelist "
|
|
f"type '{whitelist_type.upper()}'")
|
|
self.whitelist = whitelists.TYPES[whitelist_type](
|
|
self.name, global_cfg, cfg, test)
|
|
elif whitelist_type == "none":
|
|
self.logger.debug(f"{self.name}: whitelist is NONE")
|
|
self.whitelist = None
|
|
else:
|
|
raise RuntimeError(
|
|
f"{self.name}: unknown whitelist type '{whitelist_type}'")
|
|
|
|
# create notification instance
|
|
notification_type = cfg["notification_type"].lower()
|
|
if notification_type in notifications.TYPES:
|
|
self.logger.debug(
|
|
f"{self.name}: initializing notification "
|
|
f"type '{notification_type.upper()}'")
|
|
self.notification = notifications.TYPES[notification_type](
|
|
self.name, global_cfg, cfg, test)
|
|
elif notification_type == "none":
|
|
self.logger.debug(f"{self.name}: notification is NONE")
|
|
self.notification = None
|
|
else:
|
|
raise RuntimeError(
|
|
f"{self.name}: unknown notification "
|
|
f"type '{notification_type}'")
|
|
|
|
# determining milter action for this quarantine
|
|
action = cfg["action"].upper()
|
|
if action in self._actions:
|
|
self.logger.debug(f"{self.name}: action is {action}")
|
|
self.action = action
|
|
self.milter_action = self._actions[action]
|
|
else:
|
|
raise RuntimeError(
|
|
f"{self.name}: unknown action '{action}'")
|
|
|
|
self.reject_reason = cfg["reject_reason"]
|
|
|
|
# create host/network whitelist
|
|
self.host_whitelist = []
|
|
host_whitelist = set([p.strip()
|
|
for p in cfg["host_whitelist"].split(",") if p])
|
|
for host in host_whitelist:
|
|
if not host:
|
|
continue
|
|
# parse network notation
|
|
try:
|
|
net = IPNetwork(host)
|
|
except AddrFormatError as e:
|
|
raise RuntimeError(
|
|
f"{self.name}: error parsing host_whitelist: {e}")
|
|
else:
|
|
self.host_whitelist.append(net)
|
|
if self.host_whitelist:
|
|
whitelist = ", ".join([str(ip) for ip in host_whitelist])
|
|
self.logger.debug(
|
|
f"{self.name}: host whitelist: {whitelist}")
|
|
|
|
def notify(self, storage_id, recipient=None, synchronous=True):
|
|
"Notify recipient about email in storage."
|
|
if not self.storage:
|
|
raise RuntimeError(
|
|
"storage type is set to None, unable to send notification")
|
|
|
|
if not self.notification:
|
|
raise RuntimeError(
|
|
"notification type is set to None, "
|
|
"unable to send notification")
|
|
|
|
fp, metadata = self.storage.get_mail(storage_id)
|
|
|
|
if recipient is not None:
|
|
if recipient not in metadata["recipients"]:
|
|
raise RuntimeError(f"invalid recipient '{recipient}'")
|
|
recipients = [recipient]
|
|
else:
|
|
recipients = metadata["recipients"]
|
|
|
|
self.notification.notify(
|
|
metadata["queue_id"], storage_id, metadata["mailfrom"],
|
|
recipients, metadata["headers"], fp,
|
|
metadata["subgroups"], metadata["named_subgroups"],
|
|
synchronous)
|
|
fp.close()
|
|
|
|
def release(self, storage_id, recipients=None):
|
|
"Release email from storage."
|
|
if not self.storage:
|
|
raise RuntimeError(
|
|
"storage type is set to None, unable to release email")
|
|
|
|
fp, metadata = self.storage.get_mail(storage_id)
|
|
try:
|
|
mail = fp.read()
|
|
fp.close()
|
|
except IOError as e:
|
|
raise RuntimeError(f"unable to read data file: {e}")
|
|
|
|
if recipients and type(recipients) == str:
|
|
recipients = [recipients]
|
|
else:
|
|
recipients = metadata["recipients"]
|
|
|
|
for recipient in recipients:
|
|
if recipient not in metadata["recipients"]:
|
|
raise RuntimeError(f"invalid recipient '{recipient}'")
|
|
|
|
try:
|
|
mailer.smtp_send(
|
|
self.smtp_host,
|
|
self.smtp_port,
|
|
metadata["mailfrom"],
|
|
recipient,
|
|
mail)
|
|
except Exception as e:
|
|
raise RuntimeError(
|
|
f"error while sending email to '{recipient}': {e}")
|
|
self.storage.delete(storage_id, recipient)
|
|
|
|
def get_storage(self):
|
|
return self.storage
|
|
|
|
def get_notification(self):
|
|
return self.notification
|
|
|
|
def get_whitelist(self):
|
|
return self.whitelist
|
|
|
|
def host_in_whitelist(self, hostaddr):
|
|
ip = IPAddress(hostaddr[0])
|
|
for entry in self.host_whitelist:
|
|
if ip in entry:
|
|
return True
|
|
return False
|
|
|
|
def match(self, header):
|
|
return self.re.search(header)
|
|
|
|
|
|
class QuarantineMilter(Milter.Base):
|
|
"""QuarantineMilter based on Milter.Base to implement milter communication
|
|
|
|
The class variable quarantines needs to be filled by
|
|
runng the setup_milter function.
|
|
|
|
"""
|
|
quarantines = []
|
|
preferred_action = "first"
|
|
|
|
# list of default config files
|
|
_cfg_files = [
|
|
"/etc/pyquarantine/pyquarantine.conf",
|
|
os.path.expanduser('~/pyquarantine.conf'),
|
|
"pyquarantine.conf"]
|
|
|
|
def __init__(self):
|
|
self.logger = logging.getLogger(__name__)
|
|
# save runtime config, it must not change during runtime
|
|
self.quarantines = QuarantineMilter.quarantines
|
|
|
|
def _get_preferred_quarantine(self):
|
|
matching_quarantines = [
|
|
q for q in self.rcpts_quarantines.values() if q]
|
|
if self.preferred_action == "first":
|
|
quarantine = sorted(
|
|
matching_quarantines,
|
|
key=lambda q: q.index)[0]
|
|
else:
|
|
quarantine = sorted(
|
|
matching_quarantines,
|
|
key=lambda q: q.index,
|
|
reverse=True)[0]
|
|
return quarantine
|
|
|
|
@staticmethod
|
|
def get_cfg_files():
|
|
return QuarantineMilter._cfg_files
|
|
|
|
@staticmethod
|
|
def set_cfg_files(cfg_files):
|
|
QuarantineMilter._cfg_files = cfg_files
|
|
|
|
def connect(self, hostname, family, hostaddr):
|
|
self.hostaddr = hostaddr
|
|
self.logger.debug(
|
|
f"accepted milter connection from {hostaddr[0]} "
|
|
f"port {hostaddr[1]}")
|
|
for quarantine in self.quarantines.copy():
|
|
if quarantine.host_in_whitelist(hostaddr):
|
|
self.logger.debug(
|
|
f"host {hostaddr[0]} is in whitelist of "
|
|
f"quarantine {quarantine.name}")
|
|
self.quarantines.remove(quarantine)
|
|
if not self.quarantines:
|
|
self.logger.debug(
|
|
f"host {hostaddr[0]} is in whitelist of all "
|
|
f"quarantines, skip further processing")
|
|
return Milter.ACCEPT
|
|
return Milter.CONTINUE
|
|
|
|
@Milter.noreply
|
|
def envfrom(self, mailfrom, *str):
|
|
self.mailfrom = "@".join(parse_addr(mailfrom)).lower()
|
|
self.recipients = set()
|
|
return Milter.CONTINUE
|
|
|
|
@Milter.noreply
|
|
def envrcpt(self, to, *str):
|
|
self.recipients.add("@".join(parse_addr(to)).lower())
|
|
return Milter.CONTINUE
|
|
|
|
@Milter.noreply
|
|
def data(self):
|
|
self.qid = self.getsymval('i')
|
|
self.logger.debug(
|
|
f"{self.qid}: received queue-id from MTA")
|
|
self.recipients = list(self.recipients)
|
|
self.logger.debug(
|
|
f"{self.qid}: initializing memory buffer to save email data")
|
|
# initialize memory buffer to save email data
|
|
self.fp = BytesIO()
|
|
self.headers = []
|
|
return Milter.CONTINUE
|
|
|
|
def header(self, name, value):
|
|
try:
|
|
# remove surrogates from value
|
|
value = value.encode(
|
|
errors="surrogateescape").decode(errors="replace")
|
|
self.logger.debug(f"{self.qid}: received header: {name}: {value}")
|
|
# write email header to memory buffer
|
|
self.fp.write(f"{name}: {value}\r\n".encode(
|
|
encoding="ascii", errors="replace"))
|
|
header = make_header(decode_header(value), errors="replace")
|
|
value = str(header).replace("\x00", "")
|
|
self.logger.debug(
|
|
f"{self.qid}: decoded header: {name}: {value}")
|
|
self.headers.append((name, value))
|
|
return Milter.CONTINUE
|
|
except Exception as e:
|
|
self.logger.exception(
|
|
f"an exception occured in header function: {e}")
|
|
return Milter.TEMPFAIL
|
|
|
|
def eoh(self):
|
|
try:
|
|
self.fp.write("\r\n".encode(encoding="ascii"))
|
|
self.wl_cache = whitelists.WhitelistCache()
|
|
|
|
# initialize dicts to set quaranines per recipient and keep matches
|
|
self.rcpts_quarantines = {}
|
|
self.matches = {}
|
|
|
|
# iterate email headers
|
|
rcpts_to_check = self.recipients.copy()
|
|
for name, value in self.headers:
|
|
header = f"{name}: {value}"
|
|
self.logger.debug(
|
|
f"{self.qid}: checking header against configured "
|
|
f"quarantines: {header}")
|
|
# iterate quarantines
|
|
for quarantine in self.quarantines:
|
|
if len(self.rcpts_quarantines) == len(
|
|
self.recipients):
|
|
# every recipient matched a quarantine already
|
|
if quarantine.index >= max(
|
|
[q.index for q in
|
|
self.rcpts_quarantines.values()]):
|
|
# all recipients matched a quarantine with at least
|
|
# the same precedence already, skip checks against
|
|
# quarantines with lower precedence
|
|
self.logger.debug(
|
|
f"{self.qid}: {quarantine.name}: skip further "
|
|
f"checks of this header")
|
|
break
|
|
|
|
# check email header against quarantine regex
|
|
self.logger.debug(
|
|
f"{self.qid}: {quarantine.name}: checking header "
|
|
f"against regex '{str(quarantine.regex)}'")
|
|
match = quarantine.match(header)
|
|
if match:
|
|
self.logger.debug(
|
|
f"{self.qid}: {quarantine.name}: "
|
|
f"header matched regex")
|
|
# check for whitelisted recipients
|
|
whitelist = quarantine.get_whitelist()
|
|
if whitelist:
|
|
try:
|
|
wl_recipients = self.wl_cache.get_recipients(
|
|
whitelist,
|
|
self.mailfrom,
|
|
rcpts_to_check)
|
|
except RuntimeError as e:
|
|
self.logger.error(
|
|
f"{self.qid}: {quarantine.name}: unable "
|
|
f"to query whitelist: {e}")
|
|
return Milter.TEMPFAIL
|
|
else:
|
|
wl_recipients = {}
|
|
|
|
# iterate recipients
|
|
for rcpt in rcpts_to_check.copy():
|
|
if rcpt in wl_recipients:
|
|
# recipient is whitelisted in this quarantine
|
|
self.logger.debug(
|
|
f"{self.qid}: {quarantine.name}: "
|
|
f"recipient '{rcpt}' is whitelisted")
|
|
continue
|
|
|
|
if rcpt not in self.rcpts_quarantines or \
|
|
self.rcpts_quarantines[rcpt].index > \
|
|
quarantine.index:
|
|
self.logger.debug(
|
|
f"{self.qid}: {quarantine.name}: set "
|
|
f"quarantine for recipient '{rcpt}'")
|
|
# save match for later use as template
|
|
# variables
|
|
self.matches[quarantine.name] = match
|
|
self.rcpts_quarantines[rcpt] = quarantine
|
|
if quarantine.index == 0:
|
|
# we do not need to check recipients which
|
|
# matched the quarantine with the highest
|
|
# precedence already
|
|
rcpts_to_check.remove(rcpt)
|
|
else:
|
|
self.logger.debug(
|
|
f"{self.qid}: {quarantine.name}: a "
|
|
f"quarantine with same or higher "
|
|
f"precedence matched already for "
|
|
f"recipient '{rcpt}'")
|
|
|
|
if not rcpts_to_check:
|
|
self.logger.debug(
|
|
f"{self.qid}: all recipients matched the first "
|
|
f"quarantine, skipping all remaining header checks")
|
|
break
|
|
|
|
# check if no quarantine has matched for all recipients
|
|
if not self.rcpts_quarantines:
|
|
# accept email
|
|
self.logger.info(
|
|
f"{self.qid}: passed clean for all recipients")
|
|
return Milter.ACCEPT
|
|
|
|
# check if the mail body is needed
|
|
for recipient, quarantine in self.rcpts_quarantines.items():
|
|
if quarantine.get_storage() or quarantine.get_notification():
|
|
# mail body is needed, continue processing
|
|
return Milter.CONTINUE
|
|
|
|
# quarantine and notification are disabled on all matching
|
|
# quarantines, just return configured action
|
|
quarantine = self._get_preferred_quarantine()
|
|
self.logger.info(
|
|
f"{self.qid}: {self.preferred_action} "
|
|
f"matching quarantine is '{quarantine.name}', performing "
|
|
f"milter action {quarantine.action}")
|
|
if quarantine.action == "REJECT":
|
|
self.setreply("554", "5.7.0", quarantine.reject_reason)
|
|
return quarantine.milter_action
|
|
|
|
except Exception as e:
|
|
self.logger.exception(
|
|
f"an exception occured in eoh function: {e}")
|
|
return Milter.TEMPFAIL
|
|
|
|
def body(self, chunk):
|
|
try:
|
|
# save received body chunk
|
|
self.fp.write(chunk)
|
|
except Exception as e:
|
|
self.logger.exception(
|
|
f"an exception occured in body function: {e}")
|
|
return Milter.TEMPFAIL
|
|
return Milter.CONTINUE
|
|
|
|
def eom(self):
|
|
try:
|
|
# processing recipients grouped by quarantines
|
|
quarantines = []
|
|
for quarantine, recipients in groupby(
|
|
sorted(self.rcpts_quarantines,
|
|
key=lambda x: self.rcpts_quarantines[x].index),
|
|
lambda x: self.rcpts_quarantines[x]):
|
|
quarantines.append((quarantine, list(recipients)))
|
|
|
|
# iterate quarantines sorted by index
|
|
for quarantine, recipients in sorted(
|
|
quarantines, key=lambda x: x[0].index):
|
|
headers = defaultdict(str)
|
|
for name, value in self.headers:
|
|
headers[name.lower()] = value
|
|
subgroups = self.matches[quarantine.name].groups(
|
|
default="")
|
|
named_subgroups = self.matches[quarantine.name].groupdict(
|
|
default="")
|
|
|
|
rcpts = ", ".join(recipients)
|
|
|
|
# check if a storage is configured
|
|
storage_id = ""
|
|
storage = quarantine.get_storage()
|
|
if storage:
|
|
# add email to quarantine
|
|
self.logger.info(
|
|
f"{self.qid}: adding to quarantine "
|
|
f"'{quarantine.name}' for: {rcpts}")
|
|
try:
|
|
storage_id = storage.add(
|
|
self.qid, self.mailfrom, recipients, headers,
|
|
self.fp, subgroups, named_subgroups)
|
|
except RuntimeError as e:
|
|
self.logger.error(
|
|
f"{self.qid}: unable to add to quarantine "
|
|
f"'{quarantine.name}': {e}")
|
|
return Milter.TEMPFAIL
|
|
|
|
# check if a notification is configured
|
|
notification = quarantine.get_notification()
|
|
if notification:
|
|
# notify
|
|
self.logger.info(
|
|
f"{self.qid}: sending notification to: {rcpts}")
|
|
try:
|
|
notification.notify(
|
|
self.qid, storage_id,
|
|
self.mailfrom, recipients, headers, self.fp,
|
|
subgroups, named_subgroups)
|
|
except RuntimeError as e:
|
|
self.logger.error(
|
|
f"{self.qid}: unable to send notification: {e}")
|
|
return Milter.TEMPFAIL
|
|
|
|
# remove processed recipient
|
|
for recipient in recipients:
|
|
self.delrcpt(recipient)
|
|
self.recipients.remove(recipient)
|
|
|
|
self.fp.close()
|
|
|
|
# email passed clean for at least one recipient, accepting email
|
|
if self.recipients:
|
|
rcpts = ", ".join(recipients)
|
|
self.logger.info(
|
|
f"{self.qid}: passed clean for: {rcpts}")
|
|
return Milter.ACCEPT
|
|
|
|
# return configured action
|
|
quarantine = self._get_preferred_quarantine()
|
|
self.logger.info(
|
|
f"{self.qid}: {self.preferred_action} matching "
|
|
f"quarantine is '{quarantine.name}', performing milter "
|
|
f"action {quarantine.action}")
|
|
if quarantine.action == "REJECT":
|
|
self.setreply("554", "5.7.0", quarantine.reject_reason)
|
|
return quarantine.milter_action
|
|
|
|
except Exception as e:
|
|
self.logger.exception(
|
|
f"an exception occured in eom function: {e}")
|
|
return Milter.TEMPFAIL
|
|
|
|
def close(self):
|
|
self.logger.debug(
|
|
f"disconnect from {self.hostaddr[0]} port {self.hostaddr[1]}")
|
|
return Milter.CONTINUE
|
|
|
|
|
|
def setup_milter(test=False, cfg_files=[]):
|
|
"Generate the configuration for QuarantineMilter class."
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# read config file
|
|
parser = configparser.ConfigParser()
|
|
if not cfg_files:
|
|
cfg_files = parser.read(QuarantineMilter.get_cfg_files())
|
|
else:
|
|
cfg_files = parser.read(cfg_files)
|
|
if not cfg_files:
|
|
raise RuntimeError("config file not found")
|
|
|
|
QuarantineMilter.set_cfg_files(cfg_files)
|
|
os.chdir(os.path.dirname(cfg_files[0]))
|
|
|
|
# check if mandatory config options in global section are present
|
|
if "global" not in parser.sections():
|
|
raise RuntimeError(
|
|
"mandatory section 'global' not present in config file")
|
|
for option in ["quarantines", "preferred_quarantine_action"]:
|
|
if not parser.has_option("global", option):
|
|
raise RuntimeError(
|
|
f"mandatory option '{option}' not present in config "
|
|
f"section 'global'")
|
|
|
|
# read global config section
|
|
global_cfg = dict(parser.items("global"))
|
|
preferred_action = global_cfg["preferred_quarantine_action"].lower()
|
|
if preferred_action not in ["first", "last"]:
|
|
raise RuntimeError(
|
|
"option preferred_quarantine_action has illegal value")
|
|
|
|
# read active quarantine names
|
|
quarantines = [
|
|
q.strip() for q in global_cfg["quarantines"].split(",")]
|
|
if len(quarantines) != len(set(quarantines)):
|
|
raise RuntimeError(
|
|
"at least one quarantine is specified multiple times "
|
|
"in quarantines option")
|
|
if "global" in quarantines:
|
|
quarantines.remove("global")
|
|
logger.warning(
|
|
"removed illegal quarantine name 'global' from list of "
|
|
"active quarantines")
|
|
if not quarantines:
|
|
raise RuntimeError("no quarantines configured")
|
|
|
|
milter_quarantines = []
|
|
logger.debug("preparing milter configuration ...")
|
|
# iterate quarantine names
|
|
for index, name in enumerate(quarantines):
|
|
# check if config section for current quarantine exists
|
|
if name not in parser.sections():
|
|
raise RuntimeError(
|
|
f"config section '{name}' does not exist")
|
|
|
|
cfg = dict(parser.items(name))
|
|
quarantine = Quarantine(name, index)
|
|
quarantine.setup_from_cfg(global_cfg, cfg, test)
|
|
milter_quarantines.append(quarantine)
|
|
|
|
QuarantineMilter.preferred_action = preferred_action
|
|
QuarantineMilter.quarantines = milter_quarantines
|
|
|
|
|
|
def reload_config():
|
|
"Reload the configuration of QuarantineMilter class."
|
|
logger = logging.getLogger(__name__)
|
|
|
|
try:
|
|
setup_milter()
|
|
except RuntimeError as e:
|
|
logger.info(e)
|
|
logger.info("daemon is still running with previous configuration")
|
|
else:
|
|
logger.info("reloaded configuration")
|