Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
4c1b110d18
|
|||
|
c7a027a4d8
|
|||
| 65d5dcf137 | |||
|
567e41362b
|
|||
|
0fa6ddd870
|
@@ -12,6 +12,24 @@
|
|||||||
# along with PyQuarantineMilter. If not, see <http://www.gnu.org/licenses/>.
|
# along with PyQuarantineMilter. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
|
||||||
|
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__ = [
|
__all__ = [
|
||||||
"Quarantine",
|
"Quarantine",
|
||||||
"QuarantineMilter",
|
"QuarantineMilter",
|
||||||
@@ -25,32 +43,12 @@ __all__ = [
|
|||||||
"version",
|
"version",
|
||||||
"whitelists"]
|
"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):
|
class Quarantine(object):
|
||||||
"""Quarantine class suitable for QuarantineMilter
|
"""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}
|
"DISCARD": Milter.DISCARD}
|
||||||
|
|
||||||
def __init__(self, name, index=0, regex=None, storage=None, whitelist=None,
|
def __init__(self, name, index=0, regex=None, storage=None, whitelist=None,
|
||||||
host_whitelist=[], notification=None, action="ACCEPT",
|
host_whitelist=[], notification=None, action="ACCEPT",
|
||||||
reject_reason=None):
|
reject_reason=None):
|
||||||
self.logger = logging.getLogger(__name__)
|
self.logger = logging.getLogger(__name__)
|
||||||
self.name = name
|
self.name = name
|
||||||
self.index = index
|
self.index = index
|
||||||
@@ -98,7 +96,8 @@ class Quarantine(object):
|
|||||||
cfg[opt] = defaults[opt]
|
cfg[opt] = defaults[opt]
|
||||||
else:
|
else:
|
||||||
raise RuntimeError(
|
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
|
# pre-compile regex
|
||||||
self.logger.debug(
|
self.logger.debug(
|
||||||
@@ -113,7 +112,8 @@ class Quarantine(object):
|
|||||||
storage_type = cfg["storage_type"].lower()
|
storage_type = cfg["storage_type"].lower()
|
||||||
if storage_type in storages.TYPES:
|
if storage_type in storages.TYPES:
|
||||||
self.logger.debug(
|
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.storage = storages.TYPES[storage_type](
|
||||||
self.name, global_cfg, cfg, test)
|
self.name, global_cfg, cfg, test)
|
||||||
elif storage_type == "none":
|
elif storage_type == "none":
|
||||||
@@ -127,11 +127,12 @@ class Quarantine(object):
|
|||||||
whitelist_type = cfg["whitelist_type"].lower()
|
whitelist_type = cfg["whitelist_type"].lower()
|
||||||
if whitelist_type in whitelists.TYPES:
|
if whitelist_type in whitelists.TYPES:
|
||||||
self.logger.debug(
|
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.whitelist = whitelists.TYPES[whitelist_type](
|
||||||
self.name, global_cfg, cfg, test)
|
self.name, global_cfg, cfg, test)
|
||||||
elif whitelist_type == "none":
|
elif whitelist_type == "none":
|
||||||
logger.debug(f"{self.name}: whitelist is NONE")
|
self.logger.debug(f"{self.name}: whitelist is NONE")
|
||||||
self.whitelist = None
|
self.whitelist = None
|
||||||
else:
|
else:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
@@ -141,7 +142,8 @@ class Quarantine(object):
|
|||||||
notification_type = cfg["notification_type"].lower()
|
notification_type = cfg["notification_type"].lower()
|
||||||
if notification_type in notifications.TYPES:
|
if notification_type in notifications.TYPES:
|
||||||
self.logger.debug(
|
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.notification = notifications.TYPES[notification_type](
|
||||||
self.name, global_cfg, cfg, test)
|
self.name, global_cfg, cfg, test)
|
||||||
elif notification_type == "none":
|
elif notification_type == "none":
|
||||||
@@ -149,7 +151,8 @@ class Quarantine(object):
|
|||||||
self.notification = None
|
self.notification = None
|
||||||
else:
|
else:
|
||||||
raise RuntimeError(
|
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
|
# determining milter action for this quarantine
|
||||||
action = cfg["action"].upper()
|
action = cfg["action"].upper()
|
||||||
@@ -166,7 +169,7 @@ class Quarantine(object):
|
|||||||
# create host/network whitelist
|
# create host/network whitelist
|
||||||
self.host_whitelist = []
|
self.host_whitelist = []
|
||||||
host_whitelist = set([p.strip()
|
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:
|
for host in host_whitelist:
|
||||||
if not host:
|
if not host:
|
||||||
continue
|
continue
|
||||||
@@ -174,7 +177,8 @@ class Quarantine(object):
|
|||||||
try:
|
try:
|
||||||
net = IPNetwork(host)
|
net = IPNetwork(host)
|
||||||
except AddrFormatError as e:
|
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:
|
else:
|
||||||
self.host_whitelist.append(net)
|
self.host_whitelist.append(net)
|
||||||
if self.host_whitelist:
|
if self.host_whitelist:
|
||||||
@@ -190,7 +194,8 @@ class Quarantine(object):
|
|||||||
|
|
||||||
if not self.notification:
|
if not self.notification:
|
||||||
raise RuntimeError(
|
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)
|
fp, metadata = self.storage.get_mail(storage_id)
|
||||||
|
|
||||||
@@ -255,7 +260,7 @@ class Quarantine(object):
|
|||||||
ip = IPAddress(hostaddr[0])
|
ip = IPAddress(hostaddr[0])
|
||||||
for entry in self.host_whitelist:
|
for entry in self.host_whitelist:
|
||||||
if ip in entry:
|
if ip in entry:
|
||||||
return true
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def match(self, header):
|
def match(self, header):
|
||||||
@@ -265,11 +270,12 @@ class Quarantine(object):
|
|||||||
class QuarantineMilter(Milter.Base):
|
class QuarantineMilter(Milter.Base):
|
||||||
"""QuarantineMilter based on Milter.Base to implement milter communication
|
"""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 = []
|
quarantines = []
|
||||||
preferred_quarantine_action = "first"
|
preferred_action = "first"
|
||||||
|
|
||||||
# list of default config files
|
# list of default config files
|
||||||
_cfg_files = [
|
_cfg_files = [
|
||||||
@@ -284,8 +290,8 @@ class QuarantineMilter(Milter.Base):
|
|||||||
|
|
||||||
def _get_preferred_quarantine(self):
|
def _get_preferred_quarantine(self):
|
||||||
matching_quarantines = [
|
matching_quarantines = [
|
||||||
q for q in self.recipients_quarantines.values() if q]
|
q for q in self.rcpts_quarantines.values() if q]
|
||||||
if self.preferred_quarantine_action == "first":
|
if self.preferred_action == "first":
|
||||||
quarantine = sorted(
|
quarantine = sorted(
|
||||||
matching_quarantines,
|
matching_quarantines,
|
||||||
key=lambda q: q.index)[0]
|
key=lambda q: q.index)[0]
|
||||||
@@ -307,16 +313,18 @@ class QuarantineMilter(Milter.Base):
|
|||||||
def connect(self, hostname, family, hostaddr):
|
def connect(self, hostname, family, hostaddr):
|
||||||
self.hostaddr = hostaddr
|
self.hostaddr = hostaddr
|
||||||
self.logger.debug(
|
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():
|
for quarantine in self.quarantines.copy():
|
||||||
if quarantine.host_in_whitelist(hostaddr):
|
if quarantine.host_in_whitelist(hostaddr):
|
||||||
self.logger.debug(
|
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)
|
self.quarantines.remove(quarantine)
|
||||||
if not self.quarantines:
|
if not self.quarantines:
|
||||||
self.logger.debug(
|
self.logger.debug(
|
||||||
f"host {hostaddr[0]} is in whitelist of all quarantines, "
|
f"host {hostaddr[0]} is in whitelist of all "
|
||||||
f"skip further processing")
|
f"quarantines, skip further processing")
|
||||||
return Milter.ACCEPT
|
return Milter.ACCEPT
|
||||||
return Milter.CONTINUE
|
return Milter.CONTINUE
|
||||||
|
|
||||||
@@ -362,93 +370,106 @@ class QuarantineMilter(Milter.Base):
|
|||||||
self.fp.seek(0)
|
self.fp.seek(0)
|
||||||
self.headers = BytesHeaderParser(
|
self.headers = BytesHeaderParser(
|
||||||
policy=default_policy).parse(self.fp).items()
|
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
|
# initialize dicts to set quaranines per recipient and keep matches
|
||||||
self.recipients_quarantines = {}
|
self.rcpts_quarantines = {}
|
||||||
self.quarantines_matches = {}
|
self.matches = {}
|
||||||
|
|
||||||
# iterate email headers
|
# iterate email headers
|
||||||
recipients_to_check = self.recipients.copy()
|
rcpts_to_check = self.recipients.copy()
|
||||||
for name, value in self.headers:
|
for name, value in self.headers:
|
||||||
header = f"{name}: {value}"
|
header = f"{name}: {value}"
|
||||||
self.logger.debug(
|
self.logger.debug(
|
||||||
f"{self.qid}: checking header against configured quarantines: {header}")
|
f"{self.qid}: checking header against configured "
|
||||||
|
f"quarantines: {header}")
|
||||||
# iterate quarantines
|
# iterate quarantines
|
||||||
for quarantine in self.quarantines:
|
for quarantine in self.quarantines:
|
||||||
if len(self.recipients_quarantines) == len(
|
if len(self.rcpts_quarantines) == len(
|
||||||
self.recipients):
|
self.recipients):
|
||||||
# every recipient matched a quarantine already
|
# every recipient matched a quarantine already
|
||||||
if quarantine.index >= max(
|
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
|
# all recipients matched a quarantine with at least
|
||||||
# the same precedence already, skip checks against
|
# the same precedence already, skip checks against
|
||||||
# quarantines with lower precedence
|
# quarantines with lower precedence
|
||||||
self.logger.debug(
|
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
|
break
|
||||||
|
|
||||||
# check email header against quarantine regex
|
# check email header against quarantine regex
|
||||||
self.logger.debug(
|
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)
|
match = quarantine.match(header)
|
||||||
if match:
|
if match:
|
||||||
self.logger.debug(
|
self.logger.debug(
|
||||||
f"{self.qid}: {quarantine.name}: header matched regex")
|
f"{self.qid}: {quarantine.name}: "
|
||||||
|
f"header matched regex")
|
||||||
# check for whitelisted recipients
|
# check for whitelisted recipients
|
||||||
whitelist = quarantine.get_whitelist()
|
whitelist = quarantine.get_whitelist()
|
||||||
if whitelist:
|
if whitelist:
|
||||||
try:
|
try:
|
||||||
whitelisted_recipients = self.whitelist_cache.get_whitelisted_recipients(
|
wl_recipients = self.wl_cache.get_recipients(
|
||||||
whitelist, self.mailfrom, recipients_to_check)
|
whitelist,
|
||||||
|
self.mailfrom,
|
||||||
|
rcpts_to_check)
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
self.logger.error(
|
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
|
return Milter.TEMPFAIL
|
||||||
else:
|
else:
|
||||||
whitelisted_recipients = {}
|
wl_recipients = {}
|
||||||
|
|
||||||
# iterate recipients
|
# iterate recipients
|
||||||
for recipient in recipients_to_check.copy():
|
for rcpt in rcpts_to_check.copy():
|
||||||
if recipient in whitelisted_recipients:
|
if rcpt in wl_recipients:
|
||||||
# recipient is whitelisted in this quarantine
|
# recipient is whitelisted in this quarantine
|
||||||
self.logger.debug(
|
self.logger.debug(
|
||||||
f"{self.qid}: {quarantine.name}: recipient '{recipient}' is whitelisted")
|
f"{self.qid}: {quarantine.name}: "
|
||||||
|
f"recipient '{rcpt}' is whitelisted")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if recipient not in self.recipients_quarantines.keys() or \
|
if rcpt not in self.rcpts_quarantines or \
|
||||||
self.recipients_quarantines[recipient].index > quarantine.index:
|
self.rcpts_quarantines[rcpt].index > \
|
||||||
|
quarantine.index:
|
||||||
self.logger.debug(
|
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
|
# save match for later use as template
|
||||||
# variables
|
# variables
|
||||||
self.quarantines_matches[quarantine.name] = match
|
self.matches[quarantine.name] = match
|
||||||
self.recipients_quarantines[recipient] = quarantine
|
self.rcpts_quarantines[rcpt] = quarantine
|
||||||
if quarantine.index == 0:
|
if quarantine.index == 0:
|
||||||
# we do not need to check recipients which
|
# we do not need to check recipients which
|
||||||
# matched the quarantine with the highest
|
# matched the quarantine with the highest
|
||||||
# precedence already
|
# precedence already
|
||||||
recipients_to_check.remove(recipient)
|
rcpts_to_check.remove(rcpt)
|
||||||
else:
|
else:
|
||||||
self.logger.debug(
|
self.logger.debug(
|
||||||
f"{self.qid}: {quarantine.name}: a quarantine with same or higher "
|
f"{self.qid}: {quarantine.name}: a "
|
||||||
f"precedence matched already for recipient '{recipient}'")
|
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(
|
self.logger.debug(
|
||||||
f"{self.qid}: all recipients matched the first quarantine, "
|
f"{self.qid}: all recipients matched the first "
|
||||||
f"skipping all remaining header checks")
|
f"quarantine, skipping all remaining header checks")
|
||||||
break
|
break
|
||||||
|
|
||||||
# check if no quarantine has matched for all recipients
|
# check if no quarantine has matched for all recipients
|
||||||
if not self.recipients_quarantines:
|
if not self.rcpts_quarantines:
|
||||||
# accept email
|
# accept email
|
||||||
self.logger.info(
|
self.logger.info(
|
||||||
f"{self.qid}: passed clean for all recipients")
|
f"{self.qid}: passed clean for all recipients")
|
||||||
return Milter.ACCEPT
|
return Milter.ACCEPT
|
||||||
|
|
||||||
# check if the mail body is needed
|
# 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():
|
if quarantine.get_storage() or quarantine.get_notification():
|
||||||
# mail body is needed, continue processing
|
# mail body is needed, continue processing
|
||||||
return Milter.CONTINUE
|
return Milter.CONTINUE
|
||||||
@@ -457,7 +478,7 @@ class QuarantineMilter(Milter.Base):
|
|||||||
# quarantines, just return configured action
|
# quarantines, just return configured action
|
||||||
quarantine = self._get_preferred_quarantine()
|
quarantine = self._get_preferred_quarantine()
|
||||||
self.logger.info(
|
self.logger.info(
|
||||||
f"{self.qid}: {self.preferred_quarantine_action} "
|
f"{self.qid}: {self.preferred_action} "
|
||||||
f"matching quarantine is '{quarantine.name}', performing "
|
f"matching quarantine is '{quarantine.name}', performing "
|
||||||
f"milter action {quarantine.action}")
|
f"milter action {quarantine.action}")
|
||||||
if quarantine.action == "REJECT":
|
if quarantine.action == "REJECT":
|
||||||
@@ -484,9 +505,9 @@ class QuarantineMilter(Milter.Base):
|
|||||||
# processing recipients grouped by quarantines
|
# processing recipients grouped by quarantines
|
||||||
quarantines = []
|
quarantines = []
|
||||||
for quarantine, recipients in groupby(
|
for quarantine, recipients in groupby(
|
||||||
sorted(self.recipients_quarantines,
|
sorted(self.rcpts_quarantines,
|
||||||
key=lambda x: self.recipients_quarantines[x].index),
|
key=lambda x: self.rcpts_quarantines[x].index),
|
||||||
lambda x: self.recipients_quarantines[x]):
|
lambda x: self.rcpts_quarantines[x]):
|
||||||
quarantines.append((quarantine, list(recipients)))
|
quarantines.append((quarantine, list(recipients)))
|
||||||
|
|
||||||
# iterate quarantines sorted by index
|
# iterate quarantines sorted by index
|
||||||
@@ -495,9 +516,9 @@ class QuarantineMilter(Milter.Base):
|
|||||||
headers = defaultdict(str)
|
headers = defaultdict(str)
|
||||||
for name, value in self.headers:
|
for name, value in self.headers:
|
||||||
headers[name.lower()] = value
|
headers[name.lower()] = value
|
||||||
subgroups = self.quarantines_matches[quarantine.name].groups(
|
subgroups = self.matches[quarantine.name].groups(
|
||||||
default="")
|
default="")
|
||||||
named_subgroups = self.quarantines_matches[quarantine.name].groupdict(
|
named_subgroups = self.matches[quarantine.name].groupdict(
|
||||||
default="")
|
default="")
|
||||||
|
|
||||||
rcpts = ", ".join(recipients)
|
rcpts = ", ".join(recipients)
|
||||||
@@ -508,14 +529,16 @@ class QuarantineMilter(Milter.Base):
|
|||||||
if storage:
|
if storage:
|
||||||
# add email to quarantine
|
# add email to quarantine
|
||||||
self.logger.info(
|
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:
|
try:
|
||||||
storage_id = storage.add(
|
storage_id = storage.add(
|
||||||
self.qid, self.mailfrom, recipients, headers, self.fp,
|
self.qid, self.mailfrom, recipients, headers,
|
||||||
subgroups, named_subgroups)
|
self.fp, subgroups, named_subgroups)
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
self.logger.error(
|
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
|
return Milter.TEMPFAIL
|
||||||
|
|
||||||
# check if a notification is configured
|
# check if a notification is configured
|
||||||
@@ -551,7 +574,7 @@ class QuarantineMilter(Milter.Base):
|
|||||||
# return configured action
|
# return configured action
|
||||||
quarantine = self._get_preferred_quarantine()
|
quarantine = self._get_preferred_quarantine()
|
||||||
self.logger.info(
|
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"quarantine is '{quarantine.name}', performing milter "
|
||||||
f"action {quarantine.action}")
|
f"action {quarantine.action}")
|
||||||
if quarantine.action == "REJECT":
|
if quarantine.action == "REJECT":
|
||||||
@@ -592,12 +615,13 @@ def setup_milter(test=False, cfg_files=[]):
|
|||||||
for option in ["quarantines", "preferred_quarantine_action"]:
|
for option in ["quarantines", "preferred_quarantine_action"]:
|
||||||
if not parser.has_option("global", option):
|
if not parser.has_option("global", option):
|
||||||
raise RuntimeError(
|
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
|
# read global config section
|
||||||
global_cfg = dict(parser.items("global"))
|
global_cfg = dict(parser.items("global"))
|
||||||
preferred_quarantine_action = global_cfg["preferred_quarantine_action"].lower()
|
preferred_action = global_cfg["preferred_quarantine_action"].lower()
|
||||||
if preferred_quarantine_action not in ["first", "last"]:
|
if preferred_action not in ["first", "last"]:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"option preferred_quarantine_action has illegal value")
|
"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(",")]
|
q.strip() for q in global_cfg["quarantines"].split(",")]
|
||||||
if len(quarantines) != len(set(quarantines)):
|
if len(quarantines) != len(set(quarantines)):
|
||||||
raise RuntimeError(
|
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:
|
if "global" in quarantines:
|
||||||
quarantines.remove("global")
|
quarantines.remove("global")
|
||||||
logger.warning(
|
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:
|
if not quarantines:
|
||||||
raise RuntimeError("no quarantines configured")
|
raise RuntimeError("no quarantines configured")
|
||||||
|
|
||||||
@@ -628,7 +654,7 @@ def setup_milter(test=False, cfg_files=[]):
|
|||||||
quarantine.setup_from_cfg(global_cfg, cfg, test)
|
quarantine.setup_from_cfg(global_cfg, cfg, test)
|
||||||
milter_quarantines.append(quarantine)
|
milter_quarantines.append(quarantine)
|
||||||
|
|
||||||
QuarantineMilter.preferred_quarantine_action = preferred_quarantine_action
|
QuarantineMilter.preferred_action = preferred_action
|
||||||
QuarantineMilter.quarantines = milter_quarantines
|
QuarantineMilter.quarantines = milter_quarantines
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -20,11 +20,10 @@ import logging.handlers
|
|||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from email.header import decode_header
|
|
||||||
|
|
||||||
from pyquarantine import QuarantineMilter, setup_milter
|
from pyquarantine import QuarantineMilter, setup_milter
|
||||||
from pyquarantine.version import __version__ as version
|
from pyquarantine.version import __version__ as version
|
||||||
|
|
||||||
|
|
||||||
def _get_quarantine(quarantines, name):
|
def _get_quarantine(quarantines, name):
|
||||||
try:
|
try:
|
||||||
quarantine = next((q for q in quarantines if q.name == name))
|
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'")
|
raise RuntimeError(f"invalid quarantine 'name'")
|
||||||
return quarantine
|
return quarantine
|
||||||
|
|
||||||
|
|
||||||
def _get_storage(quarantines, name):
|
def _get_storage(quarantines, name):
|
||||||
quarantine = _get_quarantine(quarantines, name)
|
quarantine = _get_quarantine(quarantines, name)
|
||||||
storage = quarantine.get_storage()
|
storage = quarantine.get_storage()
|
||||||
@@ -40,6 +40,7 @@ def _get_storage(quarantines, name):
|
|||||||
"storage type is set to NONE")
|
"storage type is set to NONE")
|
||||||
return storage
|
return storage
|
||||||
|
|
||||||
|
|
||||||
def _get_notification(quarantines, name):
|
def _get_notification(quarantines, name):
|
||||||
quarantine = _get_quarantine(quarantines, name)
|
quarantine = _get_quarantine(quarantines, name)
|
||||||
notification = quarantine.get_notification()
|
notification = quarantine.get_notification()
|
||||||
@@ -48,6 +49,7 @@ def _get_notification(quarantines, name):
|
|||||||
"notification type is set to NONE")
|
"notification type is set to NONE")
|
||||||
return notification
|
return notification
|
||||||
|
|
||||||
|
|
||||||
def _get_whitelist(quarantines, name):
|
def _get_whitelist(quarantines, name):
|
||||||
quarantine = _get_quarantine(quarantines, name)
|
quarantine = _get_quarantine(quarantines, name)
|
||||||
whitelist = quarantine.get_whitelist()
|
whitelist = quarantine.get_whitelist()
|
||||||
@@ -56,6 +58,7 @@ def _get_whitelist(quarantines, name):
|
|||||||
"whitelist type is set to NONE")
|
"whitelist type is set to NONE")
|
||||||
return whitelist
|
return whitelist
|
||||||
|
|
||||||
|
|
||||||
def print_table(columns, rows):
|
def print_table(columns, rows):
|
||||||
if not rows:
|
if not rows:
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -14,7 +14,6 @@
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import smtplib
|
import smtplib
|
||||||
import sys
|
|
||||||
|
|
||||||
from multiprocessing import Process, Queue
|
from multiprocessing import Process, Queue
|
||||||
|
|
||||||
@@ -50,7 +49,8 @@ def mailprocess():
|
|||||||
smtp_send(smtp_host, smtp_port, mailfrom, recipient, mail)
|
smtp_send(smtp_host, smtp_port, mailfrom, recipient, mail)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(
|
logger.error(
|
||||||
f"{qid}: error while sending {emailtype} to '{recipient}': {e}")
|
f"{qid}: error while sending {emailtype} "
|
||||||
|
f"to '{recipient}': {e}")
|
||||||
else:
|
else:
|
||||||
logger.info(
|
logger.info(
|
||||||
f"{qid}: successfully sent {emailtype} to: {recipient}")
|
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,
|
(smtp_host, smtp_port, qid, mailfrom, recipient, mail,
|
||||||
emailtype),
|
emailtype),
|
||||||
timeout=30)
|
timeout=30)
|
||||||
except Queue.Full as e:
|
except Queue.Full:
|
||||||
raise RuntimeError("email queue is full")
|
raise RuntimeError("email queue is full")
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ from bs4 import BeautifulSoup
|
|||||||
from cgi import escape
|
from cgi import escape
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from email import policy
|
from email import policy
|
||||||
from email.header import decode_header
|
|
||||||
from email.mime.multipart import MIMEMultipart
|
from email.mime.multipart import MIMEMultipart
|
||||||
from email.mime.text import MIMEText
|
from email.mime.text import MIMEText
|
||||||
from email.mime.image import MIMEImage
|
from email.mime.image import MIMEImage
|
||||||
@@ -136,7 +135,8 @@ class EMailNotification(BaseNotification):
|
|||||||
cfg[opt] = defaults[opt]
|
cfg[opt] = defaults[opt]
|
||||||
else:
|
else:
|
||||||
raise RuntimeError(
|
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_host = cfg["notification_email_smtp_host"]
|
||||||
self.smtp_port = cfg["notification_email_smtp_port"]
|
self.smtp_port = cfg["notification_email_smtp_port"]
|
||||||
@@ -176,11 +176,13 @@ class EMailNotification(BaseNotification):
|
|||||||
elif strip_images in ["FALSE", "OFF", "NO"]:
|
elif strip_images in ["FALSE", "OFF", "NO"]:
|
||||||
self.strip_images = False
|
self.strip_images = False
|
||||||
else:
|
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()
|
self.parser_lib = cfg["notification_email_parser_lib"].strip()
|
||||||
if self.parser_lib not in ["lxml", "html.parser"]:
|
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
|
# read email replacement image if specified
|
||||||
replacement_img = cfg["notification_email_replacement_img"].strip()
|
replacement_img = cfg["notification_email_replacement_img"].strip()
|
||||||
@@ -199,7 +201,8 @@ class EMailNotification(BaseNotification):
|
|||||||
|
|
||||||
# read images to embed if specified
|
# read images to embed if specified
|
||||||
embedded_img_paths = [
|
embedded_img_paths = [
|
||||||
p.strip() for p in cfg["notification_email_embedded_imgs"].split(",") if p]
|
p.strip() for p in cfg["notification_email_embedded_imgs"].split(
|
||||||
|
",") if p]
|
||||||
self.embedded_imgs = []
|
self.embedded_imgs = []
|
||||||
for img_path in embedded_img_paths:
|
for img_path in embedded_img_paths:
|
||||||
# read image
|
# read image
|
||||||
@@ -219,7 +222,9 @@ class EMailNotification(BaseNotification):
|
|||||||
try:
|
try:
|
||||||
body = msg.get_body(preferencelist=("html", "plain"))
|
body = msg.get_body(preferencelist=("html", "plain"))
|
||||||
except Exception as e:
|
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
|
body = None
|
||||||
|
|
||||||
if body:
|
if body:
|
||||||
@@ -228,13 +233,16 @@ class EMailNotification(BaseNotification):
|
|||||||
try:
|
try:
|
||||||
content = content.decode(encoding=charset, errors="replace")
|
content = content.decode(encoding=charset, errors="replace")
|
||||||
except LookupError:
|
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 = content.decode("utf-8", errors="replace")
|
||||||
content_type = body.get_content_type()
|
content_type = body.get_content_type()
|
||||||
if content_type == "text/plain":
|
if content_type == "text/plain":
|
||||||
# convert text/plain to text/html
|
# convert text/plain to text/html
|
||||||
self.logger.debug(
|
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<br/>",
|
content = re.sub(r"^(.*)$", r"\1<br/>",
|
||||||
escape(content), flags=re.MULTILINE)
|
escape(content), flags=re.MULTILINE)
|
||||||
else:
|
else:
|
||||||
@@ -248,7 +256,8 @@ class EMailNotification(BaseNotification):
|
|||||||
# create BeautifulSoup object
|
# create BeautifulSoup object
|
||||||
length = len(content)
|
length = len(content)
|
||||||
self.logger.debug(
|
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")
|
f"text length is {length} bytes")
|
||||||
soup = BeautifulSoup(content, self.parser_lib)
|
soup = BeautifulSoup(content, self.parser_lib)
|
||||||
self.logger.debug(
|
self.logger.debug(
|
||||||
@@ -263,7 +272,8 @@ class EMailNotification(BaseNotification):
|
|||||||
# completly remove bad elements
|
# completly remove bad elements
|
||||||
for element in soup(EMailNotification._bad_tags):
|
for element in soup(EMailNotification._bad_tags):
|
||||||
self.logger.debug(
|
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()
|
element.extract()
|
||||||
|
|
||||||
# remove not whitelisted elements, but keep their content
|
# remove not whitelisted elements, but keep their content
|
||||||
@@ -279,11 +289,13 @@ class EMailNotification(BaseNotification):
|
|||||||
if attribute not in EMailNotification._good_attributes:
|
if attribute not in EMailNotification._good_attributes:
|
||||||
if element.name == "a" and attribute == "href":
|
if element.name == "a" and attribute == "href":
|
||||||
self.logger.debug(
|
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"] = "#"
|
element["href"] = "#"
|
||||||
else:
|
else:
|
||||||
self.logger.debug(
|
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])
|
del(element.attrs[attribute])
|
||||||
return soup
|
return soup
|
||||||
|
|
||||||
@@ -338,16 +350,17 @@ class EMailNotification(BaseNotification):
|
|||||||
self.logger.debug(f"{qid}: parsing email template")
|
self.logger.debug(f"{qid}: parsing email template")
|
||||||
|
|
||||||
# generate dict containing all template variables
|
# generate dict containing all template variables
|
||||||
variables = defaultdict(str,
|
variables = defaultdict(
|
||||||
EMAIL_HTML_TEXT=sanitized_text,
|
str,
|
||||||
EMAIL_FROM=escape(headers["from"]),
|
EMAIL_HTML_TEXT=sanitized_text,
|
||||||
EMAIL_ENVELOPE_FROM=escape(mailfrom),
|
EMAIL_FROM=escape(headers["from"]),
|
||||||
EMAIL_ENVELOPE_FROM_URL=escape(quote(mailfrom)),
|
EMAIL_ENVELOPE_FROM=escape(mailfrom),
|
||||||
EMAIL_TO=escape(headers["to"]),
|
EMAIL_ENVELOPE_FROM_URL=escape(quote(mailfrom)),
|
||||||
EMAIL_ENVELOPE_TO=escape(recipient),
|
EMAIL_TO=escape(headers["to"]),
|
||||||
EMAIL_ENVELOPE_TO_URL=escape(quote(recipient)),
|
EMAIL_ENVELOPE_TO=escape(recipient),
|
||||||
EMAIL_SUBJECT=escape(headers["subject"]),
|
EMAIL_ENVELOPE_TO_URL=escape(quote(recipient)),
|
||||||
EMAIL_QUARANTINE_ID=storage_id)
|
EMAIL_SUBJECT=escape(headers["subject"]),
|
||||||
|
EMAIL_QUARANTINE_ID=storage_id)
|
||||||
|
|
||||||
if subgroups:
|
if subgroups:
|
||||||
number = 0
|
number = 0
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import pyquarantine
|
|||||||
|
|
||||||
from pyquarantine.version import __version__ as version
|
from pyquarantine.version import __version__ as version
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"Run PyQuarantine-Milter."
|
"Run PyQuarantine-Milter."
|
||||||
# parse command line
|
# parse command line
|
||||||
|
|||||||
@@ -73,14 +73,16 @@ class FileMailStorage(BaseMailStorage):
|
|||||||
cfg[opt] = defaults[opt]
|
cfg[opt] = defaults[opt]
|
||||||
else:
|
else:
|
||||||
raise RuntimeError(
|
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"]
|
self.directory = cfg["storage_directory"]
|
||||||
|
|
||||||
# check if quarantine directory exists and is writable
|
# check if quarantine directory exists and is writable
|
||||||
if not os.path.isdir(self.directory) or not os.access(
|
if not os.path.isdir(self.directory) or not os.access(
|
||||||
self.directory, os.W_OK):
|
self.directory, os.W_OK):
|
||||||
raise RuntimeError(
|
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"
|
self._metadata_suffix = ".metadata"
|
||||||
|
|
||||||
def _save_datafile(self, storage_id, fp):
|
def _save_datafile(self, storage_id, fp):
|
||||||
@@ -203,7 +205,8 @@ class FileMailStorage(BaseMailStorage):
|
|||||||
if len(recipients) == 1 and \
|
if len(recipients) == 1 and \
|
||||||
recipients[0] not in metadata["recipients"]:
|
recipients[0] not in metadata["recipients"]:
|
||||||
continue
|
continue
|
||||||
elif len(set(recipients + metadata["recipients"])) == len(recipients + metadata["recipients"]):
|
elif len(set(recipients + metadata["recipients"])) == \
|
||||||
|
len(recipients + metadata["recipients"]):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
emails[storage_id] = metadata
|
emails[storage_id] = metadata
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
__version__ = "1.0.1"
|
__version__ = "1.0.3"
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
import logging
|
import logging
|
||||||
import peewee
|
import peewee
|
||||||
import re
|
import re
|
||||||
import sys
|
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from playhouse.db_url import connect
|
from playhouse.db_url import connect
|
||||||
@@ -88,7 +87,8 @@ class DatabaseWhitelist(WhitelistBase):
|
|||||||
defaults = {}
|
defaults = {}
|
||||||
|
|
||||||
# check config
|
# 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:
|
if opt in cfg:
|
||||||
continue
|
continue
|
||||||
if opt in global_cfg:
|
if opt in global_cfg:
|
||||||
@@ -97,7 +97,8 @@ class DatabaseWhitelist(WhitelistBase):
|
|||||||
cfg[opt] = defaults[opt]
|
cfg[opt] = defaults[opt]
|
||||||
else:
|
else:
|
||||||
raise RuntimeError(
|
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"]
|
tablename = cfg["whitelist_db_table"]
|
||||||
connection_string = cfg["whitelist_db_connection"]
|
connection_string = cfg["whitelist_db_connection"]
|
||||||
@@ -168,7 +169,8 @@ class DatabaseWhitelist(WhitelistBase):
|
|||||||
|
|
||||||
# generate list of possible mailfroms
|
# generate list of possible mailfroms
|
||||||
self.logger.debug(
|
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 = [""]
|
mailfroms = [""]
|
||||||
if "@" in mailfrom and not mailfrom.startswith("@"):
|
if "@" in mailfrom and not mailfrom.startswith("@"):
|
||||||
domain = mailfrom.split("@")[1]
|
domain = mailfrom.split("@")[1]
|
||||||
@@ -221,7 +223,8 @@ class DatabaseWhitelist(WhitelistBase):
|
|||||||
try:
|
try:
|
||||||
for entry in list(self.model.select()):
|
for entry in list(self.model.select()):
|
||||||
if older_than is not None:
|
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
|
continue
|
||||||
|
|
||||||
if mailfrom is not None:
|
if mailfrom is not None:
|
||||||
@@ -290,10 +293,11 @@ class WhitelistCache(object):
|
|||||||
mailfrom, recipient)
|
mailfrom, recipient)
|
||||||
return self.cache[whitelist][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)
|
self.load(whitelist, mailfrom, recipients)
|
||||||
return list(
|
return list(filter(
|
||||||
filter(lambda x: self.cache[whitelist][x], self.cache[whitelist].keys()))
|
lambda x: self.cache[whitelist][x],
|
||||||
|
self.cache[whitelist].keys()))
|
||||||
|
|
||||||
|
|
||||||
# list of whitelist types and their related whitelist classes
|
# list of whitelist types and their related whitelist classes
|
||||||
|
|||||||
Reference in New Issue
Block a user