]> code.delx.au - offlineimap/blobdiff - offlineimap/folder/IMAP.py
fix behaviour for delete/expunge, when lacking rights
[offlineimap] / offlineimap / folder / IMAP.py
index f1d6d50ba905f826bdc96f50e73418f6e81b3413..df1b3297cb2afb9b125a3af827ceabc483ee6936 100644 (file)
@@ -1,5 +1,5 @@
 # IMAP folder support
-# Copyright (C) 2002-2004 John Goerzen
+# Copyright (C) 2002-2007 John Goerzen
 # <jgoerzen@complete.org>
 #
 #    This program is free software; you can redistribute it and/or modify
 #
 #    You should have received a copy of the GNU General Public License
 #    along with this program; if not, write to the Free Software
-#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+#    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
 
 from Base import BaseFolder
-from offlineimap import imaputil, imaplib
+import imaplib
+from offlineimap import imaputil, imaplibutil
 from offlineimap.ui import UIBase
 from offlineimap.version import versionstr
 import rfc822, time, string, random, binascii, re
@@ -40,6 +41,16 @@ class IMAPFolder(BaseFolder):
         self.randomgenerator = random.Random()
         BaseFolder.__init__(self)
 
+    def selectro(self, imapobj):
+        """Select this folder when we do not need write access.
+        Prefer SELECT to EXAMINE if we can, since some servers
+        (Courier) do not stabilize UID validity until the folder is
+        selected."""
+        try:
+            imapobj.select(self.getfullname())
+        except imapobj.readonly:
+            imapobj.select(self.getfullname(), readonly = 1)
+
     def getaccountname(self):
         return self.accountname
 
@@ -59,11 +70,52 @@ class IMAPFolder(BaseFolder):
         imapobj = self.imapserver.acquireconnection()
         try:
             # Primes untagged_responses
-            imapobj.select(self.getfullname(), readonly = 1)
+            self.selectro(imapobj)
             return long(imapobj.untagged_responses['UIDVALIDITY'][0])
         finally:
             self.imapserver.releaseconnection(imapobj)
     
+    def quickchanged(self, statusfolder):
+        # An IMAP folder has definitely changed if the number of
+        # messages or the UID of the last message have changed.  Otherwise
+        # only flag changes could have occurred.
+        imapobj = self.imapserver.acquireconnection()
+        try:
+            # Primes untagged_responses
+            imapobj.select(self.getfullname(), readonly = 1, force = 1)
+            try:
+                # Some mail servers do not return an EXISTS response if
+                # the folder is empty.
+                maxmsgid = long(imapobj.untagged_responses['EXISTS'][0])
+            except KeyError:
+                return True
+
+            # Different number of messages than last time?
+            if maxmsgid != len(statusfolder.getmessagelist()):
+                return True
+
+            if maxmsgid < 1:
+                # No messages; return
+                return False
+
+            # Now, get the UID for the last message.
+            response = imapobj.fetch('%d' % maxmsgid, '(UID)')[1]
+        finally:
+            self.imapserver.releaseconnection(imapobj)
+
+        # Discard the message number.
+        messagestr = string.split(response[0], maxsplit = 1)[1]
+        options = imaputil.flags2hash(messagestr)
+        if not options.has_key('UID'):
+            return True
+        uid = long(options['UID'])
+        saveduids = statusfolder.getmessagelist().keys()
+        saveduids.sort()
+        if uid != saveduids[-1]:
+            return True
+
+        return False
+
     def cachemessagelist(self):
         imapobj = self.imapserver.acquireconnection()
         self.messagelist = {}
@@ -84,7 +136,7 @@ class IMAPFolder(BaseFolder):
             # Now, get the flags and UIDs for these.
             # We could conceivably get rid of maxmsgid and just say
             # '1:*' here.
-            response = imapobj.fetch('1:%d' % maxmsgid, '(FLAGS UID)')[1]
+            response = imapobj.fetch('1:%d' % maxmsgid, '(FLAGS UID INTERNALDATE)')[1]
         finally:
             self.imapserver.releaseconnection(imapobj)
         for messagestr in response:
@@ -98,7 +150,8 @@ class IMAPFolder(BaseFolder):
             else:
                 uid = long(options['UID'])
                 flags = imaputil.flagsimap2maildir(options['FLAGS'])
-                self.messagelist[uid] = {'uid': uid, 'flags': flags}
+                rtime = imaplibutil.Internaldate2epoch(messagestr)
+                self.messagelist[uid] = {'uid': uid, 'flags': flags, 'time': rtime}
 
     def getmessagelist(self):
         return self.messagelist
@@ -115,6 +168,9 @@ class IMAPFolder(BaseFolder):
                 
         finally:
             self.imapserver.releaseconnection(imapobj)
+
+    def getmessagetime(self, uid):
+        return self.messagelist[uid]['time']
     
     def getmessageflags(self, uid):
         return self.messagelist[uid]['flags']
@@ -150,7 +206,7 @@ class IMAPFolder(BaseFolder):
 
     def savemessage_searchforheader(self, imapobj, headername, headervalue):
         if imapobj.untagged_responses.has_key('APPENDUID'):
-            return long(imapobj.untagged_responses['APPENDUID'][0].split(' ')[1])
+            return long(imapobj.untagged_responses['APPENDUID'][-1].split(' ')[1])
 
         ui = UIBase.getglobalui()
         ui.debug('imap', 'savemessage_searchforheader called for %s: %s' % \
@@ -158,13 +214,17 @@ class IMAPFolder(BaseFolder):
         # Now find the UID it got.
         headervalue = imapobj._quote(headervalue)
         try:
-            matchinguids = imapobj.uid('search', None,
-                                       '(HEADER %s %s)' % (headername, headervalue))[1][0]
-        except imapobj.error:
+            matchinguids = imapobj.uid('search', 'HEADER', headername, headervalue)[1][0]
+        except imapobj.error, err:
             # IMAP server doesn't implement search or had a problem.
+            ui.debug('imap', "savemessage_searchforheader: got IMAP error '%s' while attempting to UID SEARCH for message with header %s" % (err, headername))
             return 0
         ui.debug('imap', 'savemessage_searchforheader got initial matchinguids: ' + repr(matchinguids))
-               
+
+        if matchinguids == '':
+            ui.debug('imap', "savemessage_searchforheader: UID SEARCH for message with header %s yielded no results" % headername)
+            return 0
+
         matchinguids = matchinguids.split(' ')
         ui.debug('imap', 'savemessage_searchforheader: matchinguids now ' + \
                  repr(matchinguids))
@@ -173,7 +233,7 @@ class IMAPFolder(BaseFolder):
         matchinguids.sort()
         return long(matchinguids[0])
 
-    def savemessage(self, uid, content, flags):
+    def savemessage(self, uid, content, flags, rtime):
         imapobj = self.imapserver.acquireconnection()
         ui = UIBase.getglobalui()
         ui.debug('imap', 'savemessage: called')
@@ -190,16 +250,29 @@ class IMAPFolder(BaseFolder):
             # In order to get the new uid, we need to save off the message ID.
 
             message = rfc822.Message(StringIO(content))
-            datetuple = rfc822.parsedate(message.getheader('Date'))
+            datetuple_msg = rfc822.parsedate(message.getheader('Date'))
             # Will be None if missing or not in a valid format.
-            if datetuple == None:
+
+            # If time isn't known
+            if rtime == None and datetuple_msg == None:
                 datetuple = time.localtime()
+            elif rtime == None:
+                datetuple = datetuple_msg
+            else:
+                datetuple = time.localtime(rtime)
+
             try:
                 if datetuple[0] < 1981:
                     raise ValueError
+
+                # Check for invalid date
+                datetuple_check = time.localtime(time.mktime(datetuple))
+                if datetuple[:2] != datetuple_check[:2]:
+                    raise ValueError
+
                 # This could raise a value error if it's not a valid format.
                 date = imaplib.Time2Internaldate(datetuple) 
-            except ValueError:
+            except (ValueError, OverflowError):
                 # Argh, sometimes it's a valid format but year is 0102
                 # or something.  Argh.  It seems that Time2Internaldate
                 # will rause a ValueError if the year is 0102 but not 1902,
@@ -227,11 +300,11 @@ class IMAPFolder(BaseFolder):
             assert(imapobj.check()[0] == 'OK')
 
             # Keep trying until we get the UID.
-            try:
-                ui.debug('imap', 'savemessage: first attempt to get new UID')
-                uid = self.savemessage_searchforheader(imapobj, headername,
-                                                       headervalue)
-            except ValueError:
+            ui.debug('imap', 'savemessage: first attempt to get new UID')
+            uid = self.savemessage_searchforheader(imapobj, headername,
+                                                   headervalue)
+            # See docs for savemessage in Base.py for explanation of this and other return values
+            if uid <= 0:
                 ui.debug('imap', 'savemessage: first attempt to get new UID failed.  Going to run a NOOP and try again.')
                 assert(imapobj.noop()[0] == 'OK')
                 uid = self.savemessage_searchforheader(imapobj, headername,
@@ -239,7 +312,9 @@ class IMAPFolder(BaseFolder):
         finally:
             self.imapserver.releaseconnection(imapobj)
 
-        self.messagelist[uid] = {'uid': uid, 'flags': flags}
+        if uid: # avoid UID FETCH 0 crash happening later on
+            self.messagelist[uid] = {'uid': uid, 'flags': flags}
+
         ui.debug('imap', 'savemessage: returning %d' % uid)
         return uid
 
@@ -253,7 +328,7 @@ class IMAPFolder(BaseFolder):
                 return
             result = imapobj.uid('store', '%d' % uid, 'FLAGS',
                                  imaputil.flagsmaildir2imap(flags))
-            assert result[0] == 'OK', 'Error with store: ' + r[1]
+            assert result[0] == 'OK', 'Error with store: ' + '. '.join(r[1])
         finally:
             self.imapserver.releaseconnection(imapobj)
         result = result[1][0]
@@ -282,6 +357,12 @@ class IMAPFolder(BaseFolder):
         self.processmessagesflags('-', uidlist, flags)
 
     def processmessagesflags(self, operation, uidlist, flags):
+        if len(uidlist) > 101:
+            # Hack for those IMAP ervers with a limited line length
+            self.processmessagesflags(operation, uidlist[:100], flags)
+            self.processmessagesflags(operation, uidlist[100:], flags)
+            return
+        
         imapobj = self.imapserver.acquireconnection()
         try:
             try:
@@ -293,7 +374,7 @@ class IMAPFolder(BaseFolder):
                             imaputil.listjoin(uidlist),
                             operation + 'FLAGS',
                             imaputil.flagsmaildir2imap(flags))
-            assert r[0] == 'OK', 'Error with store: ' + r[1]
+            assert r[0] == 'OK', 'Error with store: ' + '. '.join(r[1])
             r = r[1]
         finally:
             self.imapserver.releaseconnection(imapobj)
@@ -343,12 +424,17 @@ class IMAPFolder(BaseFolder):
         self.addmessagesflags_noconvert(uidlist, ['T'])
         imapobj = self.imapserver.acquireconnection()
         try:
-            try:
-                imapobj.select(self.getfullname())
-            except imapobj.readonly:
+            # Making sure, that we have the necessary rights
+            # ensuring that we access readonly: python's braindead imaplib.py
+            # otherwise might raise an exception during the myrights() call
+            imapobj.select(self.getfullname(),readonly=1)
+            if not 'd' in imapobj.myrights(self.getfullname())[1][0].split()[1]:
+                # no delete/expunge rights
                 UIBase.getglobalui().deletereadonly(self, uidlist)
                 return
+
             if self.expunge:
+                imapobj.select(self.getfullname())
                 assert(imapobj.expunge()[0] == 'OK')
         finally:
             self.imapserver.releaseconnection(imapobj)