diff --git a/Mailnag/backends/imap.py b/Mailnag/backends/imap.py index 7fe460d..eb5949c 100644 --- a/Mailnag/backends/imap.py +++ b/Mailnag/backends/imap.py @@ -27,6 +27,7 @@ import email import logging import re import Mailnag.common.imaplib2 as imaplib +from Mailnag.common.imaplib2 import AUTH class ImapBackend: """Implementation of IMAP mail boxes.""" @@ -42,6 +43,7 @@ class ImapBackend: self.ssl = ssl # bool self.folders = folders self._conn = None + self._conn_closed = True def get_connection(self, use_existing): @@ -82,18 +84,29 @@ class ImapBackend: except: pass raise # re-throw exception + self._conn_closed = False + + # Need to get out of AUTH mode of fresh connections. + if self._conn.state == AUTH: + self.select() + return self._conn def close(self): - self._conn.close() + # if conn has already been closed, don't try to close it again + if not self._conn_closed: + self._conn.close() + self._conn_closed = True self._conn.logout() + self._conn = None def has_connection(self): return (self._conn != None) and \ (self._conn.state != imaplib.LOGOUT) and \ - (not self._conn.Terminate) + (not self._conn.Terminate) and \ + (not self._conn_closed) def list_messages(self): @@ -151,3 +164,38 @@ class ImapBackend: return lst + + # TODO: Temporarily public. Make private. + def select(self): + if len(self.folders) == 1: + self._conn.select(self.folders[0]) + else: + self._conn.select("INBOX") + + + def notify_next_change(self, callback=None, timeout=None): + # register idle callback that is called whenever an idle event + # arrives (new mail / mail deleted). + # the callback is called after minutes at the latest. + # gmail sends keepalive events every 5 minutes. + + # idle callback (runs on a further thread) + def _idle_callback(args): + # check if the connection has been reset by provider + self._conn_closed = (args[2] != None) and (args[2][0] is self._conn.abort) + + # call actual callback + callback(args) + + self._conn.idle(callback = _idle_callback, timeout = timeout) + + + def cancel_notifications(self): + try: + if self._conn != None: + # Exit possible active idle state. + # (also calls idle_callback) + self._conn.noop() + except: + pass + diff --git a/Mailnag/common/accounts.py b/Mailnag/common/accounts.py index 9b2c8b6..850d657 100644 --- a/Mailnag/common/accounts.py +++ b/Mailnag/common/accounts.py @@ -92,6 +92,19 @@ class Account: return self.backend.list_messages() + # TODO: Temporarily here. Remove when no needed. + def select(self): + self.backend.select() + + + def notify_next_change(self, callback=None, timeout=None): + self.backend.notify_next_change(callback, timeout) + + + def cancel_notifications(self): + self.backend.cancel_notifications() + + # Requests folder names (list) from a server. # Relevant for IMAP accounts only. # Returns an empty list when used on POP3 accounts. diff --git a/Mailnag/daemon/idlers.py b/Mailnag/daemon/idlers.py index 8f53075..8e740a1 100644 --- a/Mailnag/daemon/idlers.py +++ b/Mailnag/daemon/idlers.py @@ -25,7 +25,6 @@ import threading import time import logging -from Mailnag.common.imaplib2 import AUTH from Mailnag.common.exceptions import InvalidOperationException @@ -41,14 +40,7 @@ class Idler(object): self._sync_callback = sync_callback self._account = account self._idle_timeout = idle_timeout - # use_existing = True: - # connection has been opened in mailnagdaemon.py already (immediate check) - self._conn = account.get_connection(use_existing = True) self._disposed = False - - # Need to get out of AUTH mode of fresh connections. - if self._conn.state == AUTH: - self._select(self._conn, account) def start(self): @@ -63,34 +55,27 @@ class Idler(object): self._event.set() self._thread.join() - try: - if self._conn != None: - # Exit possible active idle state. - # (also calls idle_callback) - self._conn.noop() - except: - pass - self._disposed = True logging.info('Idler closed') # idle thread def _idle(self): + # use_existing = True: + # connection has been opened in mailnagdaemon.py already (immediate check) + self._conn = self._account.get_connection(use_existing = True) + while True: # if the event is set here, # disposed() must have been called # so stop the idle thread. if self._event.isSet(): - return + break self._needsync = False self._conn_closed = False - # register idle callback that is called whenever an idle event arrives (new mail / mail deleted). - # the callback is called after minutes at the latest. - # gmail sends keepalive events every 5 minutes. - self._conn.idle(callback = self._idle_callback, timeout = 60 * self._idle_timeout) + self._account.notify_next_change(callback = self._idle_callback, timeout = 60 * self._idle_timeout) # waits for the event to be set # (in idle callback or in dispose()) @@ -99,17 +84,17 @@ class Idler(object): # if the event is set due to idle sync if self._needsync: self._event.clear() - if self._conn_closed: + if not self._account.has_connection(): self._reconnect() - if self._conn != None: + if self._account.has_connection(): self._sync_callback(self._account) + self._account.cancel_notifications() + # idle callback (runs on a further thread) def _idle_callback(self, args): - # check if the connection has been reset by provider - self._conn_closed = (args[2] != None) and (args[2][0] is self._conn.abort) # flag that a mail sync is needed self._needsync = True # trigger waiting _idle thread @@ -120,14 +105,10 @@ class Idler(object): # connection has been reset by provider -> try to reconnect logging.info("Idler thread for account '%s' has been disconnected" % self._account.name) - # conn has already been closed, don't try to close it again - # self._conn.close() # (calls idle_callback) - - # shutdown existing callback thread - self._conn.logout() + self._account.close() self._conn = None - while (self._conn == None) and (not self._event.isSet()): + while (not self._account.has_connection()) and (not self._event.isSet()): logging.info("Trying to reconnect Idler thread for account '%s'." % self._account.name) try: self._conn = self._account.get_connection(use_existing = False) @@ -137,18 +118,8 @@ class Idler(object): logging.info("Trying to reconnect Idler thread for account '%s' in %s minutes" % (self._account.name, str(self.RECONNECT_RETRY_INTERVAL))) self._wait(60 * self.RECONNECT_RETRY_INTERVAL) # don't hammer the server - - if self._conn != None: - self._select(self._conn, self._account) - def _select(self, conn, account): - if len(account.folders) == 1: - conn.select(account.folders[0]) - else: - conn.select("INBOX") - - def _wait(self, secs): start_time = time.time() while (((time.time() - start_time) < secs) and (not self._event.isSet())):