Merge branch 'next'

This commit is contained in:
Sebastian Spaeth 2011-09-29 11:21:38 +02:00
commit 105012957f
24 changed files with 467 additions and 288 deletions

View File

@ -15,11 +15,6 @@ New Features
Changes
-------
Bug Fixes
---------
Pending for the next major release
==================================
* UIs get shorter and nicer names. (API changing)

View File

@ -11,6 +11,65 @@ ChangeLog
on releases. And because I'm lazy, it will also be used as a draft for the
releases announces.
OfflineIMAP v6.4.0 (2011-09-29)
===============================
This is the first stable release to support the forward-compatible per-account locks and remote folder creation that has been introduced in the 6.3.5 series.
* Various regression and bug fixes from the last couple of RCs
OfflineIMAP v6.3.5-rc3 (2011-09-21)
===================================
Changes
-------
* Refresh server capabilities after login, so we know that Gmail
supports UIDPLUS (it only announces that after login, not
before). This prevents us from adding custom headers to Gmail uploads.
Bug Fixes
---------
* Fix the creation of folders on remote repositories, which was still
botched on rc2.
OfflineIMAP v6.3.5-rc2 (2011-09-19)
===================================
New Features
------------
* Implement per-account locking, so that it will possible to sync
different accounts at the same time. The old global lock is still in
place for backward compatibility reasons (to be able to run old and
new versions of OfflineImap concurrently) and will be removed in the
future. Starting with this version, OfflineImap will be
forward-compatible with the per-account locking style.
* Implement RFC 2595 LOGINDISABLED. Warn the user and abort when we
attempt a plaintext login but the server has explicitly disabled
plaintext logins rather than crashing.
* Folders will now also be automatically created on the REMOTE side of
an account if they exist on the local side. Use the folderfilters
setting on the local side to prevent some folders from migrating to
the remote side. Also, if you have a nametrans setting on the remote
repository, you might need a nametrans setting on the local repository
that leads to the original name (reverse nametrans).
Changes
-------
* Documentation improvements concerning 'restoreatime' and some code cleanup
* Maildir repositories now also respond to folderfilter= configurations.
Bug Fixes
---------
* New emails are not created with "-rwxr-xr-x" but as "-rw-r--r--"
anymore, fixing a regression in 6.3.4.
OfflineIMAP v6.3.5-rc1 (2011-09-12)
===================================

View File

@ -476,7 +476,8 @@ To only get the All Mail folder from a Gmail account, you would e.g. do::
Another nametrans transpose example
-----------------------------------
Put everything in a GMX. subfolder except for the boxes INBOX, Draft, and Sent which should keep the same name::
Put everything in a GMX. subfolder except for the boxes INBOX, Draft,
and Sent which should keep the same name::
nametrans: lambda folder: folder if folder in ['INBOX', 'Drafts', 'Sent'] \
else re.sub(r'^', r'GMX.', folder)
@ -484,7 +485,9 @@ Put everything in a GMX. subfolder except for the boxes INBOX, Draft, and Sent w
2 IMAP using name translations
------------------------------
Synchronizing 2 IMAP accounts to local Maildirs that are "next to each other", so that mutt can work on both. Full email setup described by Thomas Kahle at `http://dev.gentoo.org/~tomka/mail.html`_
Synchronizing 2 IMAP accounts to local Maildirs that are "next to each
other", so that mutt can work on both. Full email setup described by
Thomas Kahle at `http://dev.gentoo.org/~tomka/mail.html`_
offlineimap.conf::
@ -534,11 +537,25 @@ offlineimap.conf::
ssl = yes
maxconnections = 2
One of the coolest things about offlineimap is that you can inject arbitrary python code. The file specified with::
One of the coolest things about offlineimap is that you can call
arbitrary python code from your configuration. To do this, specify a
pythonfile with::
pythonfile=~/bin/offlineimap-helpers.py
contains python functions that I used for two purposes: Fetching passwords from the gnome-keyring and translating folder names on the server to local foldernames. The python file should contain all the functions that are called here. get_username and get_password are part of the interaction with gnome-keyring and not printed here. Find them in the example file that is in the tarball or here. The folderfilter is a lambda term that, well, filters which folders to get. `oimaptransfolder_acc2` translates remote folders into local folders with a very simple logic. The `INBOX` folder will simply have the same name as the account while any other folder will have the account name and a dot as a prefix. offlineimap handles the renaming correctly in both directions::
Your pythonfile needs to contain implementations for the functions
that you want to use in offflineimaprc. The example uses it for two
purposes: Fetching passwords from the gnome-keyring and translating
folder names on the server to local foldernames. An example
implementation of get_username and get_password showing how to query
gnome-keyring is contained in
`http://dev.gentoo.org/~tomka/mail-setup.tar.bz2`_ The folderfilter is
a lambda term that, well, filters which folders to get. The function
`oimaptransfolder_acc2` translates remote folders into local folders
with a very simple logic. The `INBOX` folder will have the same name
as the account while any other folder will have the account name and a
dot as a prefix. This is useful for hierarchichal display in mutt.
Offlineimap handles the renaming correctly in both directions::
import re
def oimaptransfolder_acc1(foldername):

View File

@ -285,12 +285,12 @@ localfolders = ~/Test
sep = .
# Some users on *nix platforms may not want the atime (last access
# time) to be modified by OfflineIMAP. In these cases, they would
# want to set restoreatime to yes. OfflineIMAP will make an effort
# to not touch the atime if you do that.
# Some users may not want the atime (last access time) of folders to be
# modified by OfflineIMAP. If 'restoreatime' is set to yes, OfflineIMAP
# will restore the atime of the "new" and "cur" folders in each maildir
# folder to their original value after each sync.
#
# In most cases, the default of no should be sufficient.
# In nearly all cases, the default should be fine.
restoreatime = no

View File

