1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
|
"""Module mlmmj implements some wrappers around mlmmj."""
import threading
# We assume that this will happen in global scope at import time before the milter is serving.
SINGLETON_MLMMJ = MlmmjConfig(source=MlmmjSource())
def GetSingletonConfig():
global SINGLETON_MLMMJ
return SINGLETON_MLMMJ
class MlmmjConfig(object):
"""Contains the config for mlmmj.
The config supports looking up if an address is a mailing list.
The config supports looking up if an address is subscribed to a list.
The config is reloaded after every refresh_count lookups
or refresh_time seconds.
This is designed to be used by a postfix milter where multiple milters
will share one instance of this config and the result is that this
class should be thread-safe.
"""
def __init__(self, source, refresh_time=600, refresh_count=10000):
self.source = source
self.refresh_time = refresh_time
self.refresh_count = refresh_count
self.lock = threading.Lock()
self.subscribers = source.GetSubscribers()
def IsMailingList(self, address):
with self.lock:
return address in self.subscribers
def IsSubscribed(self, subscriber_address, list_name):
with self.lock:
if list_name not in self.subscribers:
return False
return subscriber_address in self.subscribers[list_name].subscribers
class MlmmjSource(object):
"""This is an interface to interacting with mlmmj directly.
Because the milter will call "IsList" and "IsSubscribed" we want to avoid
letting external calls touch the filesystem. A trivial implementation might
be:
def IsList(address):
return os.path.exists(os.path.join(list_path, address))
But IMHO this is very leaky and naughty people could potentially try to use
it to do bad things. Instead we control the filesystem accesses as well as
invocations of mlmmj-list ourselves.
"""
# The value in our subscribers dict is a set of mtimes and a subscriber list.
# We only update the subscribers when the mtimes are mismatched.
MLData = collections.namedtuple('MLData', ['mtimes', 'subscribers'])
def __init__(self, list_path='/var/lists'):
self.list_path = list_path
self.subscribers = {}
Update()
def Update(self):
lists = os.listdir(list_path)
# /var/lists on the mailing lists server is messy; filter out non directories.
# /var/lists has a RETIRED directory, filter that out too.
lists = [f for f in lists if os.path.isdir(f) and f != 'RETIRED']
# In case there are 'extra' directories; use LISTNAME/control as a sentinel value for
# "this directory probably contains an mlmmj list heirarchy."
lists = [f for f in lists if not os.path.exists(os.path.join(f, 'control')]
for ml in lists:
mtimes = MlmmjSource._GetMTimes(self.list_path, ml)
if ml in self.subscribers:
if self.subscribers.mtimes == mtimes:
# mtimes are up to date, we have the latest subscriber list for this ML
continue
subscribers = MlmmjSource._GetSubscribers(self.list_path, ml)
self.subscribers[ml] = MLData(mtimes=mtimes, subscribers=subscribers)
@staticmethod
def _GetSubscribers(list_path, listname):
# -s is the normal subscriber list.
data = subprocess.check_output(['mlmmj-list', '-L', os.path.join(list_path, list_name), '-s'])
# -d is the digest subscribers list.
data += subprocess.check_output(['mlmmj-list', '-L', os.path.join(list_path, list_name), '-d'])
# -n is the nomail subscribers list.
data += subprocess.check_output(['mlmmj-list', '-L', os.path.join(list_path, list_name), '-n'])
# check_output returns bytes, convert to string so we can split on '\n'
data = data.decode('utf-8')
data = data.strip()
return data.split('\n')
@staticmethod
def _GetMTimes(list_path, listname):
dirs = ('digesters.d', 'nomailsubs.d', 'subscribers.d')
mtimes = []
for d in dirs:
try:
mtimes.append(os.stat(os.path.join(list_path, listname, d)).st_mtime)
except EnvironmentError:
pass
return mtimes
|