Merge branch 'next'

This commit is contained in:
Eygene Ryabinkin 2014-09-19 11:08:58 +04:00
commit 2009952ec9
8 changed files with 104 additions and 19 deletions

View File

@ -8,6 +8,10 @@ ChangeLog
OfflineIMAP v6.5.6.1 (YYYY-MM-DD) OfflineIMAP v6.5.6.1 (YYYY-MM-DD)
================================= =================================
* Create SQLite database directory if it doesn't exist
yet; warn if path is not a directory (Nick Farrell,
GutHub pull #102)
* Fix mangled message headers for servers without UIDPLUS: * Fix mangled message headers for servers without UIDPLUS:
X-OfflineIMAP was added with preceeding '\n' instead of X-OfflineIMAP was added with preceeding '\n' instead of
'\r\n' just before message was uploaded to the IMAP server. '\r\n' just before message was uploaded to the IMAP server.
@ -17,6 +21,8 @@ OfflineIMAP v6.5.6.1 (YYYY-MM-DD)
* Various fixes in documentation. * Various fixes in documentation.
* Fix unbounded recursion during flag update (Josh Berry).
OfflineIMAP v6.5.6 (2014-05-14) OfflineIMAP v6.5.6 (2014-05-14)
=============================== ===============================

View File

@ -236,6 +236,17 @@ class BaseFolder(object):
You must call cachemessagelist() before calling this function!""" You must call cachemessagelist() before calling this function!"""
raise NotImplementedError raise NotImplementedError
def msglist_item_initializer(self, uid):
"""
Returns value for empty messagelist element with given UID.
This function must initialize all fields of messagelist item
and must be called every time when one creates new messagelist
entry to ensure that all fields that must be present are present.
"""
raise NotImplementedError
def uidexists(self, uid): def uidexists(self, uid):
"""Returns True if uid exists""" """Returns True if uid exists"""
return uid in self.getmessagelist() return uid in self.getmessagelist()

View File

@ -110,6 +110,11 @@ class GmailFolder(IMAPFolder):
else: else:
return set() return set()
# Interface from BaseFolder
def msglist_item_initializer(self, uid):
return {'uid': uid, 'flags': set(), 'labels': set(), 'time': 0}
# TODO: merge this code with the parent's cachemessagelist: # TODO: merge this code with the parent's cachemessagelist:
# TODO: they have too much common logics. # TODO: they have too much common logics.
def cachemessagelist(self): def cachemessagelist(self):
@ -152,6 +157,7 @@ class GmailFolder(IMAPFolder):
minor = 1) minor = 1)
else: else:
uid = long(options['UID']) uid = long(options['UID'])
self.messagelist[uid] = self.msglist_item_initializer(uid)
flags = imaputil.flagsimap2maildir(options['FLAGS']) flags = imaputil.flagsimap2maildir(options['FLAGS'])
m = re.search('\(([^\)]*)\)', options['X-GM-LABELS']) m = re.search('\(([^\)]*)\)', options['X-GM-LABELS'])
if m: if m:

View File

@ -56,6 +56,13 @@ class GmailMaildirFolder(MaildirFolder):
return True return True
return False #Nope, nothing changed return False #Nope, nothing changed
# Interface from BaseFolder
def msglist_item_initializer(self, uid):
return {'flags': set(), 'labels': set(), 'labels_cached': False,
'filename': '/no-dir/no-such-file/', 'mtime': 0}
def cachemessagelist(self): def cachemessagelist(self):
if self.messagelist is None: if self.messagelist is None:
self.messagelist = self._scanfolder() self.messagelist = self._scanfolder()
@ -66,9 +73,10 @@ class GmailMaildirFolder(MaildirFolder):
filepath = os.path.join(self.getfullname(), msg['filename']) filepath = os.path.join(self.getfullname(), msg['filename'])
msg['mtime'] = long(os.stat(filepath).st_mtime) msg['mtime'] = long(os.stat(filepath).st_mtime)
def getmessagelabels(self, uid): def getmessagelabels(self, uid):
# Labels are not cached in cachemessagelist because it is too slow. # Labels are not cached in cachemessagelist because it is too slow.
if not 'labels' in self.messagelist[uid]: if not self.messagelist[uid]['labels_cached']:
filename = self.messagelist[uid]['filename'] filename = self.messagelist[uid]['filename']
filepath = os.path.join(self.getfullname(), filename) filepath = os.path.join(self.getfullname(), filename)
@ -82,10 +90,11 @@ class GmailMaildirFolder(MaildirFolder):
self.messagelist[uid]['labels'] = \ self.messagelist[uid]['labels'] = \
imaputil.labels_from_header(self.labelsheader, imaputil.labels_from_header(self.labelsheader,
self.getmessageheader(content, self.labelsheader)) self.getmessageheader(content, self.labelsheader))
self.messagelist[uid]['labels_cached'] = True
return self.messagelist[uid]['labels'] return self.messagelist[uid]['labels']
def getmessagemtime(self, uid): def getmessagemtime(self, uid):
if not 'mtime' in self.messagelist[uid]: if not 'mtime' in self.messagelist[uid]:
return 0 return 0

View File

@ -202,6 +202,10 @@ class IMAPFolder(BaseFolder):
return msgsToFetch return msgsToFetch
# Interface from BaseFolder
def msglist_item_initializer(self, uid):
return {'uid': uid, 'flags': set(), 'time': 0}
# Interface from BaseFolder # Interface from BaseFolder
def cachemessagelist(self): def cachemessagelist(self):
@ -239,6 +243,7 @@ class IMAPFolder(BaseFolder):
minor = 1) minor = 1)
else: else:
uid = long(options['UID']) uid = long(options['UID'])
self.messagelist[uid] = self.msglist_item_initializer(uid)
flags = imaputil.flagsimap2maildir(options['FLAGS']) flags = imaputil.flagsimap2maildir(options['FLAGS'])
rtime = imaplibutil.Internaldate2epoch(messagestr) rtime = imaplibutil.Internaldate2epoch(messagestr)
self.messagelist[uid] = {'uid': uid, 'flags': flags, 'time': rtime} self.messagelist[uid] = {'uid': uid, 'flags': flags, 'time': rtime}
@ -641,7 +646,8 @@ class IMAPFolder(BaseFolder):
if imapobj: self.imapserver.releaseconnection(imapobj) if imapobj: self.imapserver.releaseconnection(imapobj)
if uid: # avoid UID FETCH 0 crash happening later on if uid: # avoid UID FETCH 0 crash happening later on
self.messagelist[uid] = {'uid': uid, 'flags': flags} self.messagelist[uid] = self.msglist_item_initializer(uid)
self.messagelist[uid]['flags'] = flags
self.ui.debug('imap', 'savemessage: returning new UID %d' % uid) self.ui.debug('imap', 'savemessage: returning new UID %d' % uid)
return uid return uid
@ -755,14 +761,7 @@ class IMAPFolder(BaseFolder):
def deletemessagesflags(self, uidlist, flags): def deletemessagesflags(self, uidlist, flags):
self.__processmessagesflags('-', uidlist, flags) self.__processmessagesflags('-', uidlist, flags)
def __processmessagesflags(self, operation, uidlist, flags): def __processmessagesflags_real(self, operation, uidlist, flags):
# XXX: should really iterate over batches of 100 UIDs
if len(uidlist) > 101:
# Hack for those IMAP servers with a limited line length
self.__processmessagesflags(operation, uidlist[:100], flags)
self.__processmessagesflags(operation, uidlist[100:], flags)
return
imapobj = self.imapserver.acquireconnection() imapobj = self.imapserver.acquireconnection()
try: try:
try: try:
@ -804,6 +803,16 @@ class IMAPFolder(BaseFolder):
elif operation == '-': elif operation == '-':
self.messagelist[uid]['flags'] -= flags self.messagelist[uid]['flags'] -= flags
def __processmessagesflags(self, operation, uidlist, flags):
# Hack for those IMAP servers with a limited line length
batch_size = 100
for i in xrange(0, len(uidlist), batch_size):
self.__processmessagesflags_real(operation,
uidlist[i:i + batch_size], flags)
return
# Interface from BaseFolder # Interface from BaseFolder
def change_message_uid(self, uid, new_uid): def change_message_uid(self, uid, new_uid):
"""Change the message from existing uid to new_uid """Change the message from existing uid to new_uid