@ -24,26 +24,25 @@ class CustomConfigParser(SafeConfigParser):
"""Same as config.get, but returns the "default" option if there
is no such option specified."""
if self.has_option(section, option):
return apply(self.get, [section, option] + list(args), kwargs)
return self.get(*(section, option) + args, **kwargs)
else:
return default
def getdefaultint(self, section, option, default, *args, **kwargs):
if self.has_option(section, option):
return apply(self.getint, [section, option] + list(args), kwargs)
return self.getint (*(section, option) + args, **kwargs)
else:
return default
def getdefaultfloat(self, section, option, default, *args, **kwargs):
if self.has_option(section, option):
return apply(self.getfloat, [section, option] + list(args), kwargs)
return self.getfloat(*(section, option) + args, **kwargs)
else:
return default
def getdefaultboolean(self, section, option, default, *args, **kwargs):
if self.has_option(section, option):
return apply(self.getboolean, [section, option] + list(args),
kwargs)
return self.getboolean(*(section, option) + args, **kwargs)
else:
return default
@ -91,9 +90,9 @@ class ConfigHelperMixin:
def _confighelper_runner(self, option, default, defaultfunc, mainfunc):
"""Return config value for getsection()"""
if default == CustomConfigDefault:
return apply(mainfunc, [self.getsection(), option])
return mainfunc(*[self.getsection(), option])
else:
return apply(defaultfunc, [self.getsection(), option, default])
return defaultfunc(*[self.getsection(), option, default])
def getconf(self, option,

View File

@ -1,7 +1,7 @@
__all__ = ['OfflineImap']
__productname__ = 'OfflineIMAP'
__version__ = "6.3.5-rc1"
__version__ = "6.4.0"
__copyright__ = "Copyright 2002-2011 John Goerzen & contributors"
__author__ = "John Goerzen"
__author_email__= "john@complete.org"

View File

@ -25,6 +25,11 @@ import os
from sys import exc_info
import traceback
try:
import fcntl
except:
pass # ok if this fails, we can do without
def getaccountlist(customconfig):
return customconfig.getsectionlist('Account')
@ -159,6 +164,36 @@ class SyncableAccount(Account):
functions :meth:`syncrunner`, :meth:`sync`, :meth:`syncfolders`,
used for syncing."""
def __init__(self, *args, **kwargs):
Account.__init__(self, *args, **kwargs)
self._lockfd = None
self._lockfilepath = os.path.join(self.config.getmetadatadir(),
"%s.lock" % self)
def lock(self):
"""Lock the account, throwing an exception if it is locked already"""
# Take a new-style per-account lock
self._lockfd = open(self._lockfilepath, 'w')
try:
fcntl.lockf(self._lockfd, fcntl.LOCK_EX|fcntl.LOCK_NB)
except NameError:
#fcntl not available (Windows), disable file locking... :(
pass
except IOError:
self._lockfd.close()
raise OfflineImapError("Could not lock account %s." % self,
OfflineImapError.ERROR.REPO)
def unlock(self):
"""Unlock the account, deleting the lock file"""
#If we own the lock file, delete it
if self._lockfd and not self._lockfd.closed:
self._lockfd.close()
try:
os.unlink(self._lockfilepath)
except OSError:
pass #Failed to delete for some reason.
def syncrunner(self):
self.ui.registerthread(self.name)
self.ui.acct(self.name)
@ -175,6 +210,7 @@ class SyncableAccount(Account):
while looping:
try:
try:
self.lock()
self.sync()
except (KeyboardInterrupt, SystemExit):
raise
@ -194,6 +230,7 @@ class SyncableAccount(Account):
if self.refreshperiod:
looping = 3
finally:
self.unlock()
if looping and self.sleeper() >= 2:
looping = 0
self.ui.acctdone(self.name)
@ -231,12 +268,16 @@ class SyncableAccount(Account):
localrepos = self.localrepos
statusrepos = self.statusrepos
# replicate the folderstructure from REMOTE to LOCAL
if not localrepos.getconf('readonly', False):
if not localrepos.getconfboolean('readonly', False):
self.ui.syncfolders(remoterepos, localrepos)
remoterepos.syncfoldersto(localrepos, statusrepos)
# iterate through all folders on the remote repo and sync
for remotefolder in remoterepos.getfolders():
if not remotefolder.sync_this:
self.ui.debug('', "Not syncing filtered remote folder '%s'"
"[%s]" % (remotefolder, remoterepos))
continue # Filtered out remote folder
thread = InstanceLimitedThread(\
instancename = 'FOLDER_' + self.remoterepos.getname(),
target = syncfolder,
@ -286,7 +327,9 @@ class SyncableAccount(Account):
def syncfolder(accountname, remoterepos, remotefolder, localrepos,
statusrepos, quick):
"""This function is called as target for the
InstanceLimitedThread invokation in SyncableAccount."""
InstanceLimitedThread invokation in SyncableAccount.
Filtered folders on the remote side will not invoke this function."""
ui = getglobalui()
ui.registerthread(accountname)
try:
@ -294,6 +337,14 @@ def syncfolder(accountname, remoterepos, remotefolder, localrepos,
localfolder = localrepos.\
getfolder(remotefolder.getvisiblename().\
replace(remoterepos.getsep(), localrepos.getsep()))
#Filtered folders on the remote side will not invoke this
#function, but we need to NOOP if the local folder is filtered
#out too:
if not localfolder.sync_this:
ui.debug('', "Not syncing filtered local folder '%s'" \
% localfolder)
return
# Write the mailboxes
mbnames.add(accountname, localfolder.getvisiblename())
@ -345,7 +396,7 @@ def syncfolder(accountname, remoterepos, remotefolder, localrepos,
remotefolder.getmessagecount())
# Synchronize remote changes.
if not localrepos.getconf('readonly', False):
if not localrepos.getconfboolean('readonly', False):
ui.syncingmessages(remoterepos, remotefolder, localrepos, localfolder)
remotefolder.syncmessagesto(localfolder, statusfolder)
else:
@ -353,7 +404,7 @@ def syncfolder(accountname, remoterepos, remotefolder, localrepos,
% localrepos.getname())
# Synchronize local changes
if not remoterepos.getconf('readonly', False):
if not remoterepos.getconfboolean('readonly', False):
ui.syncingmessages(localrepos, localfolder, remoterepos, remotefolder)
localfolder.syncmessagesto(remotefolder, statusfolder)
else:
@ -369,8 +420,15 @@ def syncfolder(accountname, remoterepos, remotefolder, localrepos,
if e.severity > OfflineImapError.ERROR.FOLDER:
raise
else:
ui.error(e, exc_info()[2], msg = "Aborting folder sync '%s' "
"[acc: '%s']" % (localfolder, accountname))
#if the initial localfolder assignement bailed out, the localfolder var will not be available, so we need
ui.error(e, exc_info()[2], msg = "Aborting sync, folder '%s' "
"[acc: '%s']" % (
remotefolder.getvisiblename().\
replace(remoterepos.getsep(), localrepos.getsep()),
accountname))
# we reconstruct foldername above rather than using
# localfolder, as the localfolder var is not
# available if assignment fails.
except Exception, e:
ui.error(e, msg = "ERROR in syncfolder for %s folder %s: %s" % \
(accountname,remotefolder.getvisiblename(),

View File

@ -28,8 +28,23 @@ except NameError:
from sets import Set as set
class BaseFolder(object):
def __init__(self):
def __init__(self, name, repository):
"""
:para name: Path & name of folder minus root or reference
:para repository: Repository() in which the folder is.
"""
self.sync_this = True
"""Should this folder be included in syncing?"""
self.ui = getglobalui()
self.name = name
self.repository = repository
self.visiblename = repository.nametrans(name)
# In case the visiblename becomes '.' (top-level) we use '' as
# that is the name that e.g. the Maildir scanning will return
# for the top-level dir.
if self.visiblename == '.':
self.visiblename = ''
self.config = repository.getconfig()
def getname(self):
"""Returns name"""
@ -38,6 +53,11 @@ class BaseFolder(object):
def __str__(self):
return self.name
@property
def accountname(self):
"""Account name as string"""
return self.repository.accountname
def suggeststhreads(self):
"""Returns true if this folder suggests using threads for actions;
false otherwise. Probably only IMAP will return true."""
@ -55,7 +75,8 @@ class BaseFolder(object):
return 1
def getvisiblename(self):
return self.name
"""The nametrans-transposed name of the folder's name"""
return self.visiblename
def getrepository(self):
"""Returns the repository object that this folder is within."""
@ -233,7 +254,7 @@ class BaseFolder(object):
# self.getmessage(). So, don't call self.getmessage unless
# really needed.
if register: # output that we start a new thread
self.ui.registerthread(self.getaccountname())
self.ui.registerthread(self.accountname)
try:
message = None
@ -289,7 +310,7 @@ class BaseFolder(object):
self.ui.error(e, exc_info()[2])
except Exception, e:
self.ui.error(e, "Copying message %s [acc: %s]:\n %s" %\
(uid, self.getaccountname(),
(uid, self.accountname,
traceback.format_exc()))
raise #raise on unknown errors, so we can fix those
@ -437,5 +458,5 @@ class BaseFolder(object):
self.ui.error(e, exc_info()[2])
except Exception, e:
self.ui.error(e, exc_info()[2], "Syncing folder %s [acc: %s]" %\
(self, self.getaccountname()))
(self, self.accountname))
raise # raise unknown Exceptions so we can fix them

View File

@ -33,13 +33,12 @@ class GmailFolder(IMAPFolder):
http://mail.google.com/support/bin/answer.py?answer=77657&topic=12815
"""
def __init__(self, imapserver, name, visiblename, accountname, repository):
def __init__(self, imapserver, name, repository):
super(GmailFolder, self).__init__(imapserver, name, repository)
self.realdelete = repository.getrealdelete(name)
self.trash_folder = repository.gettrashfolder(name)
#: Gmail will really delete messages upon EXPUNGE in these folders
self.real_delete_folders = [ self.trash_folder, repository.getspamfolder() ]
IMAPFolder.__init__(self, imapserver, name, visiblename, \
accountname, repository)
def deletemessages_noconvert(self, uidlist):
uidlist = [uid for uid in uidlist if uid in self.messagelist]

View File

@ -32,19 +32,15 @@ except NameError:
class IMAPFolder(BaseFolder):
def __init__(self, imapserver, name, visiblename, accountname, repository):
self.config = imapserver.config
def __init__(self, imapserver, name, repository):
name = imaputil.dequote(name)
super(IMAPFolder, self).__init__(name, repository)
self.expunge = repository.getexpunge()
self.name = imaputil.dequote(name)
self.root = None # imapserver.root
self.sep = imapserver.delim
self.imapserver = imapserver
self.messagelist = None
self.visiblename = visiblename
self.accountname = accountname
self.repository = repository
self.randomgenerator = random.Random()
BaseFolder.__init__(self)
#self.ui is set in BaseFolder
def selectro(self, imapobj):
@ -61,9 +57,6 @@ class IMAPFolder(BaseFolder):
except imapobj.readonly:
imapobj.select(self.getfullname(), readonly = 1)
def getaccountname(self):
return self.accountname
def suggeststhreads(self):
return 1
@ -73,9 +66,6 @@ class IMAPFolder(BaseFolder):
def getcopyinstancelimit(self):
return 'MSGCOPY_' + self.repository.getname()
def getvisiblename(self):
return self.visiblename
def getuidvalidity(self):
imapobj = self.imapserver.acquireconnection()
try:
@ -89,26 +79,34 @@ class IMAPFolder(BaseFolder):
# 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
imaptype, imapdata = imapobj.select(self.getfullname(), readonly = 1, force = 1)
# 1. Some mail servers do not return an EXISTS response
# if the folder is empty. 2. ZIMBRA servers can return
# multiple EXISTS replies in the form 500, 1000, 1500,
# 1623 so check for potentially multiple replies.
if imapdata == [None]:
return True
maxmsgid = 0
for msgid in imapdata:
maxmsgid = max(long(msgid), maxmsgid)
# Different number of messages than last time?
if maxmsgid != statusfolder.getmessagecount():
return True
finally:
self.imapserver.releaseconnection(imapobj)
retry = True # Should we attempt another round or exit?
while retry:
retry = False
imapobj = self.imapserver.acquireconnection()
try:
# Select folder and get number of messages
restype, imapdata = imapobj.select(self.getfullname(), True,
True)
except OfflineImapError, e:
# retry on dropped connections, raise otherwise
self.imapserver.releaseconnection(imapobj, True)
if e.severity == OfflineImapError.ERROR.FOLDER_RETRY:
retry = True
else: raise
finally:
self.imapserver.releaseconnection(imapobj)
# 1. Some mail servers do not return an EXISTS response
# if the folder is empty. 2. ZIMBRA servers can return
# multiple EXISTS replies in the form 500, 1000, 1500,
# 1623 so check for potentially multiple replies.
if imapdata == [None]:
return True
maxmsgid = 0
for msgid in imapdata:
maxmsgid = max(long(msgid), maxmsgid)
# Different number of messages than last time?
if maxmsgid != statusfolder.getmessagecount():
return True
return False
def cachemessagelist(self):
@ -120,7 +118,7 @@ class IMAPFolder(BaseFolder):
imapobj = self.imapserver.acquireconnection()
try:
res_type, imapdata = imapobj.select(self.getfullname(), True)
res_type, imapdata = imapobj.select(self.getfullname(), True, True)
if imapdata == [None] or imapdata[0] == '0':
# Empty folder, no need to populate message list
return
@ -211,9 +209,9 @@ class IMAPFolder(BaseFolder):
res_type, data = imapobj.uid('fetch', str(uid),
'(BODY.PEEK[])')
fails_left = 0
except imapobj.abort(), e:
except imapobj.abort, e:
# Release dropped connection, and get a new one
self.imapserver.releaseconnection(imapobj)
self.imapserver.releaseconnection(imapobj, True)
imapobj = self.imapserver.acquireconnection()
self.ui.error(e, exc_info()[2])
fails_left -= 1
@ -495,11 +493,10 @@ class IMAPFolder(BaseFolder):
self.savemessageflags(uid, flags)
return uid
retry_left = 2 # succeeded in APPENDING?
imapobj = self.imapserver.acquireconnection()
try:
success = False # succeeded in APPENDING?
while not success:
while retry_left:
# UIDPLUS extension provides us with an APPENDUID response.
use_uidplus = 'UIDPLUS' in imapobj.capabilities
@ -536,21 +533,27 @@ class IMAPFolder(BaseFolder):
(typ, dat) = imapobj.append(self.getfullname(),
imaputil.flagsmaildir2imap(flags),
date, content)
success = True
retry_left = 0 # Mark as success
except imapobj.abort, e:
# connection has been reset, release connection and retry.
self.ui.error(e, exc_info()[2])
retry_left -= 1
self.imapserver.releaseconnection(imapobj, True)
imapobj = self.imapserver.acquireconnection()
except imapobj.error, e:
# If the server responds with 'BAD', append() raise()s directly.
# So we need to prepare a response ourselves.
typ, dat = 'BAD', str(e)
if typ != 'OK': #APPEND failed
raise OfflineImapError("Saving msg in folder '%s', repository "
"'%s' failed. Server reponded; %s %s\nMessage content was:"
" %s" % (self, self.getrepository(), typ, dat, dbg_output),
if not retry_left:
raise OfflineImapError("Saving msg in folder '%s', "
"repository '%s' failed. Server reponded: %s\n"
"Message content was: %s" %
(self, self.getrepository(), str(e), dbg_output),
OfflineImapError.ERROR.MESSAGE)
self.ui.error(e, exc_info()[2])
except imapobj.error, e: # APPEND failed
# If the server responds with 'BAD', append()
# raise()s directly. So we catch that too.
raise OfflineImapError("Saving msg folder '%s', repo '%s'"
"failed. Server reponded: %s\nMessage content was: "
"%s" % (self, self.getrepository(), str(e), dbg_output),
OfflineImapError.ERROR.MESSAGE)
# Checkpoint. Let it write out stuff, etc. Eg searches for
# just uploaded messages won't work if we don't do this.
(typ,dat) = imapobj.check()

View File

@ -26,22 +26,15 @@ except NameError:
magicline = "OFFLINEIMAP LocalStatus CACHE DATA - DO NOT MODIFY - FORMAT 1"
class LocalStatusFolder(BaseFolder):
def __init__(self, root, name, repository, accountname, config):
self.name = name
self.root = root
def __init__(self, name, repository):
super(LocalStatusFolder, self).__init__(name, repository)
self.sep = '.'
self.config = config
self.filename = os.path.join(root, self.getfolderbasename())
self.filename = os.path.join(self.getroot(), self.getfolderbasename())
self.messagelist = {}
self.repository = repository
self.savelock = threading.Lock()
self.doautosave = config.getdefaultboolean("general", "fsync", False)
self.doautosave = self.config.getdefaultboolean("general", "fsync",
False)
"""Should we perform fsyncs as often as possible?"""
self.accountname = accountname
super(LocalStatusFolder, self).__init__()
def getaccountname(self):
return self.accountname
def storesmessages(self):
return 0
@ -53,7 +46,7 @@ class LocalStatusFolder(BaseFolder):
return self.name
def getroot(self):
return self.root
return self.repository.root
def getsep(self):
return self.sep

View File

@ -46,12 +46,8 @@ class LocalStatusSQLiteFolder(LocalStatusFolder):
#current version of our db format
cur_version = 1
def __init__(self, root, name, repository, accountname, config):
super(LocalStatusSQLiteFolder, self).__init__(root, name,
repository,
accountname,
config)
def __init__(self, name, repository):
super(LocalStatusSQLiteFolder, self).__init__(name, repository)
# dblock protects against concurrent writes in same connection
self._dblock = Lock()
#Try to establish connection, no need for threadsafety in __init__

View File

@ -58,34 +58,23 @@ def gettimeseq():
timelock.release()
class MaildirFolder(BaseFolder):
def __init__(self, root, name, sep, repository, accountname, config):
self.name = name
self.config = config
self.dofsync = config.getdefaultboolean("general", "fsync", True)
def __init__(self, root, name, sep, repository):
super(MaildirFolder, self).__init__(name, repository)
self.dofsync = self.config.getdefaultboolean("general", "fsync", True)
self.root = root
self.sep = sep
self.messagelist = None
self.repository = repository
self.accountname = accountname
self.wincompatible = self.config.getdefaultboolean(
"Account "+self.accountname, "maildir-windows-compatible", False)
if self.wincompatible == False:
self.infosep = ':'
else:
self.infosep = '!'
self.infosep = '!' if self.wincompatible else ':'
"""infosep is the separator between maildir name and flag appendix"""
self.flagmatchre = re.compile(self.infosep + '.*2,([A-Z]+)')
BaseFolder.__init__(self)
#self.ui is set in BaseFolder.init()
# Cache the full folder path, as we use getfullname() very often
self._fullname = os.path.join(self.getroot(), self.getname())
def getaccountname(self):
return self.accountname
def getfullname(self):
"""Return the absolute file path to the Maildir folder (sans cur|new)"""
return self._fullname
@ -176,10 +165,8 @@ class MaildirFolder(BaseFolder):
flags = set(flagmatch.group(1))
else:
flags = set()
# 'filename' is 'dirannex/filename', e.g. cur/123_U=1_FMD5=1:2,S
retval[uid] = {'uid': uid,
'flags': flags,
'filename': file}
# 'filename' is 'dirannex/filename', e.g. cur/123,U=1,FMD5=1:2,S
retval[uid] = {'flags': flags, 'filename': file}
return retval
def quickchanged(self, statusfolder):
@ -213,11 +200,10 @@ class MaildirFolder(BaseFolder):
# read it as text?
return retval.replace("\r\n", "\n")
def getmessagetime( self, uid ):
def getmessagetime(self, uid):
filename = self.messagelist[uid]['filename']
filepath = os.path.join(self.getfullname(), filename)
st = os.stat(filepath)
return st.st_mtime
return os.path.getmtime(filepath)
def savemessage(self, uid, content, flags, rtime):
# This function only ever saves to tmp/,
@ -246,7 +232,7 @@ class MaildirFolder(BaseFolder):
# open file and write it out
try:
fd = os.open(os.path.join(tmpdir, messagename),
os.O_EXCL|os.O_CREAT|os.O_WRONLY)
os.O_EXCL|os.O_CREAT|os.O_WRONLY, 0666)
except OSError, e:
if e.errno == 17:
#FILE EXISTS ALREADY
@ -267,7 +253,7 @@ class MaildirFolder(BaseFolder):
if rtime != None:
os.utime(os.path.join(tmpdir, messagename), (rtime, rtime))
self.messagelist[uid] = {'uid': uid, 'flags': set(),
self.messagelist[uid] = {'flags': set(),
'filename': os.path.join('tmp', messagename)}
# savemessageflags moves msg to 'cur' or 'new' as appropriate
self.savemessageflags(uid, flags)

View File

@ -1,6 +1,5 @@
# IMAP server support
# Copyright (C) 2002 - 2007 John Goerzen
# <jgoerzen@complete.org>
# Copyright (C) 2002 - 2011 John Goerzen & contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@ -56,11 +55,11 @@ class IMAPServer:
self.config = repos.getconfig()
self.tunnel = repos.getpreauthtunnel()
self.usessl = repos.getssl()
self.username = repos.getuser()
self.username = None if self.tunnel else repos.getuser()
self.password = None
self.passworderror = None
self.goodpassword = None
self.hostname = repos.gethost()
self.hostname = None if self.tunnel else repos.gethost()
self.port = repos.getport()
if self.port == None:
self.port = 993 if self.usessl else 143
@ -262,6 +261,12 @@ class IMAPServer:
except imapobj.error, val:
self.plainauth(imapobj)
else:
# Use plaintext login, unless
# LOGINDISABLED (RFC2595)
if 'LOGINDISABLED' in imapobj.capabilities:
raise OfflineImapError("Plaintext login "
"disabled by server. Need to use SSL?",
OfflineImapError.ERROR.REPO)
self.plainauth(imapobj)
# Would bail by here if there was a failure.
success = 1
@ -270,6 +275,11 @@ class IMAPServer:
self.passworderror = str(val)
raise
# update capabilities after login, e.g. gmail serves different ones
typ, dat = imapobj.capability()
if dat != [None]:
imapobj.capabilities = tuple(dat[-1].upper().split())
if self.delim == None:
listres = imapobj.list(self.reference, '""')[1]
if listres == [None] or listres == None:
@ -539,7 +549,7 @@ class IdleThread(object):
try:
# End IDLE mode with noop, imapobj can point to a dropped conn.
imapobj.noop()
except imapobj.abort():
except imapobj.abort:
self.ui.warn('Attempting NOOP on dropped connection %s' % \
imapobj.identifier)
self.parent.releaseconnection(imapobj, True)

View File

@ -34,16 +34,15 @@ def debug(*args):
getglobalui().debug('imap', " ".join(msg))
def dequote(string):
"""Takes a string which may or may not be quoted and returns it, unquoted.
This function does NOT consider parenthised lists to be quoted.
"""
"""Takes string which may or may not be quoted and unquotes it.
if not (string[0] == '"' and string[-1] == '"'):
return string
string = string[1:-1] # Strip off quotes.
string = string.replace('\\"', '"')
string = string.replace('\\\\', '\\')
debug("dequote() returning:", string)
It only considers double quotes. This function does NOT consider
parenthised lists to be quoted.
"""
if string and string.startswith('"') and string.endswith('"'):
string = string[1:-1] # Strip off the surrounding quotes.
string = string.replace('\\"', '"')
string = string.replace('\\\\', '\\')
return string
def flagsplit(string):

View File

@ -24,20 +24,17 @@ import signal
import socket
import logging
from optparse import OptionParser
try:
import fcntl
except ImportError:
pass #it's OK
import offlineimap
from offlineimap import accounts, threadutil, syncmaster
from offlineimap.error import OfflineImapError
from offlineimap.ui import UI_LIST, setglobalui, getglobalui
from offlineimap.CustomConfig import CustomConfigParser
try:
import fcntl
hasfcntl = 1
except:
hasfcntl = 0
lockfd = None
class OfflineImap:
"""The main class that encapsulates the high level use of OfflineImap.
@ -46,17 +43,6 @@ class OfflineImap:
oi = OfflineImap()
oi.run()
"""
def lock(self, config, ui):
global lockfd, hasfcntl
if not hasfcntl:
return
lockfd = open(config.getmetadatadir() + "/lock", "w")
try:
fcntl.flock(lockfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
except IOError:
ui.locked()
ui.terminate(1)
def run(self):
"""Parse the commandline and invoke everything"""
@ -253,7 +239,6 @@ class OfflineImap:
config.set(section, "folderfilter", folderfilter)
config.set(section, "folderincludes", folderincludes)
self.lock(config, ui)
self.config = config
def sigterm_handler(signum, frame):
@ -330,6 +315,18 @@ class OfflineImap:
#various initializations that need to be performed:
offlineimap.mbnames.init(config, syncaccounts)
#TODO: keep legacy lock for a few versions, then remove.
self._legacy_lock = open(self.config.getmetadatadir() + "/lock",
'w')
try:
fcntl.lockf(self._legacy_lock, fcntl.LOCK_EX|fcntl.LOCK_NB)
except NameError:
#fcntl not available (Windows), disable file locking... :(
pass
except IOError:
raise OfflineImapError("Could not take global lock.",
OfflineImapError.ERROR.REPO)
if options.singlethreading:
#singlethreaded
self.sync_singlethreaded(syncaccounts, config)
@ -349,8 +346,9 @@ class OfflineImap:
return
except (SystemExit):
raise
except:
ui.mainException()
except Exception, e:
ui.error(e)
ui.terminate()
def sync_singlethreaded(self, accs, config):
"""Executed if we do not want a separate syncmaster thread

View File

@ -16,19 +16,23 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import re
import os.path
import traceback
from sys import exc_info
from offlineimap import CustomConfig
from offlineimap.ui import getglobalui
from offlineimap.error import OfflineImapError
class BaseRepository(object, CustomConfig.ConfigHelperMixin):
def __init__(self, reposname, account):
self.ui = getglobalui()
self.account = account
self.config = account.getconfig()
self.name = reposname
self.localeval = account.getlocaleval()
self.accountname = self.account.getname()
self._accountname = self.account.getname()
self.uiddir = os.path.join(self.config.getmetadatadir(), 'Repository-' + self.name)
if not os.path.exists(self.uiddir):
os.mkdir(self.uiddir, 0700)
@ -39,17 +43,30 @@ class BaseRepository(object, CustomConfig.ConfigHelperMixin):
if not os.path.exists(self.uiddir):
os.mkdir(self.uiddir, 0700)
# The 'restoreatime' config parameter only applies to local Maildir
# mailboxes.
self.nametrans = lambda foldername: foldername
self.folderfilter = lambda foldername: 1
self.folderincludes = []
self.foldersort = cmp
if self.config.has_option(self.getsection(), 'nametrans'):
self.nametrans = self.localeval.eval(
self.getconf('nametrans'), {'re': re})
if self.config.has_option(self.getsection(), 'folderfilter'):
self.folderfilter = self.localeval.eval(
self.getconf('folderfilter'), {'re': re})
if self.config.has_option(self.getsection(), 'folderincludes'):
self.folderincludes = self.localeval.eval(
self.getconf('folderincludes'), {'re': re})
if self.config.has_option(self.getsection(), 'foldersort'):
self.foldersort = self.localeval.eval(
self.getconf('foldersort'), {'re': re})
def restore_atime(self):
if self.config.get('Repository ' + self.name, 'type').strip() != \
'Maildir':
return
"""Sets folders' atime back to their values after a sync
if not self.config.has_option('Repository ' + self.name, 'restoreatime') or not self.config.getboolean('Repository ' + self.name, 'restoreatime'):
return
return self.restore_folder_atimes()
Controlled by the 'restoreatime' config parameter (default
False), applies only to local Maildir mailboxes and does nothing
on all other repository types."""
pass
def connect(self):
"""Establish a connection to the remote, if necessary. This exists
@ -75,15 +92,17 @@ class BaseRepository(object, CustomConfig.ConfigHelperMixin):
def __str__(self):
return self.name
@property
def accountname(self):
"""Account name as string"""
return self._accountname
def getuiddir(self):
return self.uiddir
def getmapdir(self):
return self.mapdir
def getaccountname(self):
return self.accountname
def getsection(self):
return 'Repository ' + self.name
@ -117,7 +136,11 @@ class BaseRepository(object, CustomConfig.ConfigHelperMixin):
def syncfoldersto(self, dst_repo, status_repo):
"""Syncs the folders in this repository to those in dest.
It does NOT sync the contents of those folders."""
It does NOT sync the contents of those folders. nametrans rules
in both directions will be honored, but there are NO checks yet
that forward and backward nametrans actually match up!
Configuring nametrans on BOTH repositories therefore could lead
to infinite folder creation cycles."""
src_repo = self
src_folders = src_repo.getfolders()
dst_folders = dst_repo.getfolders()
@ -130,33 +153,66 @@ class BaseRepository(object, CustomConfig.ConfigHelperMixin):
src_repo.getsep(), dst_repo.getsep())] = folder
dst_hash = {}
for folder in dst_folders:
dst_hash[folder.getvisiblename()] = folder
dst_hash[folder.name] = folder
#
# Find new folders.
for key in src_hash.keys():
if not key in dst_hash:
# Find new folders on src_repo.
for src_name, src_folder in src_hash.iteritems():
if src_folder.sync_this and not src_name in dst_hash:
try:
dst_repo.makefolder(key)
status_repo.makefolder(key.replace(dst_repo.getsep(),
status_repo.getsep()))
except (KeyboardInterrupt):
dst_repo.makefolder(src_name)
except OfflineImapError, e:
self.ui.error(e, exc_info()[2],
"Creating folder %s on repository %s" %\
(src_name, dst_repo))
raise
except:
self.ui.warn("ERROR Attempting to create folder " \
+ key + ":" +traceback.format_exc())
status_repo.makefolder(src_name.replace(dst_repo.getsep(),
status_repo.getsep()))
# Find new folders on dst_repo.
for dst_name, dst_folder in dst_hash.iteritems():
if dst_folder.sync_this and not dst_name in src_hash:
# nametrans sanity check!
# Does nametrans back&forth lead to identical names?
#src_name is the unmodified full src_name that would be created
newsrc_name = dst_folder.getvisiblename().replace(
dst_repo.getsep(),
src_repo.getsep())
folder = self.getfolder(newsrc_name)
# would src repo filter out the new folder name? In this
# case don't create it on it:
if not self.folderfilter(newsrc_name):
self.ui.debug('', "Not creating folder '%s' (repository '%s"
"') as it would be filtered out on that repository." %
(newsrc_name, self))
continue
# apply reverse nametrans to see if we end up with the same name
newdst_name = folder.getvisiblename().replace(
src_repo.getsep(), dst_repo.getsep())
if dst_name != newdst_name:
raise OfflineImapError("INFINITE FOLDER CREATION DETECTED! "
"Folder '%s' (repository '%s') would be created as fold"
"er '%s' (repository '%s'). The latter becomes '%s' in "
"return, leading to infinite folder creation cycles.\n "
"SOLUTION: 1) Do set your nametrans rules on both repos"
"itories so they lead to identical names if applied bac"
"k and forth. 2) Use folderfilter settings on a reposit"
"ory to prevent some folders from being created on the "
"other side." % (dst_name, dst_repo, newsrc_name,
src_repo, newdst_name),
OfflineImapError.ERROR.REPO)
# end sanity check, actually create the folder
#
try:
src_repo.makefolder(newsrc_name)
except OfflineImapError, e:
self.ui.error(e, exc_info()[2],
"Creating folder %s on repository %s" %\
(src_name, dst_repo))
raise
status_repo.makefolder(newsrc_name.replace(
src_repo.getsep(), status_repo.getsep()))
# Find deleted folders.
#
# We don't delete folders right now.
#for key in desthash.keys():
# if not key in srchash:
# dest.deletefolder(key)
##### Keepalive
def startkeepalive(self):
"""The default implementation will do nothing."""
pass

View File

@ -59,8 +59,7 @@ class GmailRepository(IMAPRepository):
def getfolder(self, foldername):
return self.getfoldertype()(self.imapserver, foldername,
self.nametrans(foldername),
self.accountname, self)
self)
def getfoldertype(self):
return folder.Gmail.GmailFolder

View File

@ -21,7 +21,6 @@ from offlineimap import folder, imaputil, imapserver, OfflineImapError
from offlineimap.folder.UIDMaps import MappedIMAPFolder
from offlineimap.threadutil import ExitNotifyThread
from threading import Event
import re
import types
import os
from sys import exc_info
@ -36,23 +35,6 @@ class IMAPRepository(BaseRepository):
self._host = None
self.imapserver = imapserver.IMAPServer(self)
self.folders = None
self.nametrans = lambda foldername: foldername
self.folderfilter = lambda foldername: 1
self.folderincludes = []
self.foldersort = cmp
localeval = self.localeval
if self.config.has_option(self.getsection(), 'nametrans'):
self.nametrans = localeval.eval(self.getconf('nametrans'),
{'re': re})
if self.config.has_option(self.getsection(), 'folderfilter'):
self.folderfilter = localeval.eval(self.getconf('folderfilter'),
{'re': re})
if self.config.has_option(self.getsection(), 'folderincludes'):
self.folderincludes = localeval.eval(self.getconf('folderincludes'),
{'re': re})
if self.config.has_option(self.getsection(), 'foldersort'):
self.foldersort = localeval.eval(self.getconf('foldersort'),
{'re': re})
def startkeepalive(self):
keepalivetime = self.getkeepalive()
@ -259,9 +241,7 @@ class IMAPRepository(BaseRepository):
def getfolder(self, foldername):
return self.getfoldertype()(self.imapserver, foldername,
self.nametrans(foldername),
self.accountname, self)
return self.getfoldertype()(self.imapserver, foldername, self)
def getfoldertype(self):
return folder.IMAP.IMAPFolder
@ -280,8 +260,7 @@ class IMAPRepository(BaseRepository):
imapobj = self.imapserver.acquireconnection()
# check whether to list all folders, or subscribed only
listfunction = imapobj.list
if self.config.has_option(self.getsection(), 'subscribedonly'):
if self.getconf('subscribedonly') == "yes":
if self.getconfboolean('subscribedonly', False):
listfunction = imapobj.lsub
try:
listresult = listfunction(directory = self.imapserver.reference)[1]
@ -298,13 +277,14 @@ class IMAPRepository(BaseRepository):
if '\\noselect' in flaglist:
continue
foldername = imaputil.dequote(name)
if not self.folderfilter(foldername):
self.ui.debug('imap',"Filtering out '%s' due to folderfilter" %\
foldername)
continue
retval.append(self.getfoldertype()(self.imapserver, foldername,
self.nametrans(foldername),
self.accountname, self))
self))
# filter out the folder?
if not self.folderfilter(foldername):
self.ui.debug('imap', "Filtering out '%s'[%s] due to folderfilt"
"er" % (foldername, self))
retval[-1].sync_this = False
# Add all folderincludes
if len(self.folderincludes):
imapobj = self.imapserver.acquireconnection()
try:
@ -320,26 +300,36 @@ class IMAPRepository(BaseRepository):
continue
retval.append(self.getfoldertype()(self.imapserver,
foldername,
self.nametrans(foldername),
self.accountname, self))
self))
finally:
self.imapserver.releaseconnection(imapobj)
retval.sort(lambda x, y: self.foldersort(x.getvisiblename(), y.getvisiblename()))
self.folders = retval
return retval
return self.folders
def makefolder(self, foldername):
"""Create a folder on the IMAP server
:param foldername: Full path of the folder to be created."""
#TODO: IMHO this existing commented out code is correct and
#should be enabled, but this would change the behavior for
#existing configurations who have a 'reference' set on a Mapped
#IMAP server....:
#if self.getreference() != '""':
# newname = self.getreference() + self.getsep() + foldername
#else:
# newname = foldername
newname = foldername
imapobj = self.imapserver.acquireconnection()
try:
result = imapobj.create(newname)
self.ui._msg("Creating new IMAP folder '%s' on server %s" %\
(foldername, self))
result = imapobj.create(foldername)
if result[0] != 'OK':
raise RuntimeError, "Repository %s could not create folder %s: %s" % (self.getname(), foldername, str(result))
raise OfflineImapError("Folder '%s'[%s] could not be created. "
"Server responded: %s" % \
(foldername, self, str(result)),
OfflineImapError.ERROR.FOLDER)
finally:
self.imapserver.releaseconnection(imapobj)

