fix race when receiving mail during rule process
authorOlivier Matz <zer0@droids-corp.org>
Sun, 27 Nov 2016 14:37:18 +0000 (15:37 +0100)
committerOlivier Matz <olivier.matz@6wind.com>
Mon, 28 Nov 2016 09:51:16 +0000 (10:51 +0100)
Get the state (uidnext) for each inbox used in the configuration.
It gives the uid of the next message the will be added in the mbox.
We will only care about messages with a uid lower than this uidnext,
to avoid race conditions with a message arriving while we are in the

Signed-off-by: Olivier Matz <zer0@droids-corp.org>
imapami/__init__.py
imapami/rules.py

index 5000e77..b817646 100644 (file)
@@ -37,6 +37,7 @@ import imaplib
 import inspect
 import logging
 import pydoc
+import re
 import sys
 import yaml
 
@@ -111,6 +112,7 @@ class Imapami(object):
         self.logger = self._get_logger(loglevel)
         self._load_config(config)
         self._update_logger()
+        self.uidnext = {}
 
     def _get_logger(self, loglevel):
         """
@@ -211,14 +213,38 @@ class Imapami(object):
         imap.login(login, password)
         self.imap = imap
 
+    def get_uidnext(self):
+        """
+        Get the state (uidnext) for each inbox used in the configuration.
+        It gives the uid of the next message the will be added in the mbox.
+        We will only care about messages with a uid lower than this uidnext,
+        to avoid race conditions with a message arriving while we are in the
+        middle of rules processing.
+        """
+        self.logger.info('Getting inbox state...')
+        mboxes = [self.config["inbox"]] + [rule.inbox for rule in self.rules]
+        for m in mboxes:
+            if m is None:
+                continue
+            if self.uidnext.get(m, None) is not None:
+                continue
+            self.imap.select(m)
+            typ, dat = self.imap.status(m, "(UIDNEXT)")
+            if typ != 'OK':
+                raise ValueError("cannot get UIDNEXT: %s", typ)
+            match = re.match("[^ ]* \(UIDNEXT ([0-9]+)\)", dat[0])
+            if match is None:
+                raise ValueError("cannot match UIDNEXT: %s", typ)
+            self.uidnext[m] = int(match.groups()[0])
+        self.logger.info('Done: %r', self.uidnext)
+
     def process_rules(self):
         """
         Process the rules.
         """
         self.logger.info('Processing rules...')
-        inbox = self.config["inbox"]
         for rule in self.rules:
-            rule.process(self, inbox)
+            rule.process(self)
         self.logger.info('Done.')
 
     def close(self):
@@ -318,6 +344,7 @@ def main():
         sys.exit(0)
 
     p.connect()
+    p.get_uidnext()
     p.process_rules()
     p.close()
 
index 9efd6dd..3adff91 100644 (file)
@@ -156,12 +156,6 @@ class ImapamiRule(object):
         :returns:
           A list of IMAP items
         """
-        # select the input mailbox
-        if self.inbox is not None:
-            ami.imap.select(self.inbox)
-        else:
-            ami.imap.select(inbox)
-
         # search messages matching conditions
         criteria = "(%s)" % self.get_criteria(ami)
         ami.logger.debug("processing rule %s, inbox %s, imap criteria %s",
@@ -173,6 +167,7 @@ class ImapamiRule(object):
             return
 
         item_list = items[0].split()
+        item_list = [i for i in item_list if int(i) < ami.uidnext[inbox]]
         ami.logger.debug("matching mails returned by server: %s", item_list)
         return item_list
 
@@ -202,15 +197,19 @@ class ImapamiRule(object):
         ami.logger.debug('get imap parts = %s', parts_str)
         return parts_str, parts
 
-    def process(self, ami, inbox):
+    def process(self, ami):
         """
         Process the rule.
 
         :arg Imapami ami:
           The Imapami object
-        :arg string inbox:
-          The default input mailbox directory.
         """
+        if self.inbox is not None:
+            inbox = self.inbox
+        else:
+            inbox = ami.config["inbox"]
+        ami.imap.select(inbox)
+
         # get the list of items (mails) matching the condition criteria
         item_list = self._search(ami, inbox)
         # determine what parts should be fetched