View File

@ -57,6 +57,11 @@ class LocalStatusFolder(BaseFolder):
os.unlink(self.filename) os.unlink(self.filename)
# Interface from BaseFolder
def msglist_item_initializer(self, uid):
return {'uid': uid, 'flags': set(), 'labels': set(), 'time': 0, 'mtime': 0}
def readstatus_v1(self, fp): def readstatus_v1(self, fp):
""" """
Read status folder in format version 1. Read status folder in format version 1.
@ -76,7 +81,8 @@ class LocalStatusFolder(BaseFolder):
(line, self.filename) (line, self.filename)
self.ui.warn(errstr) self.ui.warn(errstr)
raise ValueError(errstr) raise ValueError(errstr)
self.messagelist[uid] = {'uid': uid, 'flags': flags, 'mtime': 0, 'labels': set()} self.messagelist[uid] = self.msglist_item_initializer(uid)
self.messagelist[uid]['flags'] = flags
def readstatus(self, fp): def readstatus(self, fp):
@ -100,7 +106,10 @@ class LocalStatusFolder(BaseFolder):
(line, self.filename) (line, self.filename)
self.ui.warn(errstr) self.ui.warn(errstr)
raise ValueError(errstr) raise ValueError(errstr)
self.messagelist[uid] = {'uid': uid, 'flags': flags, 'mtime': mtime, 'labels': labels} self.messagelist[uid] = self.msglist_item_initializer(uid)
self.messagelist[uid]['flags'] = flags
self.messagelist[uid]['mtime'] = mtime
self.messagelist[uid]['labels'] = labels
# Interface from BaseFolder # Interface from BaseFolder
@ -197,7 +206,11 @@ class LocalStatusFolder(BaseFolder):
self.savemessageflags(uid, flags) self.savemessageflags(uid, flags)
return uid return uid
self.messagelist[uid] = {'uid': uid, 'flags': flags, 'time': rtime, 'mtime': mtime, 'labels': labels} self.messagelist[uid] = self.msglist_item_initializer(uid)
self.messagelist[uid]['flags'] = flags
self.messagelist[uid]['time'] = rtime
self.messagelist[uid]['mtime'] = mtime
self.messagelist[uid]['labels'] = labels
self.save() self.save()
return uid return uid

View File

@ -14,7 +14,7 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software # along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import os.path import os
import re import re
from threading import Lock from threading import Lock
from .Base import BaseFolder from .Base import BaseFolder
@ -51,6 +51,12 @@ class LocalStatusSQLiteFolder(BaseFolder):
self._newfolder = False # flag if the folder is new self._newfolder = False # flag if the folder is new
dirname = os.path.dirname(self.filename)
if not os.path.exists(dirname):
os.makedirs(dirname)
if not os.path.isdir(dirname):
raise UserWarning("SQLite database path '%s' is not a directory." % dirname)
# dblock protects against concurrent writes in same connection # dblock protects against concurrent writes in same connection
self._dblock = Lock() self._dblock = Lock()
@ -178,14 +184,23 @@ class LocalStatusSQLiteFolder(BaseFolder):
self._newfolder = True self._newfolder = True
# Interface from BaseFolder
def msglist_item_initializer(self, uid):
return {'uid': uid, 'flags': set(), 'labels': set(), 'time': 0, 'mtime': 0}
# Interface from BaseFolder # Interface from BaseFolder
def cachemessagelist(self): def cachemessagelist(self):
self.messagelist = {} self.messagelist = {}
cursor = self.connection.execute('SELECT id,flags,mtime,labels from status') cursor = self.connection.execute('SELECT id,flags,mtime,labels from status')
for row in cursor: for row in cursor:
uid = row[0]
self.messagelist[uid] = self.msglist_item_initializer(uid)
flags = set(row[1]) flags = set(row[1])
labels = set([lb.strip() for lb in row[3].split(',') if len(lb.strip()) > 0]) labels = set([lb.strip() for lb in row[3].split(',') if len(lb.strip()) > 0])
self.messagelist[row[0]] = {'uid': row[0], 'flags': flags, 'mtime': row[2], 'labels': labels} self.messagelist[uid]['flags'] = flags
self.messagelist[uid]['labels'] = labels
self.messagelist[uid]['mtime'] = row[2]
# Interface from LocalStatusFolder # Interface from LocalStatusFolder
def save(self): def save(self):
@ -265,6 +280,7 @@ class LocalStatusSQLiteFolder(BaseFolder):
self.savemessageflags(uid, flags) self.savemessageflags(uid, flags)
return uid return uid
self.messagelist[uid] = self.msglist_item_initializer(uid)
self.messagelist[uid] = {'uid': uid, 'flags': flags, 'time': rtime, 'mtime': mtime, 'labels': labels} self.messagelist[uid] = {'uid': uid, 'flags': flags, 'time': rtime, 'mtime': mtime, 'labels': labels}
flags = ''.join(sorted(flags)) flags = ''.join(sorted(flags))
labels = ', '.join(sorted(labels)) labels = ', '.join(sorted(labels))
@ -272,9 +288,11 @@ class LocalStatusSQLiteFolder(BaseFolder):
(uid,flags,mtime,labels)) (uid,flags,mtime,labels))
return uid return uid
# Interface from BaseFolder # Interface from BaseFolder
def savemessageflags(self, uid, flags): def savemessageflags(self, uid, flags):
self.messagelist[uid] = {'uid': uid, 'flags': flags} assert self.uidexists(uid)
self.messagelist[uid]['flags'] = flags
flags = ''.join(sorted(flags)) flags = ''.join(sorted(flags))
self.__sql_write('UPDATE status SET flags=? WHERE id=?',(flags,uid)) self.__sql_write('UPDATE status SET flags=? WHERE id=?',(flags,uid))

