diff --git a/pyquarantine/__init__.py b/pyquarantine/__init__.py
index e586b92..4a7ca72 100644
--- a/pyquarantine/__init__.py
+++ b/pyquarantine/__init__.py
@@ -2,17 +2,26 @@
# 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
+# 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 .
#
-__all__ = ["QuarantineMilter", "generate_milter_config", "reload_config", "mailer", "notifications", "run", "quarantines", "whitelists"]
+__all__ = [
+ "QuarantineMilter",
+ "generate_milter_config",
+ "reload_config",
+ "mailer",
+ "notifications",
+ "run",
+ "quarantines",
+ "whitelists"]
+
name = "pyquarantine"
import Milter
@@ -42,9 +51,15 @@ class QuarantineMilter(Milter.Base):
global_config = None
# list of default config files
- _config_files = ["/etc/pyquarantine/pyquarantine.conf", os.path.expanduser('~/pyquarantine.conf'), "pyquarantine.conf"]
+ _config_files = [
+ "/etc/pyquarantine/pyquarantine.conf",
+ os.path.expanduser('~/pyquarantine.conf'),
+ "pyquarantine.conf"]
# list of possible actions
- _actions = {"ACCEPT": Milter.ACCEPT, "REJECT": Milter.REJECT, "DISCARD": Milter.DISCARD}
+ _actions = {
+ "ACCEPT": Milter.ACCEPT,
+ "REJECT": Milter.REJECT,
+ "DISCARD": Milter.DISCARD}
def __init__(self):
self.logger = logging.getLogger(__name__)
@@ -53,11 +68,17 @@ class QuarantineMilter(Milter.Base):
self.config = QuarantineMilter.config
def _get_preferred_quarantine(self):
- matching_quarantines = [q for q in self.recipients_quarantines.values() if q]
+ matching_quarantines = [
+ q for q in self.recipients_quarantines.values() if q]
if self.global_config["preferred_quarantine_action"] == "first":
- quarantine = sorted(matching_quarantines, key=lambda x: x["index"])[0]
+ quarantine = sorted(
+ matching_quarantines,
+ key=lambda x: x["index"])[0]
else:
- quarantine = sorted(matching_quarantines, key=lambda x: x["index"], reverse=True)[0]
+ quarantine = sorted(
+ matching_quarantines,
+ key=lambda x: x["index"],
+ reverse=True)[0]
return quarantine
@staticmethod
@@ -73,18 +94,25 @@ class QuarantineMilter(Milter.Base):
QuarantineMilter._config_files = config_files
def connect(self, IPname, family, hostaddr):
- self.logger.debug("accepted milter connection from {} port {}".format(*hostaddr))
+ self.logger.debug(
+ "accepted milter connection from {} port {}".format(
+ *hostaddr))
ip = IPAddress(hostaddr[0])
for quarantine in self.config.copy():
for ignore in quarantine["ignore_hosts_list"]:
if ip in ignore:
- self.logger.debug("host {} is ignored by quarantine {}".format(hostaddr[0], quarantine["name"]))
+ self.logger.debug(
+ "host {} is ignored by quarantine {}".format(
+ hostaddr[0], quarantine["name"]))
self.config.remove(quarantine)
break
if not self.config:
- self.logger.debug("host {} is ignored by all quarantines, skip further processing", hostaddr[0])
+ self.logger.debug(
+ "host {} is ignored by all quarantines, "
+ "skip further processing",
+ hostaddr[0])
return Milter.ACCEPT
- return Milter.CONTINUE
+ return Milter.CONTINUE
@Milter.noreply
def envfrom(self, mailfrom, *str):
@@ -100,7 +128,8 @@ class QuarantineMilter(Milter.Base):
@Milter.noreply
def data(self):
self.queueid = self.getsymval('i')
- self.logger.debug("{}: received queue-id from MTA".format(self.queueid))
+ self.logger.debug(
+ "{}: received queue-id from MTA".format(self.queueid))
self.recipients = list(self.recipients)
self.headers = []
return Milter.CONTINUE
@@ -122,28 +151,43 @@ class QuarantineMilter(Milter.Base):
recipients_to_check = self.recipients.copy()
for name, value in self.headers:
header = "{}: {}".format(name, value)
- self.logger.debug("{}: checking header against configured quarantines: {}".format(self.queueid, header))
+ self.logger.debug(
+ "{}: checking header against configured quarantines: {}".format(
+ self.queueid, header))
# iterate quarantines
for quarantine in self.config:
- if len(self.recipients_quarantines) == len(self.recipients):
+ if len(self.recipients_quarantines) == len(
+ self.recipients):
# every recipient matched a quarantine already
- if quarantine["index"] >= max([q["index"] for q in self.recipients_quarantines.values()]):
- # all recipients matched a quarantine with at least the same precedence already, skip checks against quarantines with lower precedence
- self.logger.debug("{}: {}: skip further checks of this header".format(self.queueid, quarantine["name"]))
- break
+ if quarantine["index"] >= max(
+ [q["index"] for q in self.recipients_quarantines.values()]):
+ # all recipients matched a quarantine with at least
+ # the same precedence already, skip checks against
+ # quarantines with lower precedence
+ self.logger.debug(
+ "{}: {}: skip further checks of this header".format(
+ self.queueid, quarantine["name"]))
+ break
# check email header against quarantine regex
- self.logger.debug("{}: {}: checking header against regex '{}'".format(self.queueid, quarantine["name"], quarantine["regex"]))
+ self.logger.debug(
+ "{}: {}: checking header against regex '{}'".format(
+ self.queueid, quarantine["name"], quarantine["regex"]))
match = quarantine["regex_compiled"].search(header)
if match:
- self.logger.debug("{}: {}: header matched regex".format(self.queueid, quarantine["name"]))
+ self.logger.debug(
+ "{}: {}: header matched regex".format(
+ self.queueid, quarantine["name"]))
# check for whitelisted recipients
whitelist = quarantine["whitelist_obj"]
- if whitelist != None:
+ if whitelist is not None:
try:
- whitelisted_recipients = self.whitelist_cache.get_whitelisted_recipients(whitelist, self.mailfrom, recipients_to_check)
+ whitelisted_recipients = self.whitelist_cache.get_whitelisted_recipients(
+ whitelist, self.mailfrom, recipients_to_check)
except RuntimeError as e:
- self.logger.error("{}: {}: unable to query whitelist: {}".format(self.queueid, quarantine["name"], e))
+ self.logger.error(
+ "{}: {}: unable to query whitelist: {}".format(
+ self.queueid, quarantine["name"], e))
return Milter.TEMPFAIL
else:
whitelisted_recipients = {}
@@ -152,28 +196,44 @@ class QuarantineMilter(Milter.Base):
for recipient in recipients_to_check.copy():
if recipient in whitelisted_recipients:
# recipient is whitelisted in this quarantine
- self.logger.debug("{}: {}: recipient '{}' is whitelisted".format(self.queueid, quarantine["name"], recipient))
+ self.logger.debug(
+ "{}: {}: recipient '{}' is whitelisted".format(
+ self.queueid, quarantine["name"], recipient))
continue
- if recipient not in self.recipients_quarantines.keys() or self.recipients_quarantines[recipient]["index"] > quarantine["index"]:
- self.logger.debug("{}: {}: set quarantine for recipient '{}'".format(self.queueid, quarantine["name"], recipient))
- # save match for later use as template variables
+ if recipient not in self.recipients_quarantines.keys() or \
+ self.recipients_quarantines[recipient]["index"] > quarantine["index"]:
+ self.logger.debug(
+ "{}: {}: set quarantine for recipient '{}'".format(
+ self.queueid, quarantine["name"], recipient))
+ # save match for later use as template
+ # variables
self.quarantines_matches[quarantine["name"]] = match
self.recipients_quarantines[recipient] = quarantine
if quarantine["index"] == 0:
- # we do not need to check recipients which matched the quarantine with the highest precedence already
+ # we do not need to check recipients which
+ # matched the quarantine with the highest
+ # precedence already
recipients_to_check.remove(recipient)
else:
- self.logger.debug("{}: {}: a quarantine with the same or higher precedence matched already for recipient '{}'".format(self.queueid, quarantine["name"], recipient))
+ self.logger.debug(
+ "{}: {}: a quarantine with same or higher precedence "
+ "matched already for recipient '{}'".format(
+ self.queueid, quarantine["name"], recipient))
if not recipients_to_check:
- self.logger.debug("{}: all recipients matched the first quarantine, skipping all remaining header checks".format(self.queueid))
+ self.logger.debug(
+ "{}: all recipients matched the first quarantine, "
+ "skipping all remaining header checks".format(
+ self.queueid))
break
# check if no quarantine has matched for all recipients
if not self.recipients_quarantines:
# accept email
- self.logger.info("{}: passed clean for all recipients".format(self.queueid))
+ self.logger.info(
+ "{}: passed clean for all recipients".format(
+ self.queueid))
return Milter.ACCEPT
# check if the email body is needed
@@ -184,7 +244,9 @@ class QuarantineMilter(Milter.Base):
break
if keep_body:
- self.logger.debug("{}: initializing memory buffer to save email data".format(self.queueid))
+ self.logger.debug(
+ "{}: initializing memory buffer to save email data".format(
+ self.queueid))
# initialize memory buffer to save email data
self.fp = BytesIO()
# write email headers to memory buffer
@@ -192,10 +254,15 @@ class QuarantineMilter(Milter.Base):
self.fp.write("{}: {}\n".format(name, value).encode())
self.fp.write("\n".encode())
else:
- # quarantine and notification are disabled on all matching quarantines, return configured action
+ # quarantine and notification are disabled on all matching
+ # quarantines, return configured action
quarantine = self._get_preferred_quarantine()
- self.logger.info("{}: {} matching quarantine is '{}', performing milter action {}".format(self.queueid, self.global_config["preferred_quarantine_action"],
- quarantine["name"], quarantine["action"].upper()))
+ self.logger.info(
+ "{}: {} matching quarantine is '{}', performing milter action {}".format(
+ self.queueid,
+ self.global_config["preferred_quarantine_action"],
+ quarantine["name"],
+ quarantine["action"].upper()))
if quarantine["action"] == "reject":
self.setreply("554", "5.7.0", quarantine["reject_reason"])
return quarantine["milter_action"]
@@ -203,7 +270,8 @@ class QuarantineMilter(Milter.Base):
return Milter.CONTINUE
except Exception as e:
- self.logger.exception("an exception occured in eoh function: {}".format(e))
+ self.logger.exception(
+ "an exception occured in eoh function: {}".format(e))
return Milter.TEMPFAIL
def body(self, chunk):
@@ -211,7 +279,8 @@ class QuarantineMilter(Milter.Base):
# save received body chunk
self.fp.write(chunk)
except Exception as e:
- self.logger.exception("an exception occured in body function: {}".format(e))
+ self.logger.exception(
+ "an exception occured in body function: {}".format(e))
return Milter.TEMPFAIL
return Milter.CONTINUE
@@ -220,39 +289,53 @@ class QuarantineMilter(Milter.Base):
# processing recipients grouped by quarantines
quarantines = []
for quarantine, recipients in groupby(
- sorted(self.recipients_quarantines, key=lambda x: self.recipients_quarantines[x]["index"])
- , lambda x: self.recipients_quarantines[x]):
+ sorted(self.recipients_quarantines,
+ key=lambda x: self.recipients_quarantines[x]["index"]),
+ lambda x: self.recipients_quarantines[x]):
quarantines.append((quarantine, list(recipients)))
# iterate quarantines sorted by index
- for quarantine, recipients in sorted(quarantines, key=lambda x: x[0]["index"]):
+ for quarantine, recipients in sorted(
+ quarantines, key=lambda x: x[0]["index"]):
quarantine_id = ""
headers = defaultdict(str)
for name, value in self.headers:
headers[name.lower()] = value
- subgroups = self.quarantines_matches[quarantine["name"]].groups(default="")
- named_subgroups = self.quarantines_matches[quarantine["name"]].groupdict(default="")
+ subgroups = self.quarantines_matches[quarantine["name"]].groups(
+ default="")
+ named_subgroups = self.quarantines_matches[quarantine["name"]].groupdict(
+ default="")
# check if a quarantine is configured
- if quarantine["quarantine_obj"] != None:
+ if quarantine["quarantine_obj"] is not None:
# add email to quarantine
- self.logger.info("{}: adding to quarantine '{}' for: {}".format(self.queueid, quarantine["name"], ", ".join(recipients)))
+ self.logger.info("{}: adding to quarantine '{}' for: {}".format(
+ self.queueid, quarantine["name"], ", ".join(recipients)))
try:
- quarantine_id = quarantine["quarantine_obj"].add(self.queueid, self.mailfrom, recipients, headers, self.fp,
- subgroups, named_subgroups)
+ quarantine_id = quarantine["quarantine_obj"].add(
+ self.queueid, self.mailfrom, recipients, headers, self.fp,
+ subgroups, named_subgroups)
except RuntimeError as e:
- self.logger.error("{}: unable to add to quarantine '{}': {}".format(self.queueid, quarantine["name"], e))
+ self.logger.error(
+ "{}: unable to add to quarantine '{}': {}".format(
+ self.queueid, quarantine["name"], e))
return Milter.TEMPFAIL
# check if a notification is configured
- if quarantine["notification_obj"] != None:
+ if quarantine["notification_obj"] is not None:
# notify
- self.logger.info("{}: sending notification for quarantine '{}' to: {}".format(self.queueid, quarantine["name"], ", ".join(recipients)))
+ self.logger.info(
+ "{}: sending notification for quarantine '{}' to: {}".format(
+ self.queueid, quarantine["name"], ", ".join(recipients)))
try:
- quarantine["notification_obj"].notify(self.queueid, quarantine_id, self.mailfrom, recipients, headers, self.fp,
- subgroups, named_subgroups)
+ quarantine["notification_obj"].notify(
+ self.queueid, quarantine_id,
+ self.mailfrom, recipients, headers, self.fp,
+ subgroups, named_subgroups)
except RuntimeError as e:
- self.logger.error("{}: unable to send notification for quarantine '{}': {}".format(self.queueid, quarantine["name"], e))
+ self.logger.error(
+ "{}: unable to send notification for quarantine '{}': {}".format(
+ self.queueid, quarantine["name"], e))
return Milter.TEMPFAIL
# remove processed recipient
@@ -264,19 +347,27 @@ class QuarantineMilter(Milter.Base):
# email passed clean for at least one recipient, accepting email
if self.recipients:
- self.logger.info("{}: passed clean for: {}".format(self.queueid, ", ".join(self.recipients)))
+ self.logger.info(
+ "{}: passed clean for: {}".format(
+ self.queueid, ", ".join(
+ self.recipients)))
return Milter.ACCEPT
- ## return configured action
+ # return configured action
quarantine = self._get_preferred_quarantine()
- self.logger.info("{}: {} matching quarantine is '{}', performing milter action {}".format(self.queueid, self.global_config["preferred_quarantine_action"],
- quarantine["name"], quarantine["action"].upper()))
+ self.logger.info(
+ "{}: {} matching quarantine is '{}', performing milter action {}".format(
+ self.queueid,
+ self.global_config["preferred_quarantine_action"],
+ quarantine["name"],
+ quarantine["action"].upper()))
if quarantine["action"] == "reject":
self.setreply("554", "5.7.0", quarantine["reject_reason"])
return quarantine["milter_action"]
except Exception as e:
- self.logger.exception("an exception occured in eom function: {}".format(e))
+ self.logger.exception(
+ "an exception occured in eom function: {}".format(e))
return Milter.TEMPFAIL
@@ -298,24 +389,30 @@ def generate_milter_config(configtest=False, config_files=[]):
# 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")
+ 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("mandatory option '{}' not present in config section 'global'".format(option))
+ raise RuntimeError(
+ "mandatory option '{}' not present in config section 'global'".format(option))
# read global config section
global_config = dict(parser.items("global"))
global_config["preferred_quarantine_action"] = global_config["preferred_quarantine_action"].lower()
if global_config["preferred_quarantine_action"] not in ["first", "last"]:
- raise RuntimeError("option preferred_quarantine_action has illegal value")
+ raise RuntimeError(
+ "option preferred_quarantine_action has illegal value")
# read active quarantine names
- quarantine_names = [ q.strip() for q in global_config["quarantines"].split(",") ]
+ quarantine_names = [
+ q.strip() for q in global_config["quarantines"].split(",")]
if len(quarantine_names) != len(set(quarantine_names)):
- raise RuntimeError("at least one quarantine is specified multiple times in quarantines option")
+ raise RuntimeError(
+ "at least one quarantine is specified multiple times in quarantines option")
if "global" in quarantine_names:
quarantine_names.remove("global")
- logger.warning("removed illegal quarantine name 'global' from list of active quarantines")
+ logger.warning(
+ "removed illegal quarantine name 'global' from list of active quarantines")
if not quarantine_names:
raise RuntimeError("no quarantines configured")
@@ -325,18 +422,22 @@ def generate_milter_config(configtest=False, config_files=[]):
# iterate quarantine names
for index, quarantine_name in enumerate(quarantine_names):
- # check if config section for current quarantine exists
+ # check if config section for current quarantine exists
if quarantine_name not in parser.sections():
- raise RuntimeError("config section '{}' does not exist".format(quarantine_name))
+ raise RuntimeError(
+ "config section '{}' does not exist".format(quarantine_name))
config = dict(parser.items(quarantine_name))
# check if mandatory config options are present in config
- for option in ["regex", "quarantine_type", "notification_type", "action", "whitelist_type", "smtp_host", "smtp_port"]:
+ for option in ["regex", "quarantine_type", "notification_type",
+ "action", "whitelist_type", "smtp_host", "smtp_port"]:
if option not in config.keys() and \
option in global_config.keys():
config[option] = global_config[option]
if option not in config.keys():
- raise RuntimeError("mandatory option '{}' not present in config section '{}' or 'global'".format(option, quarantine_name))
+ raise RuntimeError(
+ "mandatory option '{}' not present in config section '{}' or 'global'".format(
+ option, quarantine_name))
# check if optional config options are present in config
defaults = {
@@ -357,45 +458,67 @@ def generate_milter_config(configtest=False, config_files=[]):
config["index"] = index
# pre-compile regex
- logger.debug("{}: compiling regex '{}'".format(quarantine_name, config["regex"]))
- config["regex_compiled"] = re.compile(config["regex"], re.MULTILINE + re.DOTALL + re.IGNORECASE)
+ logger.debug(
+ "{}: compiling regex '{}'".format(
+ quarantine_name,
+ config["regex"]))
+ config["regex_compiled"] = re.compile(
+ config["regex"], re.MULTILINE + re.DOTALL + re.IGNORECASE)
# create quarantine instance
quarantine_type = config["quarantine_type"].lower()
if quarantine_type in quarantines.TYPES.keys():
- logger.debug("{}: initializing quarantine type '{}'".format(quarantine_name, quarantine_type.upper()))
- quarantine = quarantines.TYPES[quarantine_type](global_config, config, configtest)
+ logger.debug(
+ "{}: initializing quarantine type '{}'".format(
+ quarantine_name,
+ quarantine_type.upper()))
+ quarantine = quarantines.TYPES[quarantine_type](
+ global_config, config, configtest)
elif quarantine_type == "none":
logger.debug("{}: quarantine is NONE".format(quarantine_name))
quarantine = None
else:
- raise RuntimeError("{}: unknown quarantine type '{}'".format(quarantine_name, quarantine_type))
+ raise RuntimeError(
+ "{}: unknown quarantine type '{}'".format(
+ quarantine_name, quarantine_type))
config["quarantine_obj"] = quarantine
# create whitelist instance
whitelist_type = config["whitelist_type"].lower()
if whitelist_type in whitelists.TYPES.keys():
- logger.debug("{}: initializing whitelist type '{}'".format(quarantine_name, whitelist_type.upper()))
- whitelist = whitelists.TYPES[whitelist_type](global_config, config, configtest)
+ logger.debug(
+ "{}: initializing whitelist type '{}'".format(
+ quarantine_name,
+ whitelist_type.upper()))
+ whitelist = whitelists.TYPES[whitelist_type](
+ global_config, config, configtest)
elif whitelist_type == "none":
logger.debug("{}: whitelist is NONE".format(quarantine_name))
whitelist = None
else:
- raise RuntimeError("{}: unknown whitelist type '{}'".format(quarantine_name, whitelist_type))
+ raise RuntimeError(
+ "{}: unknown whitelist type '{}'".format(
+ quarantine_name, whitelist_type))
config["whitelist_obj"] = whitelist
# create notification instance
notification_type = config["notification_type"].lower()
if notification_type in notifications.TYPES.keys():
- logger.debug("{}: initializing notification type '{}'".format(quarantine_name, notification_type.upper()))
- notification = notifications.TYPES[notification_type](global_config, config, configtest)
+ logger.debug(
+ "{}: initializing notification type '{}'".format(
+ quarantine_name,
+ notification_type.upper()))
+ notification = notifications.TYPES[notification_type](
+ global_config, config, configtest)
elif notification_type == "none":
logger.debug("{}: notification is NONE".format(quarantine_name))
notification = None
else:
- raise RuntimeError("{}: unknown notification type '{}'".format(quarantine_name, notification_type))
+ raise RuntimeError(
+ "{}: unknown notification type '{}'".format(
+ quarantine_name, notification_type))
config["notification_obj"] = notification
@@ -405,11 +528,14 @@ def generate_milter_config(configtest=False, config_files=[]):
logger.debug("{}: action is {}".format(quarantine_name, action))
config["milter_action"] = QuarantineMilter.get_actions()[action]
else:
- raise RuntimeError("{}: unknown action '{}'".format(quarantine_name, action))
+ raise RuntimeError(
+ "{}: unknown action '{}'".format(
+ quarantine_name, action))
# create host/network whitelist
config["ignore_hosts_list"] = []
- ignored = set([ p.strip() for p in config["ignore_hosts"].split(",") if p])
+ ignored = set([p.strip()
+ for p in config["ignore_hosts"].split(",") if p])
for ignore in ignored:
if not ignore:
continue
@@ -421,7 +547,10 @@ def generate_milter_config(configtest=False, config_files=[]):
else:
config["ignore_hosts_list"].append(net)
if config["ignore_hosts_list"]:
- logger.debug("{}: ignore hosts: {}".format(quarantine_name, ", ".join(ignored)))
+ logger.debug(
+ "{}: ignore hosts: {}".format(
+ quarantine_name,
+ ", ".join(ignored)))
milter_config.append(config)
diff --git a/pyquarantine/cli.py b/pyquarantine/cli.py
index 4bcc18e..f3592f7 100644
--- a/pyquarantine/cli.py
+++ b/pyquarantine/cli.py
@@ -4,12 +4,12 @@
# 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
+# 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 .
#
@@ -25,7 +25,8 @@ import pyquarantine
def _get_quarantine_obj(config, quarantine):
try:
- quarantine_obj = next((q["quarantine_obj"] for q in config if q["name"] == quarantine))
+ quarantine_obj = next((q["quarantine_obj"]
+ for q in config if q["name"] == quarantine))
except StopIteration:
raise RuntimeError("invalid quarantine '{}'".format(quarantine))
return quarantine_obj
@@ -33,7 +34,8 @@ def _get_quarantine_obj(config, quarantine):
def _get_whitelist_obj(config, quarantine):
try:
- whitelist_obj = next((q["whitelist_obj"] for q in config if q["name"] == quarantine))
+ whitelist_obj = next((q["whitelist_obj"]
+ for q in config if q["name"] == quarantine))
except StopIteration:
raise RuntimeError("invalid quarantine '{}'".format(quarantine))
return whitelist_obj
@@ -51,7 +53,8 @@ def print_table(columns, rows):
# get the length of the header string
lengths = [len(header)]
# get the length of the longest value
- lengths.append(len(str(max(rows, key=lambda x: len(str(x[key])))[key])))
+ lengths.append(
+ len(str(max(rows, key=lambda x: len(str(x[key])))[key])))
# use the the longer one
length = max(lengths)
column_lengths.append(length)
@@ -70,7 +73,7 @@ def print_table(columns, rows):
print(row_format.format(*[column[0] for column in columns]))
print(separator)
- keys = [ entry[1] for entry in columns ]
+ keys = [entry[1] for entry in columns]
# print rows
for entry in rows:
row = []
@@ -81,10 +84,11 @@ def print_table(columns, rows):
def list_quarantines(config, args):
if args.batch:
- print("\n".join([ quarantine["name"] for quarantine in config ]))
+ print("\n".join([quarantine["name"] for quarantine in config]))
else:
print_table(
- [("Name", "name"), ("Quarantine", "quarantine_type"), ("Notification", "notification_type"), ("Action", "action")],
+ [("Name", "name"), ("Quarantine", "quarantine_type"),
+ ("Notification", "notification_type"), ("Action", "action")],
config
)
@@ -94,16 +98,23 @@ def list_quarantine_emails(config, args):
# get quarantine object
quarantine = _get_quarantine_obj(config, args.quarantine)
- if quarantine == None:
- raise RuntimeError("quarantine type is set to None, unable to list emails")
+ if quarantine is None:
+ raise RuntimeError(
+ "quarantine type is set to None, unable to list emails")
# find emails and transform some metadata values to strings
rows = []
- emails = quarantine.find(mailfrom=args.mailfrom, recipients=args.recipients, older_than=args.older_than)
+ emails = quarantine.find(
+ mailfrom=args.mailfrom,
+ recipients=args.recipients,
+ older_than=args.older_than)
for quarantine_id, metadata in emails.items():
row = emails[quarantine_id]
row["quarantine_id"] = quarantine_id
- row["date"] = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(metadata["date"]))
+ row["date"] = time.strftime(
+ '%Y-%m-%d %H:%M:%S',
+ time.localtime(
+ metadata["date"]))
row["mailfrom"] = metadata["mailfrom"]
row["recipient"] = metadata["recipients"].pop(0)
row["subject"] = emails[quarantine_id]["headers"]["subject"][:60]
@@ -124,9 +135,12 @@ def list_quarantine_emails(config, args):
print("\n".join(emails.keys()))
return
- if not emails: logger.info("quarantine '{}' is empty".format(args.quarantine))
+ if not emails:
+ logger.info("quarantine '{}' is empty".format(args.quarantine))
print_table(
- [("Quarantine-ID", "quarantine_id"), ("Date", "date"), ("From", "mailfrom"), ("Recipient(s)", "recipient"), ("Subject", "subject")],
+ [("Quarantine-ID", "quarantine_id"), ("Date", "date"),
+ ("From", "mailfrom"), ("Recipient(s)", "recipient"),
+ ("Subject", "subject")],
rows
)
@@ -136,25 +150,34 @@ def list_whitelist(config, args):
# get whitelist object
whitelist = _get_whitelist_obj(config, args.quarantine)
- if whitelist == None:
- raise RuntimeError("whitelist type is set to None, unable to list entries")
+ if whitelist is None:
+ raise RuntimeError(
+ "whitelist type is set to None, unable to list entries")
# find whitelist entries
- entries = whitelist.find(mailfrom=args.mailfrom, recipients=args.recipients, older_than=args.older_than)
+ entries = whitelist.find(
+ mailfrom=args.mailfrom,
+ recipients=args.recipients,
+ older_than=args.older_than)
if not entries:
- logger.info("whitelist of quarantine '{}' is empty".format(args.quarantine))
+ logger.info(
+ "whitelist of quarantine '{}' is empty".format(
+ args.quarantine))
return
# transform some values to strings
for entry_id, entry in entries.items():
entries[entry_id]["permanent_str"] = str(entry["permanent"])
- entries[entry_id]["created_str"] = entry["created"].strftime('%Y-%m-%d %H:%M:%S')
- entries[entry_id]["last_used_str"] = entry["last_used"].strftime('%Y-%m-%d %H:%M:%S')
+ entries[entry_id]["created_str"] = entry["created"].strftime(
+ '%Y-%m-%d %H:%M:%S')
+ entries[entry_id]["last_used_str"] = entry["last_used"].strftime(
+ '%Y-%m-%d %H:%M:%S')
print_table(
[
- ("ID", "id"), ("From", "mailfrom"), ("To", "recipient"), ("Created", "created_str"),
- ("Last used", "last_used_str"), ("Comment", "comment"), ("Permanent", "permanent_str")
+ ("ID", "id"), ("From", "mailfrom"), ("To", "recipient"),
+ ("Created", "created_str"), ("Last used", "last_used_str"),
+ ("Comment", "comment"), ("Permanent", "permanent_str")
],
entries.values()
)
@@ -165,32 +188,40 @@ def add_whitelist_entry(config, args):
# get whitelist object
whitelist = _get_whitelist_obj(config, args.quarantine)
- if whitelist == None:
- raise RuntimeError("whitelist type is set to None, unable to add entries")
+ if whitelist is None:
+ raise RuntimeError(
+ "whitelist type is set to None, unable to add entries")
# check existing entries
entries = whitelist.check(args.mailfrom, args.recipient)
if entries:
# check if the exact entry exists already
for entry in entries.values():
- if entry["mailfrom"] == args.mailfrom and entry["recipient"] == args.recipient:
- raise RuntimeError("an entry with this from/to combination already exists")
+ if entry["mailfrom"] == args.mailfrom and \
+ entry["recipient"] == args.recipient:
+ raise RuntimeError(
+ "an entry with this from/to combination already exists")
if not args.force:
# the entry is already covered by others
for entry_id, entry in entries.items():
entries[entry_id]["permanent_str"] = str(entry["permanent"])
- entries[entry_id]["created_str"] = entry["created"].strftime('%Y-%m-%d %H:%M:%S')
- entries[entry_id]["last_used_str"] = entry["last_used"].strftime('%Y-%m-%d %H:%M:%S')
+ entries[entry_id]["created_str"] = entry["created"].strftime(
+ '%Y-%m-%d %H:%M:%S')
+ entries[entry_id]["last_used_str"] = entry["last_used"].strftime(
+ '%Y-%m-%d %H:%M:%S')
print_table(
[
- ("ID", "id"), ("From", "mailfrom"), ("To", "recipient"), ("Created", "created_str"),
- ("Last used", "last_used_str"), ("Comment", "comment"), ("Permanent", "permanent_str")
+ ("ID", "id"), ("From", "mailfrom"), ("To", "recipient"),
+ ("Created", "created_str"), ("Last used", "last_used_str"),
+ ("Comment", "comment"), ("Permanent", "permanent_str")
],
entries.values()
)
print("")
- raise RuntimeError("from/to combination is already covered by the entries above, use --force to override.")
+ raise RuntimeError(
+ "from/to combination is already covered by the entries above, "
+ "use --force to override.")
# add entry to whitelist
whitelist.add(args.mailfrom, args.recipient, args.comment, args.permanent)
@@ -201,8 +232,9 @@ def delete_whitelist_entry(config, args):
logger = logging.getLogger(__name__)
whitelist = _get_whitelist_obj(config, args.quarantine)
- if whitelist == None:
- raise RuntimeError("whitelist type is set to None, unable to delete entries")
+ if whitelist is None:
+ raise RuntimeError(
+ "whitelist type is set to None, unable to delete entries")
whitelist.delete(args.whitelist_id)
logger.info("whitelist entry deleted successfully")
@@ -212,8 +244,9 @@ def notify_email(config, args):
logger = logging.getLogger(__name__)
quarantine = _get_quarantine_obj(config, args.quarantine)
- if quarantine == None:
- raise RuntimeError("quarantine type is set to None, unable to send notification")
+ if quarantine is None:
+ raise RuntimeError(
+ "quarantine type is set to None, unable to send notification")
quarantine.notify(args.quarantine_id, args.recipient)
logger.info("sent notification successfully")
@@ -222,8 +255,9 @@ def release_email(config, args):
logger = logging.getLogger(__name__)
quarantine = _get_quarantine_obj(config, args.quarantine)
- if quarantine == None:
- raise RuntimeError("quarantine type is set to None, unable to release email")
+ if quarantine is None:
+ raise RuntimeError(
+ "quarantine type is set to None, unable to release email")
quarantine.release(args.quarantine_id, args.recipient)
logger.info("quarantined email released successfully")
@@ -233,8 +267,9 @@ def delete_email(config, args):
logger = logging.getLogger(__name__)
quarantine = _get_quarantine_obj(config, args.quarantine)
- if quarantine == None:
- raise RuntimeError("quarantine type is set to None, unable to delete email")
+ if quarantine is None:
+ raise RuntimeError(
+ "quarantine type is set to None, unable to delete email")
quarantine.delete(args.quarantine_id, args.recipient)
logger.info("quarantined email deleted successfully")
@@ -253,78 +288,233 @@ class StdOutFilter(logging.Filter):
def main():
"PyQuarantine command-line interface."
# parse command line
- formatter_class = lambda prog: argparse.HelpFormatter(prog, max_help_position=50, width=140)
- parser = argparse.ArgumentParser(description="PyQuarantine CLI", formatter_class=formatter_class)
- parser.add_argument("-c", "--config", help="Config files to read.", nargs="+", metavar="CFG",
- default=pyquarantine.QuarantineMilter.get_configfiles())
- parser.add_argument("-d", "--debug", help="Log debugging messages.", action="store_true")
+ def formatter_class(prog): return argparse.HelpFormatter(
+ prog, max_help_position=50, width=140)
+ parser = argparse.ArgumentParser(
+ description="PyQuarantine CLI",
+ formatter_class=formatter_class)
+ parser.add_argument(
+ "-c", "--config",
+ help="Config files to read.",
+ nargs="+", metavar="CFG",
+ default=pyquarantine.QuarantineMilter.get_configfiles())
+ parser.add_argument(
+ "-d", "--debug",
+ help="Log debugging messages.",
+ action="store_true")
parser.set_defaults(syslog=False)
- subparsers = parser.add_subparsers(dest="command", title="Commands")
+ subparsers = parser.add_subparsers(
+ dest="command",
+ title="Commands")
subparsers.required = True
# list command
- list_parser = subparsers.add_parser("list", help="List available quarantines.", formatter_class=formatter_class)
- list_parser.add_argument("-b", "--batch", help="Print results using only quarantine names, each on a new line.", action="store_true")
+ list_parser = subparsers.add_parser(
+ "list",
+ help="List available quarantines.",
+ formatter_class=formatter_class)
+ list_parser.add_argument(
+ "-b", "--batch",
+ help="Print results using only quarantine names, each on a new line.",
+ action="store_true")
list_parser.set_defaults(func=list_quarantines)
# quarantine command group
- quarantine_parser = subparsers.add_parser("quarantine", description="Manage quarantines.", help="Manage quarantines.", formatter_class=formatter_class)
- quarantine_parser.add_argument("quarantine", metavar="QUARANTINE", help="Quarantine name.")
- quarantine_subparsers = quarantine_parser.add_subparsers(dest="command", title="Quarantine commands")
+ quarantine_parser = subparsers.add_parser(
+ "quarantine",
+ description="Manage quarantines.",
+ help="Manage quarantines.",
+ formatter_class=formatter_class)
+ quarantine_parser.add_argument(
+ "quarantine",
+ metavar="QUARANTINE",
+ help="Quarantine name.")
+ quarantine_subparsers = quarantine_parser.add_subparsers(
+ dest="command",
+ title="Quarantine commands")
quarantine_subparsers.required = True
# quarantine list command
- quarantine_list_parser = quarantine_subparsers.add_parser("list", description="List emails in quarantines.", help="List emails in quarantine.", formatter_class=formatter_class)
- quarantine_list_parser.add_argument("-f", "--from", dest="mailfrom", help="Filter emails by from address.", default=None, nargs="+")
- quarantine_list_parser.add_argument("-t", "--to", dest="recipients", help="Filter emails by recipient address.", default=None, nargs="+")
- quarantine_list_parser.add_argument("-o", "--older-than", dest="older_than", help="Filter emails by age (days).", default=None, type=float)
- quarantine_list_parser.add_argument("-b", "--batch", help="Print results using only email quarantine IDs, each on a new line.", action="store_true")
+ quarantine_list_parser = quarantine_subparsers.add_parser(
+ "list",
+ description="List emails in quarantines.",
+ help="List emails in quarantine.",
+ formatter_class=formatter_class)
+ quarantine_list_parser.add_argument(
+ "-f", "--from",
+ dest="mailfrom",
+ help="Filter emails by from address.",
+ default=None,
+ nargs="+")
+ quarantine_list_parser.add_argument(
+ "-t", "--to",
+ dest="recipients",
+ help="Filter emails by recipient address.",
+ default=None,
+ nargs="+")
+ quarantine_list_parser.add_argument(
+ "-o", "--older-than",
+ dest="older_than",
+ help="Filter emails by age (days).",
+ default=None,
+ type=float)
+ quarantine_list_parser.add_argument(
+ "-b", "--batch",
+ help="Print results using only email quarantine IDs, each on a new line.",
+ action="store_true")
quarantine_list_parser.set_defaults(func=list_quarantine_emails)
# quarantine notify command
- quarantine_notify_parser = quarantine_subparsers.add_parser("notify", description="Notify recipient about email in quarantine.", help="Notify recipient about email in quarantine.", formatter_class=formatter_class)
- quarantine_notify_parser.add_argument("quarantine_id", metavar="ID", help="Quarantine ID.")
- quarantine_notify_parser_group = quarantine_notify_parser.add_mutually_exclusive_group(required=True)
- quarantine_notify_parser_group.add_argument("-t", "--to", dest="recipient", help="Release email for one recipient address.")
- quarantine_notify_parser_group.add_argument("-a", "--all", help="Release email for all recipients.", action="store_true")
+ quarantine_notify_parser = quarantine_subparsers.add_parser(
+ "notify",
+ description="Notify recipient about email in quarantine.",
+ help="Notify recipient about email in quarantine.",
+ formatter_class=formatter_class)
+ quarantine_notify_parser.add_argument(
+ "quarantine_id",
+ metavar="ID",
+ help="Quarantine ID.")
+ quarantine_notify_parser_group = quarantine_notify_parser.add_mutually_exclusive_group(
+ required=True)
+ quarantine_notify_parser_group.add_argument(
+ "-t", "--to",
+ dest="recipient",
+ help="Release email for one recipient address.")
+ quarantine_notify_parser_group.add_argument(
+ "-a", "--all",
+ help="Release email for all recipients.",
+ action="store_true")
quarantine_notify_parser.set_defaults(func=notify_email)
# quarantine release command
- quarantine_release_parser = quarantine_subparsers.add_parser("release", description="Release email from quarantine.", help="Release email from quarantine.", formatter_class=formatter_class)
- quarantine_release_parser.add_argument("quarantine_id", metavar="ID", help="Quarantine ID.")
- quarantine_release_parser.add_argument("-n", "--disable-syslog", dest="syslog", help="Disable syslog messages.", action="store_false")
- quarantine_release_parser_group = quarantine_release_parser.add_mutually_exclusive_group(required=True)
- quarantine_release_parser_group.add_argument("-t", "--to", dest="recipient", help="Release email for one recipient address.")
- quarantine_release_parser_group.add_argument("-a", "--all", help="Release email for all recipients.", action="store_true")
+ quarantine_release_parser = quarantine_subparsers.add_parser(
+ "release",
+ description="Release email from quarantine.",
+ help="Release email from quarantine.",
+ formatter_class=formatter_class)
+ quarantine_release_parser.add_argument(
+ "quarantine_id",
+ metavar="ID",
+ help="Quarantine ID.")
+ quarantine_release_parser.add_argument(
+ "-n",
+ "--disable-syslog",
+ dest="syslog",
+ help="Disable syslog messages.",
+ action="store_false")
+ quarantine_release_parser_group = quarantine_release_parser.add_mutually_exclusive_group(
+ required=True)
+ quarantine_release_parser_group.add_argument(
+ "-t", "--to",
+ dest="recipient",
+ help="Release email for one recipient address.")
+ quarantine_release_parser_group.add_argument(
+ "-a", "--all",
+ help="Release email for all recipients.",
+ action="store_true")
quarantine_release_parser.set_defaults(func=release_email)
# quarantine delete command
- quarantine_delete_parser = quarantine_subparsers.add_parser("delete", description="Delete email from quarantine.", help="Delete email from quarantine.", formatter_class=formatter_class)
- quarantine_delete_parser.add_argument("quarantine_id", metavar="ID", help="Quarantine ID.")
- quarantine_delete_parser.add_argument("-n", "--disable-syslog", dest="syslog", help="Disable syslog messages.", action="store_false")
- quarantine_delete_parser_group = quarantine_delete_parser.add_mutually_exclusive_group(required=True)
- quarantine_delete_parser_group.add_argument("-t", "--to", dest="recipient", help="Delete email for one recipient address.")
- quarantine_delete_parser_group.add_argument("-a", "--all", help="Delete email for all recipients.", action="store_true")
+ quarantine_delete_parser = quarantine_subparsers.add_parser(
+ "delete",
+ description="Delete email from quarantine.",
+ help="Delete email from quarantine.",
+ formatter_class=formatter_class)
+ quarantine_delete_parser.add_argument(
+ "quarantine_id",
+ metavar="ID",
+ help="Quarantine ID.")
+ quarantine_delete_parser.add_argument(
+ "-n", "--disable-syslog",
+ dest="syslog",
+ help="Disable syslog messages.",
+ action="store_false")
+ quarantine_delete_parser_group = quarantine_delete_parser.add_mutually_exclusive_group(
+ required=True)
+ quarantine_delete_parser_group.add_argument(
+ "-t", "--to",
+ dest="recipient",
+ help="Delete email for one recipient address.")
+ quarantine_delete_parser_group.add_argument(
+ "-a", "--all",
+ help="Delete email for all recipients.",
+ action="store_true")
quarantine_delete_parser.set_defaults(func=delete_email)
# whitelist command group
- whitelist_parser = subparsers.add_parser("whitelist", description="Manage whitelists.", help="Manage whitelists.", formatter_class=formatter_class)
- whitelist_parser.add_argument("quarantine", metavar="QUARANTINE", help="Quarantine name.")
- whitelist_subparsers = whitelist_parser.add_subparsers(dest="command", title="Whitelist commands")
+ whitelist_parser = subparsers.add_parser(
+ "whitelist",
+ description="Manage whitelists.",
+ help="Manage whitelists.",
+ formatter_class=formatter_class)
+ whitelist_parser.add_argument(
+ "quarantine",
+ metavar="QUARANTINE",
+ help="Quarantine name.")
+ whitelist_subparsers = whitelist_parser.add_subparsers(
+ dest="command",
+ title="Whitelist commands")
whitelist_subparsers.required = True
# whitelist list command
- whitelist_list_parser = whitelist_subparsers.add_parser("list", description="List whitelist entries.", help="List whitelist entries.", formatter_class=formatter_class)
- whitelist_list_parser.add_argument("-f", "--from", dest="mailfrom", help="Filter entries by from address.", default=None, nargs="+")
- whitelist_list_parser.add_argument("-t", "--to", dest="recipients", help="Filter entries by recipient address.", default=None, nargs="+")
- whitelist_list_parser.add_argument("-o", "--older-than", dest="older_than", help="Filter emails by last used date (days).", default=None, type=float)
+ whitelist_list_parser = whitelist_subparsers.add_parser(
+ "list",
+ description="List whitelist entries.",
+ help="List whitelist entries.",
+ formatter_class=formatter_class)
+ whitelist_list_parser.add_argument(
+ "-f", "--from",
+ dest="mailfrom",
+ help="Filter entries by from address.",
+ default=None,
+ nargs="+")
+ whitelist_list_parser.add_argument(
+ "-t", "--to",
+ dest="recipients",
+ help="Filter entries by recipient address.",
+ default=None,
+ nargs="+")
+ whitelist_list_parser.add_argument(
+ "-o", "--older-than",
+ dest="older_than",
+ help="Filter emails by last used date (days).",
+ default=None,
+ type=float)
whitelist_list_parser.set_defaults(func=list_whitelist)
# whitelist add command
- whitelist_add_parser = whitelist_subparsers.add_parser("add", description="Add whitelist entry.", help="Add whitelist entry.", formatter_class=formatter_class)
- whitelist_add_parser.add_argument("-f", "--from", dest="mailfrom", help="From address.", required=True)
- whitelist_add_parser.add_argument("-t", "--to", dest="recipient", help="Recipient address.", required=True)
- whitelist_add_parser.add_argument("-c", "--comment", help="Comment.", default="added by CLI")
- whitelist_add_parser.add_argument("-p", "--permanent", help="Add a permanent entry.", action="store_true")
- whitelist_add_parser.add_argument("--force", help="Force adding an entry, even if already covered by another entry.", action="store_true")
+ whitelist_add_parser = whitelist_subparsers.add_parser(
+ "add",
+ description="Add whitelist entry.",
+ help="Add whitelist entry.",
+ formatter_class=formatter_class)
+ whitelist_add_parser.add_argument(
+ "-f", "--from",
+ dest="mailfrom",
+ help="From address.",
+ required=True)
+ whitelist_add_parser.add_argument(
+ "-t", "--to",
+ dest="recipient",
+ help="Recipient address.",
+ required=True)
+ whitelist_add_parser.add_argument(
+ "-c", "--comment",
+ help="Comment.",
+ default="added by CLI")
+ whitelist_add_parser.add_argument(
+ "-p", "--permanent",
+ help="Add a permanent entry.",
+ action="store_true")
+ whitelist_add_parser.add_argument(
+ "--force",
+ help="Force adding an entry, even if already covered by another entry.",
+ action="store_true")
whitelist_add_parser.set_defaults(func=add_whitelist_entry)
# whitelist delete command
- whitelist_delete_parser = whitelist_subparsers.add_parser("delete", description="Delete whitelist entry.", help="Delete whitelist entry.", formatter_class=formatter_class)
- whitelist_delete_parser.add_argument("whitelist_id", metavar="ID", help="Whitelist ID.")
+ whitelist_delete_parser = whitelist_subparsers.add_parser(
+ "delete",
+ description="Delete whitelist entry.",
+ help="Delete whitelist entry.",
+ formatter_class=formatter_class)
+ whitelist_delete_parser.add_argument(
+ "whitelist_id",
+ metavar="ID",
+ help="Whitelist ID.")
whitelist_delete_parser.set_defaults(func=delete_whitelist_entry)
args = parser.parse_args()
@@ -336,7 +526,8 @@ def main():
# setup console log
if args.debug:
- formatter = logging.Formatter("%(levelname)s: [%(name)s] - %(message)s")
+ formatter = logging.Formatter(
+ "%(levelname)s: [%(name)s] - %(message)s")
else:
formatter = logging.Formatter("%(levelname)s: %(message)s")
# stdout
@@ -355,17 +546,21 @@ def main():
# try to generate milter configs
try:
- global_config, config = pyquarantine.generate_milter_config(config_files=args.config, configtest=True)
+ global_config, config = pyquarantine.generate_milter_config(
+ config_files=args.config, configtest=True)
except RuntimeError as e:
logger.error(e)
sys.exit(255)
if args.syslog:
# setup syslog
- sysloghandler = logging.handlers.SysLogHandler(address="/dev/log", facility=logging.handlers.SysLogHandler.LOG_MAIL)
+ sysloghandler = logging.handlers.SysLogHandler(
+ address="/dev/log",
+ facility=logging.handlers.SysLogHandler.LOG_MAIL)
sysloghandler.setLevel(loglevel)
if args.debug:
- formatter = logging.Formatter("pyquarantine: [%(name)s] [%(levelname)s] %(message)s")
+ formatter = logging.Formatter(
+ "pyquarantine: [%(name)s] [%(levelname)s] %(message)s")
else:
formatter = logging.Formatter("pyquarantine: %(message)s")
sysloghandler.setFormatter(formatter)
diff --git a/pyquarantine/mailer.py b/pyquarantine/mailer.py
index 76e5117..8cb5405 100644
--- a/pyquarantine/mailer.py
+++ b/pyquarantine/mailer.py
@@ -2,12 +2,12 @@
# 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
+# 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 .
#
@@ -38,31 +38,37 @@ def mailprocess():
try:
while True:
m = queue.get()
- if not m: break
+ if not m:
+ break
smtp_host, smtp_port, queueid, mailfrom, recipient, mail, emailtype = m
try:
smtp_send(smtp_host, smtp_port, mailfrom, recipient, mail)
except Exception as e:
- logger.error("{}: error while sending {} to '{}': {}".format(queueid, emailtype, recipient, e))
+ logger.error(
+ "{}: error while sending {} to '{}': {}".format(
+ queueid, emailtype, recipient, e))
else:
- logger.info("{}: successfully sent {} to: {}".format(queueid, emailtype, recipient))
+ logger.info(
+ "{}: successfully sent {} to: {}".format(
+ queueid, emailtype, recipient))
except KeyboardInterrupt:
pass
logger.debug("mailer process terminated")
-def sendmail(smtp_host, smtp_port, queueid, mailfrom, recipients, mail, emailtype="email"):
+def sendmail(smtp_host, smtp_port, queueid, mailfrom, recipients, mail,
+ emailtype="email"):
"Send an email."
global logger
global process
global queue
- if type(recipients) == str:
+ if isinstance(recipients, str):
recipients = [recipients]
# start mailprocess if it is not started yet
- if process == None:
+ if process is None:
process = Process(target=mailprocess)
process.daemon = True
logger.debug("starting mailer process")
@@ -70,6 +76,9 @@ def sendmail(smtp_host, smtp_port, queueid, mailfrom, recipients, mail, emailtyp
for recipient in recipients:
try:
- queue.put((smtp_host, smtp_port, queueid, mailfrom, recipient, mail, emailtype), timeout=30)
+ queue.put(
+ (smtp_host, smtp_port, queueid, mailfrom, recipient, mail,
+ emailtype),
+ timeout=30)
except Queue.Full as e:
raise RuntimeError("email queue is full")
diff --git a/pyquarantine/notifications.py b/pyquarantine/notifications.py
index 6807f15..7baa2dc 100644
--- a/pyquarantine/notifications.py
+++ b/pyquarantine/notifications.py
@@ -2,12 +2,12 @@
# 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
+# 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 .
#
@@ -26,15 +26,18 @@ from os.path import basename
from pyquarantine import mailer
+
class BaseNotification(object):
"Notification base class"
+
def __init__(self, global_config, config, configtest=False):
self.quarantine_name = config["name"]
self.global_config = global_config
self.config = config
self.logger = logging.getLogger(__name__)
- def notify(self, queueid, quarantine_id, mailfrom, recipients, headers, fp, subgroups=None, named_subgroups=None, synchronous=False):
+ def notify(self, queueid, quarantine_id, mailfrom, recipients, headers,
+ fp, subgroups=None, named_subgroups=None, synchronous=False):
fp.seek(0)
pass
@@ -44,74 +47,85 @@ class EMailNotification(BaseNotification):
_html_text = "text/html"
_plain_text = "text/plain"
_bad_tags = [
- "applet",
- "embed",
- "frame",
- "frameset",
- "head",
- "iframe",
- "script"
+ "applet",
+ "embed",
+ "frame",
+ "frameset",
+ "head",
+ "iframe",
+ "script"
]
_good_tags = [
- "a",
- "b",
- "br",
- "center",
- "div",
- "font",
- "h1",
- "h2",
- "h3",
- "h4",
- "h5",
- "h6",
- "i",
- "img",
- "li",
- "span",
- "table",
- "td",
- "th",
- "tr",
- "tt",
- "u",
- "ul"
+ "a",
+ "b",
+ "br",
+ "center",
+ "div",
+ "font",
+ "h1",
+ "h2",
+ "h3",
+ "h4",
+ "h5",
+ "h6",
+ "i",
+ "img",
+ "li",
+ "span",
+ "table",
+ "td",
+ "th",
+ "tr",
+ "tt",
+ "u",
+ "ul"
]
good_attributes = [
- "align",
- "alt",
- "bgcolor",
- "border",
- "cellpadding",
- "cellspacing",
- "color",
- "colspan",
- "dir",
- "face",
- "headers",
- "height",
- "id",
- "name",
- "rowspan",
- "size",
- "src",
- "style",
- "title",
- "type",
- "valign",
- "value",
- "width"
+ "align",
+ "alt",
+ "bgcolor",
+ "border",
+ "cellpadding",
+ "cellspacing",
+ "color",
+ "colspan",
+ "dir",
+ "face",
+ "headers",
+ "height",
+ "id",
+ "name",
+ "rowspan",
+ "size",
+ "src",
+ "style",
+ "title",
+ "type",
+ "valign",
+ "value",
+ "width"
]
def __init__(self, global_config, config, configtest=False):
- super(EMailNotification, self).__init__(global_config, config, configtest)
+ super(EMailNotification, self).__init__(
+ global_config, config, configtest)
# check if mandatory options are present in config
- for option in ["smtp_host", "smtp_port", "notification_email_envelope_from", "notification_email_from", "notification_email_subject", "notification_email_template", "notification_email_replacement_img", "notification_email_embedded_imgs"]:
+ for option in [
+ "smtp_host",
+ "smtp_port",
+ "notification_email_envelope_from",
+ "notification_email_from",
+ "notification_email_subject",
+ "notification_email_template",
+ "notification_email_replacement_img",
+ "notification_email_embedded_imgs"]:
if option not in self.config.keys() and option in self.global_config.keys():
self.config[option] = self.global_config[option]
if option not in self.config.keys():
- raise RuntimeError("mandatory option '{}' not present in config section '{}' or 'global'".format(option, self.quarantine_name))
+ raise RuntimeError(
+ "mandatory option '{}' not present in config section '{}' or 'global'".format(
+ option, self.quarantine_name))
self.smtp_host = self.config["smtp_host"]
self.smtp_port = self.config["smtp_port"]
@@ -125,17 +139,20 @@ class EMailNotification(BaseNotification):
try:
self.from_header.format_map(testvars)
except ValueError as e:
- raise RuntimeError("error parsing notification_email_from: {}".format(e))
+ raise RuntimeError(
+ "error parsing notification_email_from: {}".format(e))
# test-parse subject
try:
self.subject.format_map(testvars)
except ValueError as e:
- raise RuntimeError("error parsing notification_email_subject: {}".format(e))
+ raise RuntimeError(
+ "error parsing notification_email_subject: {}".format(e))
# read and parse email notification template
try:
- self.template = open(self.config["notification_email_template"], "r").read()
+ self.template = open(
+ self.config["notification_email_template"], "r").read()
self.template.format_map(testvars)
except IOError as e:
raise RuntimeError("error reading template: {}".format(e))
@@ -143,19 +160,24 @@ class EMailNotification(BaseNotification):
raise RuntimeError("error parsing template: {}".format(e))
# read email replacement image if specified
- replacement_img_path = self.config["notification_email_replacement_img"].strip()
+ replacement_img_path = self.config["notification_email_replacement_img"].strip(
+ )
if replacement_img_path:
try:
- self.replacement_img = MIMEImage(open(replacement_img_path, "rb").read())
+ self.replacement_img = MIMEImage(
+ open(replacement_img_path, "rb").read())
except IOError as e:
- raise RuntimeError("error reading replacement image: {}".format(e))
+ raise RuntimeError(
+ "error reading replacement image: {}".format(e))
else:
- self.replacement_img.add_header("Content-ID", "")
+ self.replacement_img.add_header(
+ "Content-ID", "")
else:
self.replacement_img = None
# read images to embed if specified
- embedded_img_paths = [ p.strip() for p in self.config["notification_email_embedded_imgs"].split(",") if p]
+ embedded_img_paths = [
+ p.strip() for p in self.config["notification_email_embedded_imgs"].split(",") if p]
self.embedded_imgs = []
for img_path in embedded_img_paths:
# read image
@@ -167,19 +189,24 @@ class EMailNotification(BaseNotification):
img.add_header("Content-ID", "<{}>".format(basename(img_path)))
self.embedded_imgs.append(img)
-
def get_text(self, queueid, part):
"Get the mail text in html form from email part."
mimetype = part.get_content_type()
- self.logger.debug("{}: extracting content of email text part".format(queueid))
+ self.logger.debug(
+ "{}: extracting content of email text part".format(queueid))
text = part.get_payload(decode=True)
if mimetype == EMailNotification._plain_text:
- self.logger.debug("{}: content mimetype is {}, converting to {}".format(queueid, mimetype, self._html_text))
- text = re.sub(r"^(.*)$", r"\1
\n", text.decode(), flags=re.MULTILINE)
+ self.logger.debug(
+ "{}: content mimetype is {}, converting to {}".format(
+ queueid, mimetype, self._html_text))
+ text = re.sub(r"^(.*)$", r"\1
\n",
+ text.decode(), flags=re.MULTILINE)
else:
- self.logger.debug("{}: content mimetype is {}".format(queueid, mimetype))
+ self.logger.debug(
+ "{}: content mimetype is {}".format(
+ queueid, mimetype))
return BeautifulSoup(text, "lxml")
@@ -189,12 +216,13 @@ class EMailNotification(BaseNotification):
for part in msg.get_payload():
mimetype = part.get_content_type()
- if mimetype in [EMailNotification._plain_text, EMailNotification._html_text]:
+ if mimetype in [EMailNotification._plain_text,
+ EMailNotification._html_text]:
soup = self.get_text(queueid, part)
elif mimetype.startswith("multipart"):
soup = self.get_text_multipart(queueid, part, preferred)
- if soup != None and mimetype == preferred:
+ if soup is not None and mimetype == preferred:
break
return soup
@@ -204,13 +232,17 @@ class EMailNotification(BaseNotification):
# completly remove bad elements
for element in soup(EMailNotification._bad_tags):
- self.logger.debug("{}: removing dangerous tag '{}' and its content".format(queueid, element.name))
+ self.logger.debug(
+ "{}: removing dangerous tag '{}' and its content".format(
+ queueid, element.name))
element.extract()
# remove not whitelisted elements, but keep their content
for element in soup.find_all(True):
if element.name not in EMailNotification._good_tags:
- self.logger.debug("{}: removing tag '{}', keep its content".format(queueid, element.name))
+ self.logger.debug(
+ "{}: removing tag '{}', keep its content".format(
+ queueid, element.name))
element.replaceWithChildren()
# remove not whitelisted attributes
@@ -218,10 +250,14 @@ class EMailNotification(BaseNotification):
for attribute in element.attrs.keys():
if attribute not in EMailNotification.good_attributes:
if element.name == "a" and attribute == "href":
- self.logger.debug("{}: setting attribute href to '#' on tag '{}'".format(queueid, element.name))
+ self.logger.debug(
+ "{}: setting attribute href to '#' on tag '{}'".format(
+ queueid, element.name))
element["href"] = "#"
else:
- self.logger.debug("{}: removing attribute '{}' from tag '{}'".format(queueid, attribute, element.name))
+ self.logger.debug(
+ "{}: removing attribute '{}' from tag '{}'".format(
+ queueid, attribute, element.name))
del(element.attrs[attribute])
return soup
@@ -230,33 +266,52 @@ class EMailNotification(BaseNotification):
soup = None
mimetype = msg.get_content_type()
- self.logger.debug("{}: trying to find text part of email".format(queueid))
- if mimetype in [EMailNotification._plain_text, EMailNotification._html_text]:
+ self.logger.debug(
+ "{}: trying to find text part of email".format(queueid))
+ if mimetype in [EMailNotification._plain_text,
+ EMailNotification._html_text]:
soup = self.get_text(queueid, msg)
elif mimetype.startswith("multipart"):
soup = self.get_text_multipart(queueid, msg)
- if soup == None:
- self.logger.error("{}: unable to extract text part of email".format(queueid))
+ if soup is None:
+ self.logger.error(
+ "{}: unable to extract text part of email".format(queueid))
text = "ERROR: unable to extract text from email body"
soup = BeautifulSoup(text, "lxml", "UTF-8")
return soup
- def notify(self, queueid, quarantine_id, mailfrom, recipients, headers, fp, subgroups=None, named_subgroups=None, synchronous=False):
+ def notify(self, queueid, quarantine_id, mailfrom, recipients, headers, fp,
+ subgroups=None, named_subgroups=None, synchronous=False):
"Notify recipients via email."
- super(EMailNotification, self).notify(queueid, quarantine_id, mailfrom, recipients, headers, fp, subgroups, named_subgroups, synchronous)
+ super(
+ EMailNotification,
+ self).notify(
+ queueid,
+ quarantine_id,
+ mailfrom,
+ recipients,
+ headers,
+ fp,
+ subgroups,
+ named_subgroups,
+ synchronous)
# extract html text from email
- self.logger.debug("{}: extraction email text from original email".format(queueid))
- soup = self.get_html_text_part(queueid, email.message_from_binary_file(fp))
+ self.logger.debug(
+ "{}: extraction email text from original email".format(queueid))
+ soup = self.get_html_text_part(
+ queueid, email.message_from_binary_file(fp))
# replace picture sources
image_replaced = False
if self.replacement_img:
for element in soup("img"):
if "src" in element.attrs.keys():
- self.logger.debug("{}: replacing image: {}".format(queueid, element["src"]))
+ self.logger.debug(
+ "{}: replacing image: {}".format(
+ queueid, element["src"]))
element["src"] = "cid:removed_for_security_reasons"
image_replaced = True
@@ -266,24 +321,27 @@ class EMailNotification(BaseNotification):
# sending email notifications
for recipient in recipients:
- self.logger.debug("{}: generating notification email for '{}'".format(queueid, recipient))
+ self.logger.debug(
+ "{}: generating notification email for '{}'".format(
+ queueid, recipient))
self.logger.debug("{}: parsing email template".format(queueid))
# generate dict containing all template variables
variables = defaultdict(str,
- EMAIL_HTML_TEXT=sanitized_text,
- EMAIL_FROM=escape(headers["from"]),
- EMAIL_ENVELOPE_FROM=escape(mailfrom),
- EMAIL_TO=escape(recipient),
- EMAIL_SUBJECT=escape(headers["subject"]),
- EMAIL_QUARANTINE_ID=quarantine_id)
+ EMAIL_HTML_TEXT=sanitized_text,
+ EMAIL_FROM=escape(headers["from"]),
+ EMAIL_ENVELOPE_FROM=escape(mailfrom),
+ EMAIL_TO=escape(recipient),
+ EMAIL_SUBJECT=escape(headers["subject"]),
+ EMAIL_QUARANTINE_ID=quarantine_id)
if subgroups:
number = 0
for subgroup in subgroups:
variables["SUBGROUP_{}".format(number)] = escape(subgroup)
if named_subgroups:
- for key, value in named_subgroups.items(): named_subgroups[key] = escape(value)
+ for key, value in named_subgroups.items():
+ named_subgroups[key] = escape(value)
variables.update(named_subgroups)
# parse template
@@ -297,21 +355,29 @@ class EMailNotification(BaseNotification):
msg.attach(MIMEText(htmltext, "html", 'UTF-8'))
if image_replaced:
- self.logger.debug("{}: attaching notification_replacement_img".format(queueid))
+ self.logger.debug(
+ "{}: attaching notification_replacement_img".format(queueid))
msg.attach(self.replacement_img)
for img in self.embedded_imgs:
self.logger.debug("{}: attaching imgage".format(queueid))
msg.attach(img)
- self.logger.debug("{}: sending notification email to: {}".format(queueid, recipient))
+ self.logger.debug(
+ "{}: sending notification email to: {}".format(
+ queueid, recipient))
if synchronous:
try:
- mailer.smtp_send(self.smtp_host, self.smtp_port, self.mailfrom, recipient, msg.as_string())
+ mailer.smtp_send(self.smtp_host, self.smtp_port,
+ self.mailfrom, recipient, msg.as_string())
except Exception as e:
- raise RuntimeError("error while sending email to '{}': {}".format(recipient, e))
+ raise RuntimeError(
+ "error while sending email to '{}': {}".format(
+ recipient, e))
else:
- mailer.sendmail(self.smtp_host, self.smtp_port, queueid, self.mailfrom, recipient, msg.as_string(), "notification email")
+ mailer.sendmail(self.smtp_host, self.smtp_port, queueid,
+ self.mailfrom, recipient, msg.as_string(),
+ "notification email")
# list of notification types and their related notification classes
diff --git a/pyquarantine/quarantines.py b/pyquarantine/quarantines.py
index b6a326c..ec6dd53 100644
--- a/pyquarantine/quarantines.py
+++ b/pyquarantine/quarantines.py
@@ -2,12 +2,12 @@
# 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
+# 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 .
#
@@ -27,13 +27,15 @@ from pyquarantine import mailer
class BaseQuarantine(object):
"Quarantine base class"
+
def __init__(self, global_config, config, configtest=False):
self.name = config["name"]
self.global_config = global_config
self.config = config
self.logger = logging.getLogger(__name__)
- def add(self, queueid, mailfrom, recipients, headers, fp, subgroups=None, named_subgroups=None):
+ def add(self, queueid, mailfrom, recipients, headers,
+ fp, subgroups=None, named_subgroups=None):
"Add email to quarantine."
fp.seek(0)
return ""
@@ -41,7 +43,7 @@ class BaseQuarantine(object):
def find(self, mailfrom=None, recipients=None, older_than=None):
"Find emails in quarantine."
return
-
+
def get_metadata(self, quarantine_id):
"Return metadata of quarantined email."
return
@@ -53,7 +55,8 @@ class BaseQuarantine(object):
def notify(self, quarantine_id, recipient=None):
"Notify recipient about email in quarantine."
if not self.config["notification_obj"]:
- raise RuntimeError("notification type is set to None, unable to send notifications")
+ raise RuntimeError(
+ "notification type is set to None, unable to send notifications")
return
def release(self, quarantine_id, recipient=None):
@@ -63,6 +66,7 @@ class BaseQuarantine(object):
class FileQuarantine(BaseQuarantine):
"Quarantine class to store mails on filesystem."
+
def __init__(self, global_config, config, configtest=False):
super(FileQuarantine, self).__init__(global_config, config, configtest)
@@ -71,12 +75,17 @@ class FileQuarantine(BaseQuarantine):
if option not in self.config.keys() and option in self.global_config.keys():
self.config[option] = self.global_config[option]
if option not in self.config.keys():
- raise RuntimeError("mandatory option '{}' not present in config section '{}' or 'global'".format(option, self.name))
+ raise RuntimeError(
+ "mandatory option '{}' not present in config section '{}' or 'global'".format(
+ option, self.name))
self.directory = self.config["quarantine_directory"]
# check if quarantine directory exists and is writable
- if not os.path.isdir(self.directory) or not os.access(self.directory, os.W_OK):
- raise RuntimeError("file quarantine directory '{}' does not exist or is not writable".format(self.directory))
+ if not os.path.isdir(self.directory) or not os.access(
+ self.directory, os.W_OK):
+ raise RuntimeError(
+ "file quarantine directory '{}' does not exist or is not writable".format(
+ self.directory))
self._metadata_suffix = ".metadata"
def _save_datafile(self, quarantine_id, fp):
@@ -88,7 +97,9 @@ class FileQuarantine(BaseQuarantine):
raise RuntimeError("unable save data file: {}".format(e))
def _save_metafile(self, quarantine_id, metadata):
- metafile = os.path.join(self.directory, "{}{}".format(quarantine_id, self._metadata_suffix))
+ metafile = os.path.join(
+ self.directory, "{}{}".format(
+ quarantine_id, self._metadata_suffix))
try:
with open(metafile, "w") as f:
json.dump(metadata, f, indent=2)
@@ -109,10 +120,21 @@ class FileQuarantine(BaseQuarantine):
except IOError as e:
raise RuntimeError("unable to remove data file: {}".format(e))
- def add(self, queueid, mailfrom, recipients, headers, fp, subgroups=None, named_subgroups=None):
+ def add(self, queueid, mailfrom, recipients, headers,
+ fp, subgroups=None, named_subgroups=None):
"Add email to file quarantine and return quarantine-id."
- super(FileQuarantine, self).add(queueid, mailfrom, recipients, headers, fp, subgroups, named_subgroups)
- quarantine_id = "{}_{}".format(datetime.now().strftime("%Y%m%d%H%M%S"), queueid)
+ super(
+ FileQuarantine,
+ self).add(
+ queueid,
+ mailfrom,
+ recipients,
+ headers,
+ fp,
+ subgroups,
+ named_subgroups)
+ quarantine_id = "{}_{}".format(
+ datetime.now().strftime("%Y%m%d%H%M%S"), queueid)
# save mail
self._save_datafile(quarantine_id, fp)
@@ -140,9 +162,12 @@ class FileQuarantine(BaseQuarantine):
"Return metadata of quarantined email."
super(FileQuarantine, self).get_metadata(quarantine_id)
- metafile = os.path.join(self.directory, "{}{}".format(quarantine_id, self._metadata_suffix))
+ metafile = os.path.join(
+ self.directory, "{}{}".format(
+ quarantine_id, self._metadata_suffix))
if not os.path.isfile(metafile):
- raise RuntimeError("invalid quarantine id '{}'".format(quarantine_id))
+ raise RuntimeError(
+ "invalid quarantine id '{}'".format(quarantine_id))
try:
with open(metafile, "r") as f:
@@ -150,33 +175,41 @@ class FileQuarantine(BaseQuarantine):
except IOError as e:
raise RuntimeError("unable to read metadata file: {}".format(e))
except json.JSONDecodeError as e:
- raise RuntimeError("invalid meta file '{}': {}".format(metafile, e))
+ raise RuntimeError(
+ "invalid meta file '{}': {}".format(
+ metafile, e))
return metadata
def find(self, mailfrom=None, recipients=None, older_than=None):
"Find emails in quarantine."
super(FileQuarantine, self).find(mailfrom, recipients, older_than)
- if type(mailfrom) == str: mailfrom = [mailfrom]
- if type(recipients) == str: recipients = [recipients]
+ if isinstance(mailfrom, str):
+ mailfrom = [mailfrom]
+ if isinstance(recipients, str):
+ recipients = [recipients]
emails = {}
- metafiles = glob(os.path.join(self.directory, "*{}".format(self._metadata_suffix)))
+ metafiles = glob(os.path.join(
+ self.directory, "*{}".format(self._metadata_suffix)))
for metafile in metafiles:
- if not os.path.isfile(metafile): continue
+ if not os.path.isfile(metafile):
+ continue
- quarantine_id = os.path.basename(metafile[:-len(self._metadata_suffix)])
+ quarantine_id = os.path.basename(
+ metafile[:-len(self._metadata_suffix)])
metadata = self.get_metadata(quarantine_id)
- if older_than != None:
- if timegm(gmtime()) - metadata["date"] < (older_than * 24 * 3600):
+ if older_than is not None:
+ if timegm(gmtime()) - metadata["date"] < (older_than * 86400):
continue
- if mailfrom != None:
+ if mailfrom is not None:
if metadata["mailfrom"] not in mailfrom:
continue
- if recipients != None:
- if len(recipients) == 1 and recipients[0] not in metadata["recipients"]:
+ if recipients is not None:
+ if len(recipients) == 1 and \
+ recipients[0] not in metadata["recipients"]:
continue
elif len(set(recipients + metadata["recipients"])) == len(recipients + metadata["recipients"]):
continue
@@ -194,7 +227,7 @@ class FileQuarantine(BaseQuarantine):
except RuntimeError as e:
raise RuntimeError("unable to delete email: {}".format(e))
- if recipient == None:
+ if recipient is None:
self._remove(quarantine_id)
else:
if recipient not in metadata["recipients"]:
@@ -215,7 +248,7 @@ class FileQuarantine(BaseQuarantine):
except RuntimeError as e:
raise RuntimeError("unable to release email: {}".format(e))
- if recipient != None:
+ if recipient is not None:
if recipient not in metadata["recipients"]:
raise RuntimeError("invalid recipient '{}'".format(recipient))
recipients = [recipient]
@@ -225,11 +258,13 @@ class FileQuarantine(BaseQuarantine):
datafile = os.path.join(self.directory, quarantine_id)
try:
with open(datafile, "rb") as fp:
- self.config["notification_obj"].notify(metadata["queue_id"], quarantine_id, metadata["mailfrom"], recipients, metadata["headers"], fp,
- metadata["subgroups"], metadata["named_subgroups"], synchronous=True)
+ self.config["notification_obj"].notify(
+ metadata["queue_id"], quarantine_id, metadata["mailfrom"],
+ recipients, metadata["headers"], fp,
+ metadata["subgroups"], metadata["named_subgroups"],
+ synchronous=True)
except IOError as e:
- raise(RuntimeError("unable to read data file: {}".format(e)))
-
+ raise RuntimeError
def release(self, quarantine_id, recipient=None):
"Release email from quarantine."
@@ -240,7 +275,7 @@ class FileQuarantine(BaseQuarantine):
except RuntimeError as e:
raise RuntimeError("unable to release email: {}".format(e))
- if recipient != None:
+ if recipient is not None:
if recipient not in metadata["recipients"]:
raise RuntimeError("invalid recipient '{}'".format(recipient))
recipients = [recipient]
@@ -256,9 +291,16 @@ class FileQuarantine(BaseQuarantine):
for recipient in recipients:
try:
- mailer.smtp_send(self.config["smtp_host"], self.config["smtp_port"], metadata["mailfrom"], recipient, mail)
+ mailer.smtp_send(
+ self.config["smtp_host"],
+ self.config["smtp_port"],
+ metadata["mailfrom"],
+ recipient,
+ mail)
except Exception as e:
- raise RuntimeError("error while sending email to '{}': {}".format(recipient, e))
+ raise RuntimeError(
+ "error while sending email to '{}': {}".format(
+ recipient, e))
self.delete(quarantine_id, recipient)
diff --git a/pyquarantine/run.py b/pyquarantine/run.py
index 4839b43..1109744 100644
--- a/pyquarantine/run.py
+++ b/pyquarantine/run.py
@@ -4,12 +4,12 @@
# 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
+# 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 .
#
@@ -26,13 +26,27 @@ import pyquarantine
def main():
"Run PyQuarantine-Milter."
# parse command line
- parser = argparse.ArgumentParser(description="PyQuarantine milter daemon",
- formatter_class=lambda prog: argparse.HelpFormatter(prog, max_help_position=45, width=140))
- parser.add_argument("-c", "--config", help="List of config files to read.", nargs="+",
- default=pyquarantine.QuarantineMilter.get_configfiles())
- parser.add_argument("-s", "--socket", help="Socket used to communicate with the MTA.", required=True)
- parser.add_argument("-d", "--debug", help="Log debugging messages.", action="store_true")
- parser.add_argument("-t", "--test", help="Check configuration.", action="store_true")
+ parser = argparse.ArgumentParser(
+ description="PyQuarantine milter daemon",
+ formatter_class=lambda prog: argparse.HelpFormatter(
+ prog, max_help_position=45, width=140))
+ parser.add_argument(
+ "-c", "--config",
+ help="List of config files to read.",
+ nargs="+",
+ default=pyquarantine.QuarantineMilter.get_configfiles())
+ parser.add_argument(
+ "-s", "--socket",
+ help="Socket used to communicate with the MTA.",
+ required=True)
+ parser.add_argument(
+ "-d", "--debug",
+ help="Log debugging messages.",
+ action="store_true")
+ parser.add_argument(
+ "-t", "--test",
+ help="Check configuration.",
+ action="store_true")
args = parser.parse_args()
# setup logging
@@ -65,11 +79,14 @@ def main():
sys.exit(255)
else:
sys.exit(0)
- formatter = logging.Formatter("%(asctime)s {}: [%(levelname)s] %(message)s".format(logname), datefmt="%Y-%m-%d %H:%M:%S")
+ formatter = logging.Formatter(
+ "%(asctime)s {}: [%(levelname)s] %(message)s".format(logname),
+ datefmt="%Y-%m-%d %H:%M:%S")
stdouthandler.setFormatter(formatter)
# setup syslog
- sysloghandler = logging.handlers.SysLogHandler(address="/dev/log", facility=logging.handlers.SysLogHandler.LOG_MAIL)
+ sysloghandler = logging.handlers.SysLogHandler(
+ address="/dev/log", facility=logging.handlers.SysLogHandler.LOG_MAIL)
sysloghandler.setLevel(loglevel)
formatter = logging.Formatter("{}: %(message)s".format(syslog_name))
sysloghandler.setFormatter(formatter)
@@ -89,12 +106,13 @@ def main():
# register to have the Milter factory create instances of your class:
Milter.factory = pyquarantine.QuarantineMilter
Milter.set_exception_policy(Milter.TEMPFAIL)
- #Milter.set_flags(0) # tell sendmail which features we use
+ # Milter.set_flags(0) # tell sendmail which features we use
# run milter
rc = 0
try:
- Milter.runmilter("pyquarantine-milter", socketname=args.socket, timeout=300)
+ Miltei.runmilter("pyquarantine-milter", socketname=args.socket,
+ timeout=300)
except Milter.milter.error as e:
logger.error(e)
rc = 255
diff --git a/pyquarantine/whitelists.py b/pyquarantine/whitelists.py
index e858adc..db8dcf1 100644
--- a/pyquarantine/whitelists.py
+++ b/pyquarantine/whitelists.py
@@ -2,34 +2,36 @@
# 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
+# 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 .
#
-import datetime
import logging
import peewee
import re
import sys
+from datetime import datetime
from playhouse.db_url import connect
class WhitelistBase(object):
"Whitelist base class"
+
def __init__(self, global_config, config, configtest=False):
self.global_config = global_config
self.config = config
self.configtest = configtest
self.name = config["name"]
self.logger = logging.getLogger(__name__)
- self.valid_entry_regex = re.compile(r"^[a-zA-Z0-9_.+-]*?(@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+)?$")
+ self.valid_entry_regex = re.compile(
+ r"^[a-zA-Z0-9_.+-]*?(@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+)?$")
def check(self, mailfrom, recipient):
"Check if mailfrom/recipient combination is whitelisted."
@@ -56,15 +58,16 @@ class WhitelistBase(object):
class WhitelistModel(peewee.Model):
mailfrom = peewee.CharField()
recipient = peewee.CharField()
- created = peewee.DateTimeField(default=datetime.datetime.now)
- last_used = peewee.DateTimeField(default=datetime.datetime.now)
+ created = peewee.DateTimeField(default=datetime.now)
+ last_used = peewee.DateTimeField(default=datetime.now)
comment = peewee.TextField(default="")
permanent = peewee.BooleanField(default=False)
class Meta(object):
indexes = (
- (('mailfrom', 'recipient'), True), # trailing comma is mandatory if only one index should be created
+ # trailing comma is mandatory if only one index should be created
+ (('mailfrom', 'recipient'), True),
)
@@ -74,14 +77,21 @@ class DatabaseWhitelist(WhitelistBase):
_db_tables = {}
def __init__(self, global_config, config, configtest=False):
- super(DatabaseWhitelist, self).__init__(global_config, config, configtest)
+ super(
+ DatabaseWhitelist,
+ self).__init__(
+ global_config,
+ config,
+ configtest)
# check if mandatory options are present in config
for option in ["whitelist_db_connection", "whitelist_db_table"]:
if option not in self.config.keys() and option in self.global_config.keys():
self.config[option] = self.global_config[option]
if option not in self.config.keys():
- raise RuntimeError("mandatory option '{}' not present in config section '{}' or 'global'".format(option, self.name))
+ raise RuntimeError(
+ "mandatory option '{}' not present in config section '{}' or 'global'".format(
+ option, self.name))
tablename = self.config["whitelist_db_table"]
connection_string = self.config["whitelist_db_connection"]
@@ -91,10 +101,16 @@ class DatabaseWhitelist(WhitelistBase):
else:
try:
# connect to database
- self.logger.debug("connecting to database '{}'".format(re.sub(r"(.*?://.*?):.*?(@.*)", r"\1:\2", connection_string)))
+ self.logger.debug(
+ "connecting to database '{}'".format(
+ re.sub(
+ r"(.*?://.*?):.*?(@.*)",
+ r"\1:\2",
+ connection_string)))
db = connect(connection_string)
except Exception as e:
- raise RuntimeError("unable to connect to database: {}".format(e))
+ raise RuntimeError(
+ "unable to connect to database: {}".format(e))
DatabaseWhitelist._db_connections[connection_string] = db
@@ -103,7 +119,7 @@ class DatabaseWhitelist(WhitelistBase):
self.meta.database = db
self.meta.table_name = tablename
self.model = type("WhitelistModel_{}".format(self.name), (WhitelistModel,), {
- "Meta": self.meta
+ "Meta": self.meta
})
if connection_string not in DatabaseWhitelist._db_tables.keys():
@@ -115,7 +131,9 @@ class DatabaseWhitelist(WhitelistBase):
try:
db.create_tables([self.model])
except Exception as e:
- raise RuntimeError("unable to initialize table '{}': {}".format(tablename, e))
+ raise RuntimeError(
+ "unable to initialize table '{}': {}".format(
+ tablename, e))
def _entry_to_dict(self, entry):
result = {}
@@ -144,7 +162,9 @@ class DatabaseWhitelist(WhitelistBase):
super(DatabaseWhitelist, self).check(mailfrom, recipient)
# generate list of possible mailfroms
- self.logger.debug("query database for whitelist entries from <{}> to <{}>".format(mailfrom, recipient))
+ self.logger.debug(
+ "query database for whitelist entries from <{}> to <{}>".format(
+ mailfrom, recipient))
mailfroms = [""]
if "@" in mailfrom and not mailfrom.startswith("@"):
mailfroms.append("@{}".format(mailfrom.split("@")[1]))
@@ -158,7 +178,10 @@ class DatabaseWhitelist(WhitelistBase):
# query the database
try:
- entries = list(self.model.select().where(self.model.mailfrom.in_(mailfroms), self.model.recipient.in_(recipients)))
+ entries = list(
+ self.model.select().where(
+ self.model.mailfrom.in_(mailfroms),
+ self.model.recipient.in_(recipients)))
except Exception as e:
raise RuntimeError("unable to query database: {}".format(e))
@@ -171,7 +194,7 @@ class DatabaseWhitelist(WhitelistBase):
# use entry with the highest weight
entry = entries[0]
- entry.last_used = datetime.datetime.now()
+ entry.last_used = datetime.now()
entry.save()
result = {}
for entry in entries:
@@ -183,21 +206,23 @@ class DatabaseWhitelist(WhitelistBase):
"Find whitelist entries."
super(DatabaseWhitelist, self).find(mailfrom, recipients, older_than)
- if type(mailfrom) == str: mailfrom = [mailfrom]
- if type(recipients) == str: recipients = [recipients]
+ if isinstance(mailfrom, str):
+ mailfrom = [mailfrom]
+ if isinstance(recipients, str):
+ recipients = [recipients]
entries = {}
try:
for entry in list(self.model.select()):
- if older_than != None:
- if (datetime.datetime.now() - entry.last_used).total_seconds() < (older_than * 24 * 3600):
+ if older_than is not None:
+ if (datetime.now() - entry.last_used).total_seconds() < (older_than * 86400):
continue
- if mailfrom != None:
+ if mailfrom is not None:
if entry.mailfrom not in mailfrom:
continue
- if recipients != None:
+ if recipients is not None:
if entry.recipient not in recipients:
continue
@@ -209,10 +234,20 @@ class DatabaseWhitelist(WhitelistBase):
def add(self, mailfrom, recipient, comment, permanent):
"Add entry to whitelist."
- super(DatabaseWhitelist, self).add(mailfrom, recipient, comment, permanent)
+ super(
+ DatabaseWhitelist,
+ self).add(
+ mailfrom,
+ recipient,
+ comment,
+ permanent)
try:
- self.model.create(mailfrom=mailfrom, recipient=recipient, comment=comment, permanent=permanent)
+ self.model.create(
+ mailfrom=mailfrom,
+ recipient=recipient,
+ comment=comment,
+ permanent=permanent)
except Exception as e:
raise RuntimeError("unable to add entry to database: {}".format(e))
@@ -224,7 +259,8 @@ class DatabaseWhitelist(WhitelistBase):
query = self.model.delete().where(self.model.id == whitelist_id)
deleted = query.execute()
except Exception as e:
- raise RuntimeError("unable to delete entry from database: {}".format(e))
+ raise RuntimeError(
+ "unable to delete entry from database: {}".format(e))
if deleted == 0:
raise RuntimeError("invalid whitelist id")
@@ -239,15 +275,19 @@ class WhitelistCache(object):
self.check(whitelist, mailfrom, recipient)
def check(self, whitelist, mailfrom, recipient):
- if whitelist not in self.cache.keys(): self.cache[whitelist] = {}
- if recipient not in self.cache[whitelist].keys(): self.cache[whitelist][recipient] = None
- if self.cache[whitelist][recipient] == None:
- self.cache[whitelist][recipient] = whitelist.check(mailfrom, recipient)
+ if whitelist not in self.cache.keys():
+ self.cache[whitelist] = {}
+ if recipient not in self.cache[whitelist].keys():
+ self.cache[whitelist][recipient] = None
+ if self.cache[whitelist][recipient] is None:
+ self.cache[whitelist][recipient] = whitelist.check(
+ mailfrom, recipient)
return self.cache[whitelist][recipient]
def get_whitelisted_recipients(self, whitelist, mailfrom, recipients):
self.load(whitelist, mailfrom, recipients)
- return list(filter(lambda x: self.cache[whitelist][x], self.cache[whitelist].keys()))
+ return list(
+ filter(lambda x: self.cache[whitelist][x], self.cache[whitelist].keys()))
# list of whitelist types and their related whitelist classes