From be455c18596343c4c430babff8360b4b0b708586 Mon Sep 17 00:00:00 2001 From: Olivier Matz Date: Sun, 27 Nov 2016 15:37:18 +0100 Subject: [PATCH] fix race when receiving mail during rule process 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 --- imapami/__init__.py | 31 +++++++++++++++++++++++++++++-- imapami/rules.py | 17 ++++++++--------- 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/imapami/__init__.py b/imapami/__init__.py index 5000e77..b817646 100644 --- a/imapami/__init__.py +++ b/imapami/__init__.py @@ -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() diff --git a/imapami/rules.py b/imapami/rules.py index 9efd6dd..3adff91 100644 --- a/imapami/rules.py +++ b/imapami/rules.py @@ -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 -- 2.39.5