# 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
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
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 = {}
# 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:
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
finally:
self.imapserver.releaseconnection(imapobj)
+
+ def getmessagetime(self, uid):
+ return self.messagelist[uid]['time']
def getmessageflags(self, uid):
return self.messagelist[uid]['flags']
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')
# 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, OverflowError):
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,
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
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]
imapobj = self.imapserver.acquireconnection()
try:
+ # 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)
+ myrights = imapobj.myrights(self.getfullname())[1][0].split()[1]
+ if 'T' in flags and not 'd' in myrights or \
+ 'S' in flags and not 's' in myrights or \
+ filter(lambda x: x not in 'TS', flags) and not 'w' in myrights:
+ # no delete/expunge right, but needed or
+ # no store seen right, but needed or
+ # no write right, but needed
+ UIBase.getglobalui().flagstoreadonly(self, uidlist, flags)
+ return
+
try:
imapobj.select(self.getfullname())
except imapobj.readonly:
- UIBase.getglobalui().flagstoreadonly(self, uidlist, flags)
- return
+ # Above we made sure, we have the necessary rights.
+ # Ugly hack, to prevent an unnecessary exception:
+ # readonly: mailbox status changed to READ-ONLY
+ # imaplib should take care of that itself.
+ # The connection is anyway released below, so we dont need to
+ # undo the hack.
+ imapobj.is_readonly = True
r = imapobj.uid('store',
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)
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)