View File

@ -25,14 +25,14 @@ import re
class LocalStatusRepository(BaseRepository):
def __init__(self, reposname, account):
BaseRepository.__init__(self, reposname, account)
self.directory = os.path.join(account.getaccountmeta(), 'LocalStatus')
#statusbackend can be 'plain' or 'sqlite'
# Root directory in which the LocalStatus folders reside
self.root = os.path.join(account.getaccountmeta(), 'LocalStatus')
# statusbackend can be 'plain' or 'sqlite'
backend = self.account.getconf('status_backend', 'plain')
if backend == 'sqlite':
self._backend = 'sqlite'
self.LocalStatusFolderClass = LocalStatusSQLiteFolder
self.directory += '-sqlite'
self.root += '-sqlite'
elif backend == 'plain':
self._backend = 'plain'
self.LocalStatusFolderClass = LocalStatusFolder
@ -40,8 +40,8 @@ class LocalStatusRepository(BaseRepository):
raise SyntaxWarning("Unknown status_backend '%s' for account '%s'" \
% (backend, account.name))
if not os.path.exists(self.directory):
os.mkdir(self.directory, 0700)
if not os.path.exists(self.root):
os.mkdir(self.root, 0700)
# self._folders is a list of LocalStatusFolders()
self._folders = None
@ -60,7 +60,7 @@ class LocalStatusRepository(BaseRepository):
# replace with literal 'dot' if final path name is '.' as '.' is
# an invalid file name.
basename = re.sub('(^|\/)\.$','\\1dot', basename)
return os.path.join(self.directory, basename)
return os.path.join(self.root, basename)
def makefolder(self, foldername):
"""Create a LocalStatus Folder
@ -82,9 +82,7 @@ class LocalStatusRepository(BaseRepository):
def getfolder(self, foldername):
"""Return the Folder() object for a foldername"""
return self.LocalStatusFolderClass(self.directory, foldername,
self, self.accountname,
self.config)
return self.LocalStatusFolderClass(foldername, self)
def getfolders(self):
"""Returns a list of all cached folders."""
@ -92,7 +90,7 @@ class LocalStatusRepository(BaseRepository):
return self._folders
self._folders = []
for folder in os.listdir(self.directory):
for folder in os.listdir(self.root):
self._folders.append(self.getfolder(folder))
return self._folders

View File

@ -19,6 +19,7 @@
from Base import BaseRepository
from offlineimap import folder
from offlineimap.ui import getglobalui
from offlineimap.error import OfflineImapError
import os
from stat import *
@ -39,21 +40,25 @@ class MaildirRepository(BaseRepository):
os.mkdir(self.root, 0700)
def _append_folder_atimes(self, foldername):
"""Store the atimes of a folder's new|cur in self.folder_atimes"""
p = os.path.join(self.root, foldername)
new = os.path.join(p, 'new')
cur = os.path.join(p, 'cur')
f = p, os.stat(new)[ST_ATIME], os.stat(cur)[ST_ATIME]
self.folder_atimes.append(f)
atimes = (p, os.path.getatime(new), os.path.getatime(cur))
self.folder_atimes.append(atimes)
def restore_folder_atimes(self):
if not self.folder_atimes:
return
def restore_atime(self):
"""Sets folders' atime back to their values after a sync
for f in self.folder_atimes:
t = f[1], os.stat(os.path.join(f[0], 'new'))[ST_MTIME]
os.utime(os.path.join(f[0], 'new'), t)
t = f[2], os.stat(os.path.join(f[0], 'cur'))[ST_MTIME]
os.utime(os.path.join(f[0], 'cur'), t)
Controlled by the 'restoreatime' config parameter."""
if not self.getconfboolean('restoreatime', False):
return # not configured to restore
for (dirpath, new_atime, cur_atime) in self.folder_atimes:
new_dir = os.path.join(dirpath, 'new')
cur_dir = os.path.join(dirpath, 'cur')
os.utime(new_dir, (new_atime, os.path.getmtime(new_dir)))
os.utime(cur_dir, (cur_atime, os.path.getmtime(cur_dir)))
def getlocalroot(self):
return os.path.expanduser(self.getconf('localfolders'))
@ -110,12 +115,20 @@ class MaildirRepository(BaseRepository):
self.ui.warn("NOT YET IMPLEMENTED: DELETE FOLDER %s" % foldername)
def getfolder(self, foldername):
if self.config.has_option('Repository ' + self.name, 'restoreatime') and self.config.getboolean('Repository ' + self.name, 'restoreatime'):
self._append_folder_atimes(foldername)
return folder.Maildir.MaildirFolder(self.root, foldername,
self.getsep(), self,
self.accountname, self.config)
"""Return a Folder instance of this Maildir
If necessary, scan and cache all foldernames to make sure that
we only return existing folders and that 2 calls with the same
name will return the same object."""
# getfolders() will scan and cache the values *if* necessary
folders = self.getfolders()
for folder in folders:
if foldername == folder.name:
return folder
raise OfflineImapError("getfolder() asked for a nonexisting "
"folder '%s'." % foldername,
OfflineImapError.ERROR.FOLDER)
def _getfolders_scandir(self, root, extension = None):
"""Recursively scan folder 'root'; return a list of MailDirFolder
@ -133,7 +146,7 @@ class MaildirRepository(BaseRepository):
self.debug(" toppath = %s" % toppath)
# Iterate over directories in top & top itself.
for dirname in os.listdir(toppath) + ['.']:
for dirname in os.listdir(toppath) + ['']:
self.debug(" *** top of loop")
self.debug(" dirname = %s" % dirname)
if dirname in ['cur', 'new', 'tmp']:
@ -154,19 +167,19 @@ class MaildirRepository(BaseRepository):
os.path.isdir(os.path.join(fullname, 'tmp'))):
# This directory has maildir stuff -- process
self.debug(" This is maildir folder '%s'." % foldername)
if self.config.has_option('Repository %s' % self,
'restoreatime') and \
self.config.getboolean('Repository %s' % self,
'restoreatime'):
if self.getconfboolean('restoreatime', False):
self._append_folder_atimes(foldername)
retval.append(folder.Maildir.MaildirFolder(self.root,
foldername,
self.getsep(),
self,
self.accountname,
self.config))
if self.getsep() == '/' and dirname != '.':
self))
# filter out the folder?
if not self.folderfilter(foldername):
self.debug("Filtering out '%s'[%s] due to folderfilt"
"er" % (foldername, self))
retval[-1].sync_this = False
if self.getsep() == '/' and dirname != '':
# Recursively check sub-directories for folders too.
retval.extend(self._getfolders_scandir(root, foldername))
self.debug("_GETFOLDERS_SCANDIR RETURNING %s" % \

View File

@ -214,8 +214,7 @@ def initInstanceLimit(instancename, instancemax):
class InstanceLimitedThread(ExitNotifyThread):
def __init__(self, instancename, *args, **kwargs):
self.instancename = instancename
apply(ExitNotifyThread.__init__, (self,) + args, kwargs)
super(InstanceLimitedThread, self).__init__(*args, **kwargs)
def start(self):
instancelimitedsems[self.instancename].acquire()

View File

@ -66,7 +66,7 @@ class CursesUtil:
"""Perform an operation with full locking."""
self.lock()
try:
apply(target, args, kwargs)
target(*args, **kwargs)
finally:
self.unlock()

View File

@ -104,11 +104,10 @@ class UIBase:
ui.error(exc, sys.exc_info()[2], msg="While syncing Folder %s in "
"repo %s")
"""
cur_thread = threading.currentThread()
if msg:
self._msg("ERROR [%s]: %s\n %s" % (cur_thread, msg, exc))
self._msg("ERROR: %s\n %s" % (msg, exc))
else:
self._msg("ERROR [%s]: %s" % (cur_thread, exc))
self._msg("ERROR: %s" % (exc))
if not self.debuglist:
# only output tracebacks in debug mode
@ -344,14 +343,6 @@ class UIBase:
s.delThreadDebugLog(thread)
s.terminate(100)
def getMainExceptionString(s):
return "Main program terminated with exception:\n%s\n" %\
traceback.format_exc() + \
s.getThreadDebugLog(threading.currentThread())
def mainException(s):
s._msg(s.getMainExceptionString())
def terminate(self, exitstatus = 0, errortitle = None, errormsg = None):
"""Called to terminate the application."""
#print any exceptions that have occurred over the run