From 89a01d92c8a8bc2760995d38ddd5132adfdf8bc7 Mon Sep 17 00:00:00 2001 From: Thomas Oettli Date: Thu, 17 Oct 2019 22:25:10 +0200 Subject: [PATCH] Make source PEP8 conform --- pyquarantine/__init__.py | 295 ++++++++++++++++++-------- pyquarantine/cli.py | 379 +++++++++++++++++++++++++--------- pyquarantine/mailer.py | 29 ++- pyquarantine/notifications.py | 268 +++++++++++++++--------- pyquarantine/quarantines.py | 112 ++++++---- pyquarantine/run.py | 46 +++-- pyquarantine/whitelists.py | 102 ++++++--- 7 files changed, 865 insertions(+), 366 deletions(-) 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