From 2f3991190aedcef4ded9b41b91126fb763ef26e1 Mon Sep 17 00:00:00 2001 From: Joe Keenan Date: Wed, 10 Oct 2018 17:19:54 -0400 Subject: [PATCH] Handle null flags (Gmail) --- BetterEmail.indigoPlugin/Contents/Info.plist | 2 +- .../Contents/Server Plugin/IMAPServer.py | 15 +++-- .../Contents/Server Plugin/imaplib2.py | 60 +++++++++++++++++-- 3 files changed, 65 insertions(+), 12 deletions(-) mode change 100755 => 100644 BetterEmail.indigoPlugin/Contents/Server Plugin/imaplib2.py diff --git a/BetterEmail.indigoPlugin/Contents/Info.plist b/BetterEmail.indigoPlugin/Contents/Info.plist index 033f205..91ad41e 100755 --- a/BetterEmail.indigoPlugin/Contents/Info.plist +++ b/BetterEmail.indigoPlugin/Contents/Info.plist @@ -3,7 +3,7 @@ PluginVersion - 7.2.3 + 7.2.4 ServerApiVersion 2.0 IwsApiVersion diff --git a/BetterEmail.indigoPlugin/Contents/Server Plugin/IMAPServer.py b/BetterEmail.indigoPlugin/Contents/Server Plugin/IMAPServer.py index 76a5647..49ab10e 100755 --- a/BetterEmail.indigoPlugin/Contents/Server Plugin/IMAPServer.py +++ b/BetterEmail.indigoPlugin/Contents/Server Plugin/IMAPServer.py @@ -97,8 +97,6 @@ def poll(self): self.checkMsgs() # close the connection and log out - self.device.updateStateOnServer(key="serverStatus", value="Success") - self.device.updateStateImageOnServer(indigo.kStateImageSel.SensorOn) self.connection.close() self.connection.logout() self.logger.debug(u"\tLogged out from IMAP server: " + self.device.name) @@ -132,6 +130,8 @@ def connectIMAP(self): self.connection.login(self.imapProps['serverLogin'], self.imapProps['serverPassword']) self.logger.debug(self.device.name + u": Doing select(\"INBOX\")") self.connection.select("INBOX") + self.device.updateStateOnServer(key="serverStatus", value="Success") + self.device.updateStateImageOnServer(indigo.kStateImageSel.SensorOn) resp, data = self.connection.list() if resp == 'OK': self.logger.threaddebug(u"{}: Mailbox list:".format(self.device.name)) @@ -143,6 +143,8 @@ def connectIMAP(self): except Exception, e: self.logger.error(self.device.name + ': Error connecting to IMAP server: ' + str(e)) + self.device.updateStateOnServer(key="serverStatus", value="Failure") + self.device.updateStateImageOnServer(indigo.kStateImageSel.SensorOff) indigo.activePlugin.connErrorTriggerCheck(self.device) raise @@ -185,7 +187,10 @@ def checkMsgs(self): if not self.imapProps['checkSeen']: # only check for IndigoProcessed flag if we're not processing all messages try: typ, resp = self.connection.fetch(messageNum, '(FLAGS)') - if "$IndigoProcessed" in resp[0]: + self.logger.threaddebug(u"{}: Message # {} Flags = '{}'".format(self.device.name, messageNum, resp)) + if not resp: + self.logger.debug(self.device.name + u"%s: Message # %s has no Flags. Processing anyway." % (self.device.name, messageNum)) + elif "$IndigoProcessed" in resp[0]: self.logger.debug(self.device.name + u"%s: Message # %s already seen, skipping..." % (self.device.name, messageNum)) continue except Exception, e: @@ -253,7 +258,7 @@ def checkMsgs(self): try: if message.is_multipart(): - self.logger.threaddebug(self.device.name + 'checkMsgs: Decoding multipart message') + self.logger.threaddebug(self.device.name + u": checkMsgs: Decoding multipart message") for part in message.walk(): type = part.get_content_type() self.logger.threaddebug('\tfound type: %s' % type) @@ -287,7 +292,7 @@ def checkMsgs(self): {'key':'messageText', 'value':messageText}, {'key':'lastMessage', 'value':messageID} ] - self.logger.threaddebug('checkMsgs: Updating states on server: %s' % str(stateList)) +# self.logger.threaddebug('checkMsgs: Updating states on server: %s' % str(stateList)) self.device.updateStatesOnServer(stateList) indigo.activePlugin.triggerCheck(self.device) diff --git a/BetterEmail.indigoPlugin/Contents/Server Plugin/imaplib2.py b/BetterEmail.indigoPlugin/Contents/Server Plugin/imaplib2.py old mode 100755 new mode 100644 index d705021..1fd47d2 --- a/BetterEmail.indigoPlugin/Contents/Server Plugin/imaplib2.py +++ b/BetterEmail.indigoPlugin/Contents/Server Plugin/imaplib2.py @@ -18,9 +18,9 @@ "Internaldate2Time", "ParseFlags", "Time2Internaldate", "Mon2num", "MonthNames", "InternalDate") -__version__ = "2.55" +__version__ = "2.57" __release__ = "2" -__revision__ = "55" +__revision__ = "57" __credits__ = """ Authentication code contributed by Donn Cave June 1998. String method conversion by ESR, February 2001. @@ -109,6 +109,7 @@ 'CREATE': ((AUTH, SELECTED), True), 'DELETE': ((AUTH, SELECTED), True), 'DELETEACL': ((AUTH, SELECTED), True), + 'ENABLE': ((AUTH,), False), 'EXAMINE': ((AUTH, SELECTED), False), 'EXPUNGE': ((SELECTED,), True), 'FETCH': ((SELECTED,), True), @@ -300,17 +301,18 @@ class abort(error): pass # Service errors - close and retry class readonly(abort): pass # Mailbox status changed to READ-ONLY + # These must be encoded according to utf8 setting in _mode_xxx(): + _literal = br'.*{(?P\d+)}$' + _untagged_status = br'\* (?P\d+) (?P[A-Z-]+)( (?P.*))?' + continuation_cre = re.compile(r'\+( (?P.*))?') - literal_cre = re.compile(r'.*{(?P\d+)}$') mapCRLF_cre = re.compile(r'\r\n|\r|\n') # Need to quote "atom-specials" :- # "(" / ")" / "{" / SP / 0x00 - 0x1f / 0x7f / "%" / "*" / DQUOTE / "\" / "]" # so match not the inverse set mustquote_cre = re.compile(r"[^!#$&'+,./0-9:;<=>?@A-Z\[^_`a-z|}~-]") response_code_cre = re.compile(r'\[(?P[A-Z-]+)( (?P[^\]]*))?\]') - # sequence_set_cre = re.compile(r"^[0-9]+(:([0-9]+|\*))?(,[0-9]+(:([0-9]+|\*))?)*$") untagged_response_cre = re.compile(r'\* (?P[A-Z-]+)( (?P.*))?') - untagged_status_cre = re.compile(r'\* (?P\d+) (?P[A-Z-]+)( (?P.*))?') def __init__(self, host=None, port=None, debug=None, debug_file=None, identifier=None, timeout=None, debug_buf_lvl=None): @@ -342,6 +344,8 @@ def __init__(self, host=None, port=None, debug=None, debug_file=None, identifier + self.tagpre + r'\d+) (?P[A-Z]+) (?P.*)') + self._mode_ascii() # Only option in py2 + if __debug__: self._init_debug(debug, debug_file, debug_buf_lvl) self.resp_timeout = timeout # Timeout waiting for command response @@ -428,6 +432,28 @@ def __getattr__(self, attr): raise AttributeError("Unknown IMAP4 command: '%s'" % attr) + def _mode_ascii(self): + self.utf8_enabled = False + self._encoding = 'ascii' + if bytes != str: + self.literal_cre = re.compile(self._literal, re.ASCII) + self.untagged_status_cre = re.compile(self._untagged_status, re.ASCII) + else: + self.literal_cre = re.compile(self._literal) + self.untagged_status_cre = re.compile(self._untagged_status) + + + def _mode_utf8(self): + self.utf8_enabled = True + self._encoding = 'utf-8' + if bytes != str: + self.literal_cre = re.compile(self._literal) + self.untagged_status_cre = re.compile(self._untagged_status) + else: + self.literal_cre = re.compile(self._literal, re.UNICODE) + self.untagged_status_cre = re.compile(self._untagged_status, re.UNICODE) + + # Overridable methods @@ -676,7 +702,10 @@ def append(self, mailbox, flags, date_time, message, **kw): date_time = Time2Internaldate(date_time) else: date_time = None - self.literal = self.mapCRLF_cre.sub(CRLF, message) + literal = self.mapCRLF_cre.sub(CRLF, message) + if self.utf8_enabled: + literal = b'UTF8 (' + literal + b')' + self.literal = literal try: return self._simple_command(name, mailbox, flags, date_time, **kw) finally: @@ -774,6 +803,19 @@ def deleteacl(self, mailbox, who, **kw): return self._simple_command('DELETEACL', mailbox, who, **kw) + def enable(self, capability): + """Send an RFC5161 enable string to the server. + + (typ, [data]) = .enable(capability) + """ + if 'ENABLE' not in self.capabilities: + raise self.error("Server does not support ENABLE") + typ, data = self._simple_command('ENABLE', capability) + if typ == 'OK' and 'UTF8=ACCEPT' in capability.upper(): + self._mode_utf8() + return typ, data + + def examine(self, mailbox='INBOX', **kw): """(typ, [data]) = examine(mailbox='INBOX') Select a mailbox for READ-ONLY access. (Flushes all untagged responses.) @@ -1025,11 +1067,14 @@ def rename(self, oldmailbox, newmailbox, **kw): def search(self, charset, *criteria, **kw): """(typ, [data]) = search(charset, criterion, ...) Search mailbox for matching messages. + If UTF8 is enabled, charset MUST be None. 'data' is space separated list of matching message numbers.""" name = 'SEARCH' kw['untagged_response'] = name if charset: + if self.utf8_enabled: + raise self.error("Non-None charset not valid in UTF8 mode") return self._simple_command(name, 'CHARSET', charset, *criteria, **kw) return self._simple_command(name, *criteria, **kw) @@ -1412,6 +1457,9 @@ def _command(self, name, *args, **kw): if not ok: break + if data == 'go ahead': # Apparently not uncommon broken IMAP4 server response to AUTHENTICATE command + data = '' + # Send literal if literator is not None: