diff options
Diffstat (limited to 'src/infra.gentoo.org/milters/mlmmj.py')
-rw-r--r-- | src/infra.gentoo.org/milters/mlmmj.py | 107 |
1 files changed, 107 insertions, 0 deletions
diff --git a/src/infra.gentoo.org/milters/mlmmj.py b/src/infra.gentoo.org/milters/mlmmj.py new file mode 100644 index 0000000..5a99c92 --- /dev/null +++ b/src/infra.gentoo.org/milters/mlmmj.py @@ -0,0 +1,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 |