View File

@ -191,7 +191,9 @@ class MaildirFolder(BaseFolder):
else: else:
uid = long(uidmatch.group(1)) uid = long(uidmatch.group(1))
# 'filename' is 'dirannex/filename', e.g. cur/123,U=1,FMD5=1:2,S # 'filename' is 'dirannex/filename', e.g. cur/123,U=1,FMD5=1:2,S
retval[uid] = {'flags': flags, 'filename': filepath} retval[uid] = self.msglist_item_initializer(uid)
retval[uid]['flags'] = flags
retval[uid]['filename'] = filepath
return retval return retval
# Interface from BaseFolder # Interface from BaseFolder
@ -208,6 +210,12 @@ class MaildirFolder(BaseFolder):
return True return True
return False #Nope, nothing changed return False #Nope, nothing changed
# Interface from BaseFolder
def msglist_item_initializer(self, uid):
return {'flags': set(), 'filename': '/no-dir/no-such-file/'}
# Interface from BaseFolder # Interface from BaseFolder
def cachemessagelist(self): def cachemessagelist(self):
if self.messagelist is None: if self.messagelist is None:
@ -319,7 +327,9 @@ class MaildirFolder(BaseFolder):
if rtime != None: if rtime != None:
os.utime(os.path.join(self.getfullname(), tmpname), (rtime, rtime)) os.utime(os.path.join(self.getfullname(), tmpname), (rtime, rtime))
self.messagelist[uid] = {'flags': flags, 'filename': tmpname} self.messagelist[uid] = self.msglist_item_initializer(uid)
self.messagelist[uid]['flags'] = flags
self.messagelist[uid]['filename'] = tmpname
# savemessageflags moves msg to 'cur' or 'new' as appropriate # savemessageflags moves msg to 'cur' or 'new' as appropriate
self.savemessageflags(uid, flags) self.savemessageflags(uid, flags)
self.ui.debug('maildir', 'savemessage: returning uid %d' % uid) self.ui.debug('maildir', 'savemessage: returning uid %d' % uid)
@ -339,6 +349,9 @@ class MaildirFolder(BaseFolder):
Note that this function does not check against dryrun settings, Note that this function does not check against dryrun settings,
so you need to ensure that it is never called in a so you need to ensure that it is never called in a
dryrun mode.""" dryrun mode."""
assert uid in self.messagelist
oldfilename = self.messagelist[uid]['filename'] oldfilename = self.messagelist[uid]['filename']
dir_prefix, filename = os.path.split(oldfilename) dir_prefix, filename = os.path.split(oldfilename)
# If a message has been seen, it goes into 'cur' # If a message has been seen, it goes into 'cur'