summaryrefslogtreecommitdiff
blob: 5a99c92694b075220dfbdec940116b513395c393 (plain)
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