diff --git a/pyquarantine/__init__.py b/pyquarantine/__init__.py index cebfe38..8725185 100644 --- a/pyquarantine/__init__.py +++ b/pyquarantine/__init__.py @@ -12,6 +12,24 @@ # along with PyQuarantineMilter. If not, see . # +import Milter +import configparser +import logging +import os +import re + +from Milter.utils import parse_addr +from collections import defaultdict +from email.policy import default as default_policy +from email.parser import BytesHeaderParser +from io import BytesIO +from itertools import groupby +from netaddr import IPAddress, IPNetwork, AddrFormatError +from pyquarantine import mailer +from pyquarantine import notifications +from pyquarantine import storages +from pyquarantine import whitelists + __all__ = [ "Quarantine", "QuarantineMilter", @@ -25,32 +43,12 @@ __all__ = [ "version", "whitelists"] -name = "pyquarantine" - -import Milter -import configparser -import logging -import os -import re -import sys - -from Milter.utils import parse_addr -from collections import defaultdict -from email.policy import default as default_policy -from email.parser import BytesHeaderParser -from io import BytesIO -from itertools import groupby -from netaddr import IPAddress, IPNetwork -from pyquarantine import mailer -from pyquarantine import notifications -from pyquarantine import storages -from pyquarantine import whitelists - class Quarantine(object): """Quarantine class suitable for QuarantineMilter - The class holds all the objects and functions needed for QuarantineMilter quarantine. + The class holds all the objects and functions needed + for QuarantineMilter quarantine. """ @@ -61,8 +59,8 @@ class Quarantine(object): "DISCARD": Milter.DISCARD} def __init__(self, name, index=0, regex=None, storage=None, whitelist=None, - host_whitelist=[], notification=None, action="ACCEPT", - reject_reason=None): + host_whitelist=[], notification=None, action="ACCEPT", + reject_reason=None): self.logger = logging.getLogger(__name__) self.name = name self.index = index @@ -98,7 +96,8 @@ class Quarantine(object): cfg[opt] = defaults[opt] else: raise RuntimeError( - f"mandatory option '{opt}' not present in config section '{self.name}' or 'global'") + f"mandatory option '{opt}' not present in " + f"config section '{self.name}' or 'global'") # pre-compile regex self.logger.debug( @@ -113,7 +112,8 @@ class Quarantine(object): storage_type = cfg["storage_type"].lower() if storage_type in storages.TYPES: self.logger.debug( - f"{self.name}: initializing storage type '{storage_type.upper()}'") + f"{self.name}: initializing storage " + f"type '{storage_type.upper()}'") self.storage = storages.TYPES[storage_type]( self.name, global_cfg, cfg, test) elif storage_type == "none": @@ -127,11 +127,12 @@ class Quarantine(object): whitelist_type = cfg["whitelist_type"].lower() if whitelist_type in whitelists.TYPES: self.logger.debug( - f"{self.name}: initializing whitelist type '{whitelist_type.upper()}'") + f"{self.name}: initializing whitelist " + f"type '{whitelist_type.upper()}'") self.whitelist = whitelists.TYPES[whitelist_type]( self.name, global_cfg, cfg, test) elif whitelist_type == "none": - logger.debug(f"{self.name}: whitelist is NONE") + self.logger.debug(f"{self.name}: whitelist is NONE") self.whitelist = None else: raise RuntimeError( @@ -141,7 +142,8 @@ class Quarantine(object): notification_type = cfg["notification_type"].lower() if notification_type in notifications.TYPES: self.logger.debug( - f"{self.name}: initializing notification type '{notification_type.upper()}'") + f"{self.name}: initializing notification " + f"type '{notification_type.upper()}'") self.notification = notifications.TYPES[notification_type]( self.name, global_cfg, cfg, test) elif notification_type == "none": @@ -149,7 +151,8 @@ class Quarantine(object): self.notification = None else: raise RuntimeError( - f"{self.name}: unknown notification type '{notification_type}'") + f"{self.name}: unknown notification " + f"type '{notification_type}'") # determining milter action for this quarantine action = cfg["action"].upper() @@ -166,7 +169,7 @@ class Quarantine(object): # create host/network whitelist self.host_whitelist = [] host_whitelist = set([p.strip() - for p in cfg["host_whitelist"].split(",") if p]) + for p in cfg["host_whitelist"].split(",") if p]) for host in host_whitelist: if not host: continue @@ -174,7 +177,8 @@ class Quarantine(object): try: net = IPNetwork(host) except AddrFormatError as e: - raise RuntimeError(f"{self.name}: error parsing host_whitelist: {e}") + raise RuntimeError( + f"{self.name}: error parsing host_whitelist: {e}") else: self.host_whitelist.append(net) if self.host_whitelist: @@ -190,7 +194,8 @@ class Quarantine(object): if not self.notification: raise RuntimeError( - "notification type is set to None, unable to send notification") + "notification type is set to None, " + "unable to send notification") fp, metadata = self.storage.get_mail(storage_id) @@ -255,7 +260,7 @@ class Quarantine(object): ip = IPAddress(hostaddr[0]) for entry in self.host_whitelist: if ip in entry: - return true + return True return False def match(self, header): @@ -265,11 +270,12 @@ class Quarantine(object): class QuarantineMilter(Milter.Base): """QuarantineMilter based on Milter.Base to implement milter communication - The class variable quarantines needs to be filled by runng the setup_milter function. + The class variable quarantines needs to be filled by + runng the setup_milter function. """ quarantines = [] - preferred_quarantine_action = "first" + preferred_action = "first" # list of default config files _cfg_files = [ @@ -284,8 +290,8 @@ class QuarantineMilter(Milter.Base): def _get_preferred_quarantine(self): matching_quarantines = [ - q for q in self.recipients_quarantines.values() if q] - if self.preferred_quarantine_action == "first": + q for q in self.rcpts_quarantines.values() if q] + if self.preferred_action == "first": quarantine = sorted( matching_quarantines, key=lambda q: q.index)[0] @@ -307,16 +313,18 @@ class QuarantineMilter(Milter.Base): def connect(self, hostname, family, hostaddr): self.hostaddr = hostaddr self.logger.debug( - f"accepted milter connection from {hostaddr[0]} port {hostaddr[1]}") + f"accepted milter connection from {hostaddr[0]} " + f"port {hostaddr[1]}") for quarantine in self.quarantines.copy(): if quarantine.host_in_whitelist(hostaddr): self.logger.debug( - f"host {hostaddr[0]} is in whitelist of quarantine {quarantine['name']}") + f"host {hostaddr[0]} is in whitelist of " + f"quarantine {quarantine['name']}") self.quarantines.remove(quarantine) if not self.quarantines: self.logger.debug( - f"host {hostaddr[0]} is in whitelist of all quarantines, " - f"skip further processing") + f"host {hostaddr[0]} is in whitelist of all " + f"quarantines, skip further processing") return Milter.ACCEPT return Milter.CONTINUE @@ -362,93 +370,106 @@ class QuarantineMilter(Milter.Base): self.fp.seek(0) self.headers = BytesHeaderParser( policy=default_policy).parse(self.fp).items() - self.whitelist_cache = whitelists.WhitelistCache() + self.wl_cache = whitelists.WhitelistCache() # initialize dicts to set quaranines per recipient and keep matches - self.recipients_quarantines = {} - self.quarantines_matches = {} + self.rcpts_quarantines = {} + self.matches = {} # iterate email headers - recipients_to_check = self.recipients.copy() + rcpts_to_check = self.recipients.copy() for name, value in self.headers: header = f"{name}: {value}" self.logger.debug( - f"{self.qid}: checking header against configured quarantines: {header}") + f"{self.qid}: checking header against configured " + f"quarantines: {header}") # iterate quarantines for quarantine in self.quarantines: - if len(self.recipients_quarantines) == len( + if len(self.rcpts_quarantines) == len( self.recipients): # every recipient matched a quarantine already if quarantine.index >= max( - [q.index for q in self.recipients_quarantines.values()]): + [q.index for q in + self.rcpts_quarantines.values()]): # all recipients matched a quarantine with at least # the same precedence already, skip checks against # quarantines with lower precedence self.logger.debug( - f"{self.qid}: {quarantine.name}: skip further checks of this header") + f"{self.qid}: {quarantine.name}: skip further " + f"checks of this header") break # check email header against quarantine regex self.logger.debug( - f"{self.qid}: {quarantine.name}: checking header against regex '{quarantine.regex}'") + f"{self.qid}: {quarantine.name}: checking header " + f"against regex '{quarantine.regex}'") match = quarantine.match(header) if match: self.logger.debug( - f"{self.qid}: {quarantine.name}: header matched regex") + f"{self.qid}: {quarantine.name}: " + f"header matched regex") # check for whitelisted recipients whitelist = quarantine.get_whitelist() if whitelist: try: - whitelisted_recipients = self.whitelist_cache.get_whitelisted_recipients( - whitelist, self.mailfrom, recipients_to_check) + wl_recipients = self.wl_cache.get_recipients( + whitelist, + self.mailfrom, + rcpts_to_check) except RuntimeError as e: self.logger.error( - f"{self.qid}: {quarantine.name}: unable to query whitelist: {e}") + f"{self.qid}: {quarantine.name}: unable " + f"to query whitelist: {e}") return Milter.TEMPFAIL else: - whitelisted_recipients = {} + wl_recipients = {} # iterate recipients - for recipient in recipients_to_check.copy(): - if recipient in whitelisted_recipients: + for rcpt in rcpts_to_check.copy(): + if rcpt in wl_recipients: # recipient is whitelisted in this quarantine self.logger.debug( - f"{self.qid}: {quarantine.name}: recipient '{recipient}' is whitelisted") + f"{self.qid}: {quarantine.name}: " + f"recipient '{rcpt}' is whitelisted") continue - if recipient not in self.recipients_quarantines.keys() or \ - self.recipients_quarantines[recipient].index > quarantine.index: + if rcpt not in self.rcpts_quarantines or \ + self.rcpts_quarantines[rcpt].index > \ + quarantine.index: self.logger.debug( - f"{self.qid}: {quarantine.name}: set quarantine for recipient '{recipient}'") + f"{self.qid}: {quarantine.name}: set " + f"quarantine for recipient '{rcpt}'") # save match for later use as template # variables - self.quarantines_matches[quarantine.name] = match - self.recipients_quarantines[recipient] = quarantine + self.matches[quarantine.name] = match + self.rcpts_quarantines[rcpt] = quarantine if quarantine.index == 0: # we do not need to check recipients which # matched the quarantine with the highest # precedence already - recipients_to_check.remove(recipient) + rcpts_to_check.remove(rcpt) else: self.logger.debug( - f"{self.qid}: {quarantine.name}: a quarantine with same or higher " - f"precedence matched already for recipient '{recipient}'") + f"{self.qid}: {quarantine.name}: a " + f"quarantine with same or higher " + f"precedence matched already for " + f"recipient '{rcpt}'") - if not recipients_to_check: + if not rcpts_to_check: self.logger.debug( - f"{self.qid}: all recipients matched the first quarantine, " - f"skipping all remaining header checks") + f"{self.qid}: all recipients matched the first " + f"quarantine, skipping all remaining header checks") break # check if no quarantine has matched for all recipients - if not self.recipients_quarantines: + if not self.rcpts_quarantines: # accept email self.logger.info( f"{self.qid}: passed clean for all recipients") return Milter.ACCEPT # check if the mail body is needed - for recipient, quarantine in self.recipients_quarantines.items(): + for recipient, quarantine in self.rcpts_quarantines.items(): if quarantine.get_storage() or quarantine.get_notification(): # mail body is needed, continue processing return Milter.CONTINUE @@ -457,7 +478,7 @@ class QuarantineMilter(Milter.Base): # quarantines, just return configured action quarantine = self._get_preferred_quarantine() self.logger.info( - f"{self.qid}: {self.preferred_quarantine_action} " + f"{self.qid}: {self.preferred_action} " f"matching quarantine is '{quarantine.name}', performing " f"milter action {quarantine.action}") if quarantine.action == "REJECT": @@ -484,9 +505,9 @@ 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.rcpts_quarantines, + key=lambda x: self.rcpts_quarantines[x].index), + lambda x: self.rcpts_quarantines[x]): quarantines.append((quarantine, list(recipients))) # iterate quarantines sorted by index @@ -495,9 +516,9 @@ class QuarantineMilter(Milter.Base): headers = defaultdict(str) for name, value in self.headers: headers[name.lower()] = value - subgroups = self.quarantines_matches[quarantine.name].groups( + subgroups = self.matches[quarantine.name].groups( default="") - named_subgroups = self.quarantines_matches[quarantine.name].groupdict( + named_subgroups = self.matches[quarantine.name].groupdict( default="") rcpts = ", ".join(recipients) @@ -508,14 +529,16 @@ class QuarantineMilter(Milter.Base): if storage: # add email to quarantine self.logger.info( - f"{self.qid}: adding to quarantine '{quarantine.name}' for: {rcpts}") + f"{self.qid}: adding to quarantine " + f"'{quarantine.name}' for: {rcpts}") try: storage_id = storage.add( - self.qid, self.mailfrom, recipients, headers, self.fp, - subgroups, named_subgroups) + self.qid, self.mailfrom, recipients, headers, + self.fp, subgroups, named_subgroups) except RuntimeError as e: self.logger.error( - f"{self.qid}: unable to add to quarantine '{quarantine.name}': {e}") + f"{self.qid}: unable to add to quarantine " + f"'{quarantine.name}': {e}") return Milter.TEMPFAIL # check if a notification is configured @@ -551,7 +574,7 @@ class QuarantineMilter(Milter.Base): # return configured action quarantine = self._get_preferred_quarantine() self.logger.info( - f"{self.qid}: {self.preferred_quarantine_action} matching " + f"{self.qid}: {self.preferred_action} matching " f"quarantine is '{quarantine.name}', performing milter " f"action {quarantine.action}") if quarantine.action == "REJECT": @@ -589,15 +612,16 @@ def setup_milter(test=False, cfg_files=[]): if "global" not in parser.sections(): raise RuntimeError( "mandatory section 'global' not present in config file") - for option in ["quarantines", "preferred_quarantine_action"]: + for option in ["quarantines", "preferred_action"]: if not parser.has_option("global", option): raise RuntimeError( - f"mandatory option '{option}' not present in config section 'global'") + f"mandatory option '{option}' not present in config " + f"section 'global'") # read global config section global_cfg = dict(parser.items("global")) - preferred_quarantine_action = global_cfg["preferred_quarantine_action"].lower() - if preferred_quarantine_action not in ["first", "last"]: + preferred_action = global_cfg["preferred_quarantine_action"].lower() + if preferred_action not in ["first", "last"]: raise RuntimeError( "option preferred_quarantine_action has illegal value") @@ -606,11 +630,13 @@ def setup_milter(test=False, cfg_files=[]): q.strip() for q in global_cfg["quarantines"].split(",")] if len(quarantines) != len(set(quarantines)): raise RuntimeError( - "at least one quarantine is specified multiple times in quarantines option") + "at least one quarantine is specified multiple times " + "in quarantines option") if "global" in quarantines: quarantines.remove("global") logger.warning( - "removed illegal quarantine name 'global' from list of active quarantines") + "removed illegal quarantine name 'global' from list of " + "active quarantines") if not quarantines: raise RuntimeError("no quarantines configured") @@ -628,7 +654,7 @@ def setup_milter(test=False, cfg_files=[]): quarantine.setup_from_cfg(global_cfg, cfg, test) milter_quarantines.append(quarantine) - QuarantineMilter.preferred_quarantine_action = preferred_quarantine_action + QuarantineMilter.preferred_action = preferred_action QuarantineMilter.quarantines = milter_quarantines diff --git a/pyquarantine/cli.py b/pyquarantine/cli.py index edc8e1c..cdf901d 100644 --- a/pyquarantine/cli.py +++ b/pyquarantine/cli.py @@ -20,11 +20,10 @@ import logging.handlers import sys import time -from email.header import decode_header - from pyquarantine import QuarantineMilter, setup_milter from pyquarantine.version import __version__ as version + def _get_quarantine(quarantines, name): try: quarantine = next((q for q in quarantines if q.name == name)) @@ -32,6 +31,7 @@ def _get_quarantine(quarantines, name): raise RuntimeError(f"invalid quarantine 'name'") return quarantine + def _get_storage(quarantines, name): quarantine = _get_quarantine(quarantines, name) storage = quarantine.get_storage() @@ -40,6 +40,7 @@ def _get_storage(quarantines, name): "storage type is set to NONE") return storage + def _get_notification(quarantines, name): quarantine = _get_quarantine(quarantines, name) notification = quarantine.get_notification() @@ -48,6 +49,7 @@ def _get_notification(quarantines, name): "notification type is set to NONE") return notification + def _get_whitelist(quarantines, name): quarantine = _get_quarantine(quarantines, name) whitelist = quarantine.get_whitelist() @@ -56,6 +58,7 @@ def _get_whitelist(quarantines, name): "whitelist type is set to NONE") return whitelist + def print_table(columns, rows): if not rows: return diff --git a/pyquarantine/mailer.py b/pyquarantine/mailer.py index 83791ae..803dd64 100644 --- a/pyquarantine/mailer.py +++ b/pyquarantine/mailer.py @@ -14,7 +14,6 @@ import logging import smtplib -import sys from multiprocessing import Process, Queue @@ -50,7 +49,8 @@ def mailprocess(): smtp_send(smtp_host, smtp_port, mailfrom, recipient, mail) except Exception as e: logger.error( - f"{qid}: error while sending {emailtype} to '{recipient}': {e}") + f"{qid}: error while sending {emailtype} " + f"to '{recipient}': {e}") else: logger.info( f"{qid}: successfully sent {emailtype} to: {recipient}") @@ -82,5 +82,5 @@ def sendmail(smtp_host, smtp_port, qid, mailfrom, recipients, mail, (smtp_host, smtp_port, qid, mailfrom, recipient, mail, emailtype), timeout=30) - except Queue.Full as e: + except Queue.Full: raise RuntimeError("email queue is full") diff --git a/pyquarantine/notifications.py b/pyquarantine/notifications.py index 8c5eaf2..c163406 100644 --- a/pyquarantine/notifications.py +++ b/pyquarantine/notifications.py @@ -20,7 +20,6 @@ from bs4 import BeautifulSoup from cgi import escape from collections import defaultdict from email import policy -from email.header import decode_header from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from email.mime.image import MIMEImage @@ -136,7 +135,8 @@ class EMailNotification(BaseNotification): cfg[opt] = defaults[opt] else: raise RuntimeError( - f"mandatory option '{opt}' not present in config section '{self.name}' or 'global'") + f"mandatory option '{opt}' not present in config " + f"section '{self.name}' or 'global'") self.smtp_host = cfg["notification_email_smtp_host"] self.smtp_port = cfg["notification_email_smtp_port"] @@ -176,11 +176,13 @@ class EMailNotification(BaseNotification): elif strip_images in ["FALSE", "OFF", "NO"]: self.strip_images = False else: - raise RuntimeError("error parsing notification_email_strip_images: unknown value") + raise RuntimeError( + "error parsing notification_email_strip_images: unknown value") self.parser_lib = cfg["notification_email_parser_lib"].strip() if self.parser_lib not in ["lxml", "html.parser"]: - raise RuntimeError("error parsing notification_email_parser_lib: unknown value") + raise RuntimeError( + "error parsing notification_email_parser_lib: unknown value") # read email replacement image if specified replacement_img = cfg["notification_email_replacement_img"].strip() @@ -198,8 +200,10 @@ class EMailNotification(BaseNotification): self.replacement_img = None # read images to embed if specified - embedded_img_paths = [ - p.strip() for p in cfg["notification_email_embedded_imgs"].split(",") if p] + embedded_img_paths = [] + for p in cfg["notification_email_embedded_imgs"].split(","): + if p: + embedded_img_paths.append(p.strip()) self.embedded_imgs = [] for img_path in embedded_img_paths: # read image @@ -219,7 +223,9 @@ class EMailNotification(BaseNotification): try: body = msg.get_body(preferencelist=("html", "plain")) except Exception as e: - self.logger.error(f"{qid}: an error occured in email.message.EmailMessage.get_body: {e}") + self.logger.error( + f"{qid}: an error occured in " + f"email.message.EmailMessage.get_body: {e}") body = None if body: @@ -228,13 +234,16 @@ class EMailNotification(BaseNotification): try: content = content.decode(encoding=charset, errors="replace") except LookupError: - self.logger.info(f"{qid}: unknown encoding '{charset}', falling back to UTF-8") + self.logger.info( + f"{qid}: unknown encoding '{charset}', " + f"falling back to UTF-8") content = content.decode("utf-8", errors="replace") content_type = body.get_content_type() if content_type == "text/plain": # convert text/plain to text/html self.logger.debug( - f"{qid}: content type is {content_type}, converting to text/html") + f"{qid}: content type is {content_type}, " + f"converting to text/html") content = re.sub(r"^(.*)$", r"\1
", escape(content), flags=re.MULTILINE) else: @@ -248,7 +257,8 @@ class EMailNotification(BaseNotification): # create BeautifulSoup object length = len(content) self.logger.debug( - f"{qid}: trying to create BeatufilSoup object with parser lib {self.parser_lib}, " + f"{qid}: trying to create BeatufilSoup object with " + f"parser lib {self.parser_lib}, " f"text length is {length} bytes") soup = BeautifulSoup(content, self.parser_lib) self.logger.debug( @@ -263,7 +273,8 @@ class EMailNotification(BaseNotification): # completly remove bad elements for element in soup(EMailNotification._bad_tags): self.logger.debug( - f"{qid}: removing dangerous tag '{element.name}' and its content") + f"{qid}: removing dangerous tag '{element.name}' " + f"and its content") element.extract() # remove not whitelisted elements, but keep their content @@ -279,11 +290,13 @@ class EMailNotification(BaseNotification): if attribute not in EMailNotification._good_attributes: if element.name == "a" and attribute == "href": self.logger.debug( - f"{qid}: setting attribute href to '#' on tag '{element.name}'") + f"{qid}: setting attribute href to '#' " + f"on tag '{element.name}'") element["href"] = "#" else: self.logger.debug( - f"{qid}: removing attribute '{attribute}' from tag '{element.name}'") + f"{qid}: removing attribute '{attribute}' " + f"from tag '{element.name}'") del(element.attrs[attribute]) return soup @@ -338,16 +351,17 @@ class EMailNotification(BaseNotification): self.logger.debug(f"{qid}: parsing email template") # 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_ENVELOPE_FROM_URL=escape(quote(mailfrom)), - EMAIL_TO=escape(headers["to"]), - EMAIL_ENVELOPE_TO=escape(recipient), - EMAIL_ENVELOPE_TO_URL=escape(quote(recipient)), - EMAIL_SUBJECT=escape(headers["subject"]), - EMAIL_QUARANTINE_ID=storage_id) + variables = defaultdict( + str, + EMAIL_HTML_TEXT=sanitized_text, + EMAIL_FROM=escape(headers["from"]), + EMAIL_ENVELOPE_FROM=escape(mailfrom), + EMAIL_ENVELOPE_FROM_URL=escape(quote(mailfrom)), + EMAIL_TO=escape(headers["to"]), + EMAIL_ENVELOPE_TO=escape(recipient), + EMAIL_ENVELOPE_TO_URL=escape(quote(recipient)), + EMAIL_SUBJECT=escape(headers["subject"]), + EMAIL_QUARANTINE_ID=storage_id) if subgroups: number = 0 diff --git a/pyquarantine/run.py b/pyquarantine/run.py index 876fac0..d9b507b 100644 --- a/pyquarantine/run.py +++ b/pyquarantine/run.py @@ -24,6 +24,7 @@ import pyquarantine from pyquarantine.version import __version__ as version + def main(): "Run PyQuarantine-Milter." # parse command line diff --git a/pyquarantine/storages.py b/pyquarantine/storages.py index 63f0861..8114473 100644 --- a/pyquarantine/storages.py +++ b/pyquarantine/storages.py @@ -73,14 +73,16 @@ class FileMailStorage(BaseMailStorage): cfg[opt] = defaults[opt] else: raise RuntimeError( - f"mandatory option '{opt}' not present in config section '{self.name}' or 'global'") + f"mandatory option '{opt}' not present in config " + f"section '{self.name}' or 'global'") self.directory = cfg["storage_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( - f"file quarantine directory '{self.directory}' does not exist or is not writable") + f"file quarantine directory '{self.directory}' does " + f"not exist or is not writable") self._metadata_suffix = ".metadata" def _save_datafile(self, storage_id, fp): @@ -203,7 +205,8 @@ class FileMailStorage(BaseMailStorage): if len(recipients) == 1 and \ recipients[0] not in metadata["recipients"]: continue - elif len(set(recipients + metadata["recipients"])) == len(recipients + metadata["recipients"]): + elif len(set(recipients + metadata["recipients"])) == \ + len(recipients + metadata["recipients"]): continue emails[storage_id] = metadata diff --git a/pyquarantine/whitelists.py b/pyquarantine/whitelists.py index e182bb4..014b67f 100644 --- a/pyquarantine/whitelists.py +++ b/pyquarantine/whitelists.py @@ -15,7 +15,6 @@ import logging import peewee import re -import sys from datetime import datetime from playhouse.db_url import connect @@ -88,7 +87,8 @@ class DatabaseWhitelist(WhitelistBase): defaults = {} # check config - for opt in ["whitelist_db_connection", "whitelist_db_table"] + list(defaults.keys()): + for opt in ["whitelist_db_connection", + "whitelist_db_table"] + list(defaults.keys()): if opt in cfg: continue if opt in global_cfg: @@ -97,7 +97,8 @@ class DatabaseWhitelist(WhitelistBase): cfg[opt] = defaults[opt] else: raise RuntimeError( - f"mandatory option '{opt}' not present in config section '{self.name}' or 'global'") + f"mandatory option '{opt}' not present in config " + f"section '{self.name}' or 'global'") tablename = cfg["whitelist_db_table"] connection_string = cfg["whitelist_db_connection"] @@ -168,7 +169,8 @@ class DatabaseWhitelist(WhitelistBase): # generate list of possible mailfroms self.logger.debug( - f"query database for whitelist entries from <{mailfrom}> to <{recipient}>") + f"query database for whitelist entries from <{mailfrom}> " + f"to <{recipient}>") mailfroms = [""] if "@" in mailfrom and not mailfrom.startswith("@"): domain = mailfrom.split("@")[1] @@ -221,7 +223,8 @@ class DatabaseWhitelist(WhitelistBase): try: for entry in list(self.model.select()): if older_than is not None: - if (datetime.now() - entry.last_used).total_seconds() < (older_than * 86400): + delta = (datetime.now() - entry.last_used).total_seconds() + if delta < (older_than * 86400): continue if mailfrom is not None: @@ -290,10 +293,11 @@ class WhitelistCache(object): mailfrom, recipient) return self.cache[whitelist][recipient] - def get_whitelisted_recipients(self, whitelist, mailfrom, recipients): + def get_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