summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'src/infra.gentoo.org/milters/mlmmj.py')
-rw-r--r--src/infra.gentoo.org/milters/mlmmj.py107
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