Merge branch 'next'
Conflicts: Changelog.draft.rst Changelog.rst offlineimap/__init__.py Signed-off-by: Sebastian Spaeth <Sebastian@SSpaeth.de>
This commit is contained in:
commit
04a1403f18
@ -18,8 +18,3 @@ Changes
|
|||||||
|
|
||||||
Bug Fixes
|
Bug Fixes
|
||||||
---------
|
---------
|
||||||
|
|
||||||
* IMAP<->IMAP sync with a readonly local IMAP repository failed with a
|
|
||||||
rather mysterious "TypeError: expected a character buffer object"
|
|
||||||
error. Fix this my retrieving the list of folders early enough even
|
|
||||||
for readonly repositories.
|
|
||||||
|
@ -11,6 +11,34 @@ ChangeLog
|
|||||||
on releases. And because I'm lazy, it will also be used as a draft for the
|
on releases. And because I'm lazy, it will also be used as a draft for the
|
||||||
releases announces.
|
releases announces.
|
||||||
|
|
||||||
|
=======
|
||||||
|
OfflineIMAP v6.4.3 (2012-01-04)
|
||||||
|
===============================
|
||||||
|
|
||||||
|
New Features
|
||||||
|
------------
|
||||||
|
|
||||||
|
* add a --info command line switch that outputs useful information about
|
||||||
|
the server and the configuration for all enabled accounts.
|
||||||
|
|
||||||
|
Changes
|
||||||
|
-------
|
||||||
|
|
||||||
|
* Reworked logging which was reported to e.g. not flush output to files
|
||||||
|
often enough. User-visible changes:
|
||||||
|
a) console output goes to stderr (for now).
|
||||||
|
b) file output has timestamps and looks identical in the basic and
|
||||||
|
ttyui UIs.
|
||||||
|
c) File output should be flushed after logging by default (do
|
||||||
|
report if not).
|
||||||
|
|
||||||
|
* Bumped bundled imaplib2 to release 2.29
|
||||||
|
|
||||||
|
* Make ctrl-c exit cleanly rather aborting brutally (which could leave
|
||||||
|
around temporary files, half-written cache files, etc). Exiting on
|
||||||
|
SIGTERM and CTRL-C can take a little longer, but will be clean.
|
||||||
|
|
||||||
|
|
||||||
OfflineIMAP v6.4.2 (2011-12-01)
|
OfflineIMAP v6.4.2 (2011-12-01)
|
||||||
===============================
|
===============================
|
||||||
|
|
||||||
|
21
docs/FAQ.rst
21
docs/FAQ.rst
@ -219,6 +219,27 @@ as follows::
|
|||||||
2) while in sleep mode, you can also send a SIGUSR1. See the `Signals
|
2) while in sleep mode, you can also send a SIGUSR1. See the `Signals
|
||||||
on UNIX`_ section in the MANUAL for details.
|
on UNIX`_ section in the MANUAL for details.
|
||||||
|
|
||||||
|
I get a "Mailbox already exists" error
|
||||||
|
--------------------------------------
|
||||||
|
**Q:** When synchronizing, I receive errors such as::
|
||||||
|
|
||||||
|
Folder 'sent'[main-remote] could not be created. Server responded:
|
||||||
|
('NO', ['Mailbox already exists.'])
|
||||||
|
|
||||||
|
**A:** IMAP folders are usually case sensitive. But some IMAP servers seem
|
||||||
|
to treat "special" folders as case insensitive (e.g. the initial
|
||||||
|
INBOX. part, or folders such as "Sent" or "Trash"). If you happen to
|
||||||
|
have a folder "sent" on one side of things and a folder called "Sent"
|
||||||
|
on the other side, offlineimap will try to create those folders on
|
||||||
|
both sides. If you server happens to treat those folders as
|
||||||
|
case-insensitive you can then see this warning.
|
||||||
|
|
||||||
|
You can solve this by excluding the "sent" folder by filtering it from
|
||||||
|
the repository settings::
|
||||||
|
|
||||||
|
folderfilter= lambda f: f not in ['sent']
|
||||||
|
|
||||||
|
|
||||||
Configuration Questions
|
Configuration Questions
|
||||||
=======================
|
=======================
|
||||||
|
|
||||||
|
@ -478,7 +478,7 @@ or::
|
|||||||
folder separators be replaced with the destination repositories'
|
folder separators be replaced with the destination repositories'
|
||||||
folder separator.
|
folder separator.
|
||||||
|
|
||||||
So if ̀f` was "Sent", the first nametrans yields the translated name
|
So if 'f' was "Sent", the first nametrans yields the translated name
|
||||||
"INBOX/Sent" to be used on the other side. As that repository uses the
|
"INBOX/Sent" to be used on the other side. As that repository uses the
|
||||||
folder separator '.' rather than '/', the ultimate name to be used will
|
folder separator '.' rather than '/', the ultimate name to be used will
|
||||||
be "INBOX.Sent".
|
be "INBOX.Sent".
|
||||||
|
@ -55,7 +55,7 @@ metadata = ~/.offlineimap
|
|||||||
|
|
||||||
accounts = Test
|
accounts = Test
|
||||||
|
|
||||||
# Offlineimap can synchronize more the one account at a time. If you
|
# Offlineimap can synchronize more than one account at a time. If you
|
||||||
# want to enable this feature, set the below value to something
|
# want to enable this feature, set the below value to something
|
||||||
# greater than 1. To force it to synchronize only one account at a
|
# greater than 1. To force it to synchronize only one account at a
|
||||||
# time, set it to 1.
|
# time, set it to 1.
|
||||||
@ -200,7 +200,7 @@ remoterepository = RemoteExample
|
|||||||
# quick = 10
|
# quick = 10
|
||||||
|
|
||||||
# You can specify a pre and post sync hook to execute a external command.
|
# You can specify a pre and post sync hook to execute a external command.
|
||||||
# in this case a call to imapfilter to filter mail before the sync process
|
# In this case a call to imapfilter to filter mail before the sync process
|
||||||
# starts and a custom shell script after the sync completes.
|
# starts and a custom shell script after the sync completes.
|
||||||
# The pre sync script has to complete before a sync to the account will
|
# The pre sync script has to complete before a sync to the account will
|
||||||
# start.
|
# start.
|
||||||
@ -445,7 +445,7 @@ holdconnectionopen = no
|
|||||||
# mark them deleted on the server, but not actually delete them.
|
# mark them deleted on the server, but not actually delete them.
|
||||||
# You must use some other IMAP client to delete them if you use this
|
# You must use some other IMAP client to delete them if you use this
|
||||||
# setting; otherwise, the messgaes will just pile up there forever.
|
# setting; otherwise, the messgaes will just pile up there forever.
|
||||||
# Therefore, this setting is definately NOT recommended.
|
# Therefore, this setting is definitely NOT recommended.
|
||||||
#
|
#
|
||||||
# expunge = no
|
# expunge = no
|
||||||
|
|
||||||
|
@ -1,22 +1,18 @@
|
|||||||
__all__ = ['OfflineImap']
|
__all__ = ['OfflineImap']
|
||||||
|
|
||||||
__productname__ = 'OfflineIMAP'
|
__productname__ = 'OfflineIMAP'
|
||||||
__version__ = "6.4.2"
|
__version__ = "6.4.3"
|
||||||
__copyright__ = "Copyright 2002-2011 John Goerzen & contributors"
|
__copyright__ = "Copyright 2002-2012 John Goerzen & contributors"
|
||||||
__author__ = "John Goerzen"
|
__author__ = "John Goerzen"
|
||||||
__author_email__= "john@complete.org"
|
__author_email__= "john@complete.org"
|
||||||
__description__ = "Disconnected Universal IMAP Mail Synchronization/Reader Support"
|
__description__ = "Disconnected Universal IMAP Mail Synchronization/Reader Support"
|
||||||
__license__ = "Licensed under the GNU GPL v2+ (v2 or any later version)"
|
__license__ = "Licensed under the GNU GPL v2+ (v2 or any later version)"
|
||||||
__bigcopyright__ = """%(__productname__)s %(__version__)s
|
__bigcopyright__ = """%(__productname__)s %(__version__)s
|
||||||
%(__copyright__)s.
|
%(__license__)s""" % locals()
|
||||||
%(__license__)s.
|
|
||||||
""" % locals()
|
|
||||||
__homepage__ = "http://github.com/nicolas33/offlineimap"
|
__homepage__ = "http://github.com/nicolas33/offlineimap"
|
||||||
|
|
||||||
|
|
||||||
banner = __bigcopyright__
|
banner = __bigcopyright__
|
||||||
|
|
||||||
|
|
||||||
from offlineimap.error import OfflineImapError
|
from offlineimap.error import OfflineImapError
|
||||||
# put this last, so we don't run into circular dependencies using
|
# put this last, so we don't run into circular dependencies using
|
||||||
# e.g. offlineimap.__version__.
|
# e.g. offlineimap.__version__.
|
||||||
|
@ -50,7 +50,9 @@ class Account(CustomConfig.ConfigHelperMixin):
|
|||||||
:class:`accounts.SyncableAccount` which contains all functions used
|
:class:`accounts.SyncableAccount` which contains all functions used
|
||||||
for syncing an account."""
|
for syncing an account."""
|
||||||
#signal gets set when we should stop looping
|
#signal gets set when we should stop looping
|
||||||
abort_signal = Event()
|
abort_soon_signal = Event()
|
||||||
|
#signal gets set on CTRL-C/SIGTERM
|
||||||
|
abort_NOW_signal = Event()
|
||||||
|
|
||||||
def __init__(self, config, name):
|
def __init__(self, config, name):
|
||||||
"""
|
"""
|
||||||
@ -82,6 +84,9 @@ class Account(CustomConfig.ConfigHelperMixin):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
def getaccountmeta(self):
|
||||||
|
return os.path.join(self.metadatadir, 'Account-' + self.name)
|
||||||
|
|
||||||
def getsection(self):
|
def getsection(self):
|
||||||
return 'Account ' + self.getname()
|
return 'Account ' + self.getname()
|
||||||
|
|
||||||
@ -94,7 +99,8 @@ class Account(CustomConfig.ConfigHelperMixin):
|
|||||||
set_abort_event() to send the corresponding signal. Signum = 1
|
set_abort_event() to send the corresponding signal. Signum = 1
|
||||||
implies that we want all accounts to abort or skip the current
|
implies that we want all accounts to abort or skip the current
|
||||||
or next sleep phase. Signum = 2 will end the autorefresh loop,
|
or next sleep phase. Signum = 2 will end the autorefresh loop,
|
||||||
ie all accounts will return after they finished a sync.
|
ie all accounts will return after they finished a sync. signum=3
|
||||||
|
means, abort NOW, e.g. on SIGINT or SIGTERM.
|
||||||
|
|
||||||
This is a class method, it will send the signal to all accounts.
|
This is a class method, it will send the signal to all accounts.
|
||||||
"""
|
"""
|
||||||
@ -104,7 +110,10 @@ class Account(CustomConfig.ConfigHelperMixin):
|
|||||||
config.set('Account ' + acctsection, "skipsleep", '1')
|
config.set('Account ' + acctsection, "skipsleep", '1')
|
||||||
elif signum == 2:
|
elif signum == 2:
|
||||||
# don't autorefresh anymore
|
# don't autorefresh anymore
|
||||||
cls.abort_signal.set()
|
cls.abort_soon_signal.set()
|
||||||
|
elif signum == 3:
|
||||||
|
# abort ASAP
|
||||||
|
cls.abort_NOW_signal.set()
|
||||||
|
|
||||||
def get_abort_event(self):
|
def get_abort_event(self):
|
||||||
"""Checks if an abort signal had been sent
|
"""Checks if an abort signal had been sent
|
||||||
@ -119,7 +128,8 @@ class Account(CustomConfig.ConfigHelperMixin):
|
|||||||
skipsleep = self.getconfboolean("skipsleep", 0)
|
skipsleep = self.getconfboolean("skipsleep", 0)
|
||||||
if skipsleep:
|
if skipsleep:
|
||||||
self.config.set(self.getsection(), "skipsleep", '0')
|
self.config.set(self.getsection(), "skipsleep", '0')
|
||||||
return skipsleep or Account.abort_signal.is_set()
|
return skipsleep or Account.abort_soon_signal.is_set() or \
|
||||||
|
Account.abort_NOW_signal.is_set()
|
||||||
|
|
||||||
def sleeper(self):
|
def sleeper(self):
|
||||||
"""Sleep if the account is set to autorefresh
|
"""Sleep if the account is set to autorefresh
|
||||||
@ -149,12 +159,22 @@ class Account(CustomConfig.ConfigHelperMixin):
|
|||||||
item.stopkeepalive()
|
item.stopkeepalive()
|
||||||
|
|
||||||
if sleepresult:
|
if sleepresult:
|
||||||
if Account.abort_signal.is_set():
|
if Account.abort_soon_signal.is_set() or \
|
||||||
|
Account.abort_NOW_signal.is_set():
|
||||||
return 2
|
return 2
|
||||||
self.quicknum = 0
|
self.quicknum = 0
|
||||||
return 1
|
return 1
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
def serverdiagnostics(self):
|
||||||
|
"""Output diagnostics for all involved repositories"""
|
||||||
|
remote_repo = Repository(self, 'remote')
|
||||||
|
local_repo = Repository(self, 'local')
|
||||||
|
#status_repo = Repository(self, 'status')
|
||||||
|
self.ui.serverdiagnostics(remote_repo, 'Remote')
|
||||||
|
self.ui.serverdiagnostics(local_repo, 'Local')
|
||||||
|
#self.ui.serverdiagnostics(statusrepos, 'Status')
|
||||||
|
|
||||||
|
|
||||||
class SyncableAccount(Account):
|
class SyncableAccount(Account):
|
||||||
"""A syncable email account connecting 2 repositories
|
"""A syncable email account connecting 2 repositories
|
||||||
@ -194,7 +214,7 @@ class SyncableAccount(Account):
|
|||||||
pass #Failed to delete for some reason.
|
pass #Failed to delete for some reason.
|
||||||
|
|
||||||
def syncrunner(self):
|
def syncrunner(self):
|
||||||
self.ui.registerthread(self.name)
|
self.ui.registerthread(self)
|
||||||
accountmetadata = self.getaccountmeta()
|
accountmetadata = self.getaccountmeta()
|
||||||
if not os.path.exists(accountmetadata):
|
if not os.path.exists(accountmetadata):
|
||||||
os.mkdir(accountmetadata, 0700)
|
os.mkdir(accountmetadata, 0700)
|
||||||
@ -233,9 +253,6 @@ class SyncableAccount(Account):
|
|||||||
if looping and self.sleeper() >= 2:
|
if looping and self.sleeper() >= 2:
|
||||||
looping = 0
|
looping = 0
|
||||||
|
|
||||||
def getaccountmeta(self):
|
|
||||||
return os.path.join(self.metadatadir, 'Account-' + self.name)
|
|
||||||
|
|
||||||
def sync(self):
|
def sync(self):
|
||||||
"""Synchronize the account once, then return
|
"""Synchronize the account once, then return
|
||||||
|
|
||||||
@ -279,6 +296,8 @@ class SyncableAccount(Account):
|
|||||||
|
|
||||||
# iterate through all folders on the remote repo and sync
|
# iterate through all folders on the remote repo and sync
|
||||||
for remotefolder in remoterepos.getfolders():
|
for remotefolder in remoterepos.getfolders():
|
||||||
|
# check for CTRL-C or SIGTERM
|
||||||
|
if Account.abort_NOW_signal.is_set(): break
|
||||||
if not remotefolder.sync_this:
|
if not remotefolder.sync_this:
|
||||||
self.ui.debug('', "Not syncing filtered remote folder '%s'"
|
self.ui.debug('', "Not syncing filtered remote folder '%s'"
|
||||||
"[%s]" % (remotefolder, remoterepos))
|
"[%s]" % (remotefolder, remoterepos))
|
||||||
@ -287,9 +306,7 @@ class SyncableAccount(Account):
|
|||||||
instancename = 'FOLDER_' + self.remoterepos.getname(),
|
instancename = 'FOLDER_' + self.remoterepos.getname(),
|
||||||
target = syncfolder,
|
target = syncfolder,
|
||||||
name = "Folder %s [acc: %s]" % (remotefolder, self),
|
name = "Folder %s [acc: %s]" % (remotefolder, self),
|
||||||
args = (self.name, remoterepos, remotefolder, localrepos,
|
args = (self, remotefolder, quick))
|
||||||
statusrepos, quick))
|
|
||||||
thread.setDaemon(1)
|
|
||||||
thread.start()
|
thread.start()
|
||||||
folderthreads.append(thread)
|
folderthreads.append(thread)
|
||||||
# wait for all threads to finish
|
# wait for all threads to finish
|
||||||
@ -313,6 +330,9 @@ class SyncableAccount(Account):
|
|||||||
self.callhook(hook)
|
self.callhook(hook)
|
||||||
|
|
||||||
def callhook(self, cmd):
|
def callhook(self, cmd):
|
||||||
|
# check for CTRL-C or SIGTERM and run postsynchook
|
||||||
|
if Account.abort_NOW_signal.is_set():
|
||||||
|
return
|
||||||
if not cmd:
|
if not cmd:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
@ -328,15 +348,17 @@ class SyncableAccount(Account):
|
|||||||
except Exception, e:
|
except Exception, e:
|
||||||
self.ui.error(e, exc_info()[2], msg = "Calling hook")
|
self.ui.error(e, exc_info()[2], msg = "Calling hook")
|
||||||
|
|
||||||
|
def syncfolder(account, remotefolder, quick):
|
||||||
def syncfolder(accountname, remoterepos, remotefolder, localrepos,
|
|
||||||
statusrepos, quick):
|
|
||||||
"""This function is called as target for the
|
"""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."""
|
Filtered folders on the remote side will not invoke this function."""
|
||||||
|
remoterepos = account.remoterepos
|
||||||
|
localrepos = account.localrepos
|
||||||
|
statusrepos = account.statusrepos
|
||||||
|
|
||||||
ui = getglobalui()
|
ui = getglobalui()
|
||||||
ui.registerthread(accountname)
|
ui.registerthread(account)
|
||||||
try:
|
try:
|
||||||
# Load local folder.
|
# Load local folder.
|
||||||
localfolder = localrepos.\
|
localfolder = localrepos.\
|
||||||
@ -351,7 +373,7 @@ def syncfolder(accountname, remoterepos, remotefolder, localrepos,
|
|||||||
% localfolder)
|
% localfolder)
|
||||||
return
|
return
|
||||||
# Write the mailboxes
|
# Write the mailboxes
|
||||||
mbnames.add(accountname, localfolder.getvisiblename())
|
mbnames.add(account.name, localfolder.getvisiblename())
|
||||||
|
|
||||||
# Load status folder.
|
# Load status folder.
|
||||||
statusfolder = statusrepos.getfolder(remotefolder.getvisiblename().\
|
statusfolder = statusrepos.getfolder(remotefolder.getvisiblename().\
|
||||||
@ -430,11 +452,11 @@ def syncfolder(accountname, remoterepos, remotefolder, localrepos,
|
|||||||
"[acc: '%s']" % (
|
"[acc: '%s']" % (
|
||||||
remotefolder.getvisiblename().\
|
remotefolder.getvisiblename().\
|
||||||
replace(remoterepos.getsep(), localrepos.getsep()),
|
replace(remoterepos.getsep(), localrepos.getsep()),
|
||||||
accountname))
|
account))
|
||||||
# we reconstruct foldername above rather than using
|
# we reconstruct foldername above rather than using
|
||||||
# localfolder, as the localfolder var is not
|
# localfolder, as the localfolder var is not
|
||||||
# available if assignment fails.
|
# available if assignment fails.
|
||||||
except Exception, e:
|
except Exception, e:
|
||||||
ui.error(e, msg = "ERROR in syncfolder for %s folder %s: %s" % \
|
ui.error(e, msg = "ERROR in syncfolder for %s folder %s: %s" % \
|
||||||
(accountname,remotefolder.getvisiblename(),
|
(account, remotefolder.getvisiblename(),
|
||||||
traceback.format_exc()))
|
traceback.format_exc()))
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
from offlineimap import threadutil
|
from offlineimap import threadutil
|
||||||
from offlineimap.ui import getglobalui
|
from offlineimap.ui import getglobalui
|
||||||
from offlineimap.error import OfflineImapError
|
from offlineimap.error import OfflineImapError
|
||||||
|
import offlineimap.accounts
|
||||||
import os.path
|
import os.path
|
||||||
import re
|
import re
|
||||||
from sys import exc_info
|
from sys import exc_info
|
||||||
@ -254,7 +255,7 @@ class BaseFolder(object):
|
|||||||
# self.getmessage(). So, don't call self.getmessage unless
|
# self.getmessage(). So, don't call self.getmessage unless
|
||||||
# really needed.
|
# really needed.
|
||||||
if register: # output that we start a new thread
|
if register: # output that we start a new thread
|
||||||
self.ui.registerthread(self.accountname)
|
self.ui.registerthread(self.repository.account)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
message = None
|
message = None
|
||||||
@ -332,6 +333,9 @@ class BaseFolder(object):
|
|||||||
self.getmessageuidlist())
|
self.getmessageuidlist())
|
||||||
num_to_copy = len(copylist)
|
num_to_copy = len(copylist)
|
||||||
for num, uid in enumerate(copylist):
|
for num, uid in enumerate(copylist):
|
||||||
|
# bail out on CTRL-C or SIGTERM
|
||||||
|
if offlineimap.accounts.Account.abort_NOW_signal.is_set():
|
||||||
|
break
|
||||||
self.ui.copyingmessage(uid, num+1, num_to_copy, self, dstfolder)
|
self.ui.copyingmessage(uid, num+1, num_to_copy, self, dstfolder)
|
||||||
# exceptions are caught in copymessageto()
|
# exceptions are caught in copymessageto()
|
||||||
if self.suggeststhreads():
|
if self.suggeststhreads():
|
||||||
@ -341,7 +345,6 @@ class BaseFolder(object):
|
|||||||
target = self.copymessageto,
|
target = self.copymessageto,
|
||||||
name = "Copy message from %s:%s" % (self.repository, self),
|
name = "Copy message from %s:%s" % (self.repository, self),
|
||||||
args = (uid, dstfolder, statusfolder))
|
args = (uid, dstfolder, statusfolder))
|
||||||
thread.setDaemon(1)
|
|
||||||
thread.start()
|
thread.start()
|
||||||
threads.append(thread)
|
threads.append(thread)
|
||||||
else:
|
else:
|
||||||
@ -448,6 +451,9 @@ class BaseFolder(object):
|
|||||||
('syncing flags' , self.syncmessagesto_flags)]
|
('syncing flags' , self.syncmessagesto_flags)]
|
||||||
|
|
||||||
for (passdesc, action) in passes:
|
for (passdesc, action) in passes:
|
||||||
|
# bail out on CTRL-C or SIGTERM
|
||||||
|
if offlineimap.accounts.Account.abort_NOW_signal.is_set():
|
||||||
|
break
|
||||||
try:
|
try:
|
||||||
action(dstfolder, statusfolder)
|
action(dstfolder, statusfolder)
|
||||||
except (KeyboardInterrupt):
|
except (KeyboardInterrupt):
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
# IMAP folder support
|
# IMAP folder support
|
||||||
# Copyright (C) 2002-2007 John Goerzen
|
# Copyright (C) 2002-2011 John Goerzen & contributors
|
||||||
# <jgoerzen@complete.org>
|
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -65,9 +65,11 @@ class LocalStatusFolder(BaseFolder):
|
|||||||
file = open(self.filename, "rt")
|
file = open(self.filename, "rt")
|
||||||
self.messagelist = {}
|
self.messagelist = {}
|
||||||
line = file.readline().strip()
|
line = file.readline().strip()
|
||||||
if not line and not line.read():
|
if not line:
|
||||||
# The status file is empty - should not have happened,
|
# The status file is empty - should not have happened,
|
||||||
# but somehow did.
|
# but somehow did.
|
||||||
|
errstr = "Cache file '%s' is empty. Closing..." % self.filename
|
||||||
|
self.ui.warn(errstr)
|
||||||
file.close()
|
file.close()
|
||||||
return
|
return
|
||||||
assert(line == magicline)
|
assert(line == magicline)
|
||||||
|
@ -15,8 +15,8 @@
|
|||||||
# 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
|
||||||
|
from __future__ import with_statement # needed for python 2.5
|
||||||
from threading import *
|
from threading import Lock
|
||||||
from IMAP import IMAPFolder
|
from IMAP import IMAPFolder
|
||||||
import os.path
|
import os.path
|
||||||
|
|
||||||
|
@ -17,9 +17,9 @@ Public functions: Internaldate2Time
|
|||||||
__all__ = ("IMAP4", "IMAP4_SSL", "IMAP4_stream",
|
__all__ = ("IMAP4", "IMAP4_SSL", "IMAP4_stream",
|
||||||
"Internaldate2Time", "ParseFlags", "Time2Internaldate")
|
"Internaldate2Time", "ParseFlags", "Time2Internaldate")
|
||||||
|
|
||||||
__version__ = "2.28"
|
__version__ = "2.29"
|
||||||
__release__ = "2"
|
__release__ = "2"
|
||||||
__revision__ = "28"
|
__revision__ = "29"
|
||||||
__credits__ = """
|
__credits__ = """
|
||||||
Authentication code contributed by Donn Cave <donn@u.washington.edu> June 1998.
|
Authentication code contributed by Donn Cave <donn@u.washington.edu> June 1998.
|
||||||
String method conversion by ESR, February 2001.
|
String method conversion by ESR, February 2001.
|
||||||
@ -1234,11 +1234,13 @@ class IMAP4(object):
|
|||||||
|
|
||||||
def _choose_nonull_or_dflt(self, dflt, *args):
|
def _choose_nonull_or_dflt(self, dflt, *args):
|
||||||
dflttyp = type(dflt)
|
dflttyp = type(dflt)
|
||||||
|
if isinstance(dflttyp, basestring):
|
||||||
|
dflttyp = basestring # Allow any string type
|
||||||
for arg in args:
|
for arg in args:
|
||||||
if arg is not None:
|
if arg is not None:
|
||||||
if type(arg) is dflttyp:
|
if isinstance(arg, dflttyp):
|
||||||
return arg
|
return arg
|
||||||
if __debug__: self._log(1, 'bad arg type is %s, expecting %s' % (type(arg), dflttyp))
|
if __debug__: self._log(0, 'bad arg is %s, expecting %s' % (type(arg), dflttyp))
|
||||||
return dflt
|
return dflt
|
||||||
|
|
||||||
|
|
||||||
@ -2323,6 +2325,7 @@ if __name__ == '__main__':
|
|||||||
data = open(os.path.exists("test.data") and "test.data" or __file__).read(1000)
|
data = open(os.path.exists("test.data") and "test.data" or __file__).read(1000)
|
||||||
test_mesg = 'From: %(user)s@localhost%(lf)sSubject: IMAP4 test%(lf)s%(lf)s%(data)s' \
|
test_mesg = 'From: %(user)s@localhost%(lf)sSubject: IMAP4 test%(lf)s%(lf)s%(data)s' \
|
||||||
% {'user':USER, 'lf':'\n', 'data':data}
|
% {'user':USER, 'lf':'\n', 'data':data}
|
||||||
|
|
||||||
test_seq1 = [
|
test_seq1 = [
|
||||||
('list', ('""', '%')),
|
('list', ('""', '%')),
|
||||||
('create', ('/tmp/imaplib2_test.0',)),
|
('create', ('/tmp/imaplib2_test.0',)),
|
||||||
@ -2342,7 +2345,7 @@ if __name__ == '__main__':
|
|||||||
|
|
||||||
test_seq2 = (
|
test_seq2 = (
|
||||||
('select', ()),
|
('select', ()),
|
||||||
('response',('UIDVALIDITY',)),
|
('response', ('UIDVALIDITY',)),
|
||||||
('response', ('EXISTS',)),
|
('response', ('EXISTS',)),
|
||||||
('append', (None, None, None, test_mesg)),
|
('append', (None, None, None, test_mesg)),
|
||||||
('uid', ('SEARCH', 'SUBJECT', 'IMAP4 test')),
|
('uid', ('SEARCH', 'SUBJECT', 'IMAP4 test')),
|
||||||
@ -2351,6 +2354,7 @@ if __name__ == '__main__':
|
|||||||
('recent', ()),
|
('recent', ()),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
AsyncError = None
|
AsyncError = None
|
||||||
|
|
||||||
def responder((response, cb_arg, error)):
|
def responder((response, cb_arg, error)):
|
||||||
@ -2436,10 +2440,21 @@ if __name__ == '__main__':
|
|||||||
|
|
||||||
if 'IDLE' in M.capabilities:
|
if 'IDLE' in M.capabilities:
|
||||||
run('idle', (2,), cb=False)
|
run('idle', (2,), cb=False)
|
||||||
run('idle', (99,), cb=True) # Asynchronous, to test interruption of 'idle' by 'noop'
|
run('idle', (99,)) # Asynchronous, to test interruption of 'idle' by 'noop'
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
run('noop', (), cb=False)
|
run('noop', (), cb=False)
|
||||||
|
|
||||||
|
run('append', (None, None, None, test_mesg), cb=False)
|
||||||
|
num = run('search', (None, 'ALL'), cb=False)[0].split()[0]
|
||||||
|
dat = run('fetch', (num, '(FLAGS INTERNALDATE RFC822)'), cb=False)
|
||||||
|
M._mesg('fetch %s => %s' % (num, `dat`))
|
||||||
|
run('idle', (2,))
|
||||||
|
run('store', (num, '-FLAGS', '(\Seen)'), cb=False),
|
||||||
|
dat = run('fetch', (num, '(FLAGS INTERNALDATE RFC822)'), cb=False)
|
||||||
|
M._mesg('fetch %s => %s' % (num, `dat`))
|
||||||
|
run('uid', ('STORE', num, 'FLAGS', '(\Deleted)'))
|
||||||
|
run('expunge', ())
|
||||||
|
|
||||||
run('logout', (), cb=False)
|
run('logout', (), cb=False)
|
||||||
|
|
||||||
if debug:
|
if debug:
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
# 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
|
||||||
|
|
||||||
|
from __future__ import with_statement # needed for python 2.5
|
||||||
from offlineimap import imaplibutil, imaputil, threadutil, OfflineImapError
|
from offlineimap import imaplibutil, imaputil, threadutil, OfflineImapError
|
||||||
from offlineimap.ui import getglobalui
|
from offlineimap.ui import getglobalui
|
||||||
from threading import Lock, BoundedSemaphore, Thread, Event, currentThread
|
from threading import Lock, BoundedSemaphore, Thread, Event, currentThread
|
||||||
@ -46,7 +47,11 @@ class IMAPServer:
|
|||||||
"""Initializes all variables from an IMAPRepository() instance
|
"""Initializes all variables from an IMAPRepository() instance
|
||||||
|
|
||||||
Various functions, such as acquireconnection() return an IMAP4
|
Various functions, such as acquireconnection() return an IMAP4
|
||||||
object on which we can operate."""
|
object on which we can operate.
|
||||||
|
|
||||||
|
Public instance variables are: self.:
|
||||||
|
delim The server's folder delimiter. Only valid after acquireconnection()
|
||||||
|
"""
|
||||||
GSS_STATE_STEP = 0
|
GSS_STATE_STEP = 0
|
||||||
GSS_STATE_WRAP = 1
|
GSS_STATE_WRAP = 1
|
||||||
def __init__(self, repos):
|
def __init__(self, repos):
|
||||||
@ -97,11 +102,6 @@ class IMAPServer:
|
|||||||
self.passworderror = None
|
self.passworderror = None
|
||||||
return self.password
|
return self.password
|
||||||
|
|
||||||
def getdelim(self):
|
|
||||||
"""Returns this server's folder delimiter. Can only be called
|
|
||||||
after one or more calls to acquireconnection."""
|
|
||||||
return self.delim
|
|
||||||
|
|
||||||
def getroot(self):
|
def getroot(self):
|
||||||
"""Returns this server's folder root. Can only be called after one
|
"""Returns this server's folder root. Can only be called after one
|
||||||
or more calls to acquireconnection."""
|
or more calls to acquireconnection."""
|
||||||
@ -370,7 +370,11 @@ class IMAPServer:
|
|||||||
def close(self):
|
def close(self):
|
||||||
# Make sure I own all the semaphores. Let the threads finish
|
# Make sure I own all the semaphores. Let the threads finish
|
||||||
# their stuff. This is a blocking method.
|
# their stuff. This is a blocking method.
|
||||||
self.connectionlock.acquire()
|
with self.connectionlock:
|
||||||
|
# first, wait till all connections had been released.
|
||||||
|
# TODO: won't work IMHO, as releaseconnection() also
|
||||||
|
# requires the connectionlock, leading to a potential
|
||||||
|
# deadlock! Audit & check!
|
||||||
threadutil.semaphorereset(self.semaphore, self.maxconnections)
|
threadutil.semaphorereset(self.semaphore, self.maxconnections)
|
||||||
for imapobj in self.assignedconnections + self.availableconnections:
|
for imapobj in self.assignedconnections + self.availableconnections:
|
||||||
imapobj.logout()
|
imapobj.logout()
|
||||||
@ -381,7 +385,6 @@ class IMAPServer:
|
|||||||
self.gss_step = self.GSS_STATE_STEP
|
self.gss_step = self.GSS_STATE_STEP
|
||||||
self.gss_vc = None
|
self.gss_vc = None
|
||||||
self.gssapi = False
|
self.gssapi = False
|
||||||
self.connectionlock.release()
|
|
||||||
|
|
||||||
def keepalive(self, timeout, event):
|
def keepalive(self, timeout, event):
|
||||||
"""Sends a NOOP to each connection recorded. It will wait a maximum
|
"""Sends a NOOP to each connection recorded. It will wait a maximum
|
||||||
@ -390,42 +393,35 @@ class IMAPServer:
|
|||||||
to be invoked in a separate thread, which should be join()'d after
|
to be invoked in a separate thread, which should be join()'d after
|
||||||
the event is set."""
|
the event is set."""
|
||||||
self.ui.debug('imap', 'keepalive thread started')
|
self.ui.debug('imap', 'keepalive thread started')
|
||||||
while 1:
|
while not event.isSet():
|
||||||
self.ui.debug('imap', 'keepalive: top of loop')
|
|
||||||
if event.isSet():
|
|
||||||
self.ui.debug('imap', 'keepalive: event is set; exiting')
|
|
||||||
return
|
|
||||||
self.ui.debug('imap', 'keepalive: acquiring connectionlock')
|
|
||||||
self.connectionlock.acquire()
|
self.connectionlock.acquire()
|
||||||
numconnections = len(self.assignedconnections) + \
|
numconnections = len(self.assignedconnections) + \
|
||||||
len(self.availableconnections)
|
len(self.availableconnections)
|
||||||
self.connectionlock.release()
|
self.connectionlock.release()
|
||||||
self.ui.debug('imap', 'keepalive: connectionlock released')
|
|
||||||
threads = []
|
|
||||||
|
|
||||||
|
threads = []
|
||||||
for i in range(numconnections):
|
for i in range(numconnections):
|
||||||
self.ui.debug('imap', 'keepalive: processing connection %d of %d' % (i, numconnections))
|
self.ui.debug('imap', 'keepalive: processing connection %d of %d' % (i, numconnections))
|
||||||
if len(self.idlefolders) > i:
|
if len(self.idlefolders) > i:
|
||||||
|
# IDLE thread
|
||||||
idler = IdleThread(self, self.idlefolders[i])
|
idler = IdleThread(self, self.idlefolders[i])
|
||||||
else:
|
else:
|
||||||
|
# NOOP thread
|
||||||
idler = IdleThread(self)
|
idler = IdleThread(self)
|
||||||
idler.start()
|
idler.start()
|
||||||
threads.append(idler)
|
threads.append(idler)
|
||||||
self.ui.debug('imap', 'keepalive: thread started')
|
|
||||||
|
|
||||||
self.ui.debug('imap', 'keepalive: waiting for timeout')
|
self.ui.debug('imap', 'keepalive: waiting for timeout')
|
||||||
event.wait(timeout)
|
event.wait(timeout)
|
||||||
self.ui.debug('imap', 'keepalive: after wait')
|
self.ui.debug('imap', 'keepalive: after wait')
|
||||||
|
|
||||||
self.ui.debug('imap', 'keepalive: joining threads')
|
|
||||||
|
|
||||||
for idler in threads:
|
for idler in threads:
|
||||||
# Make sure all the commands have completed.
|
# Make sure all the commands have completed.
|
||||||
idler.stop()
|
idler.stop()
|
||||||
idler.join()
|
idler.join()
|
||||||
|
self.ui.debug('imap', 'keepalive: all threads joined')
|
||||||
self.ui.debug('imap', 'keepalive: bottom of loop')
|
self.ui.debug('imap', 'keepalive: event is set; exiting')
|
||||||
|
return
|
||||||
|
|
||||||
def verifycert(self, cert, hostname):
|
def verifycert(self, cert, hostname):
|
||||||
'''Verify that cert (in socket.getpeercert() format) matches hostname.
|
'''Verify that cert (in socket.getpeercert() format) matches hostname.
|
||||||
@ -492,10 +488,24 @@ class IdleThread(object):
|
|||||||
self.thread.join()
|
self.thread.join()
|
||||||
|
|
||||||
def noop(self):
|
def noop(self):
|
||||||
|
#TODO: AFAIK this is not optimal, we will send a NOOP on one
|
||||||
|
#random connection (ie not enough to keep all connections
|
||||||
|
#open). In case we do the noop multiple times, we can well use
|
||||||
|
#the same connection every time, as we get a random one. This
|
||||||
|
#function should IMHO send a noop on ALL available connections
|
||||||
|
#to the server.
|
||||||
imapobj = self.parent.acquireconnection()
|
imapobj = self.parent.acquireconnection()
|
||||||
|
try:
|
||||||
imapobj.noop()
|
imapobj.noop()
|
||||||
self.stop_sig.wait()
|
except imapobj.abort:
|
||||||
|
self.ui.warn('Attempting NOOP on dropped connection %s' % \
|
||||||
|
imapobj.identifier)
|
||||||
|
self.parent.releaseconnection(imapobj, True)
|
||||||
|
imapobj = None
|
||||||
|
finally:
|
||||||
|
if imapobj:
|
||||||
self.parent.releaseconnection(imapobj)
|
self.parent.releaseconnection(imapobj)
|
||||||
|
self.stop_sig.wait() # wait until we are supposed to quit
|
||||||
|
|
||||||
def dosync(self):
|
def dosync(self):
|
||||||
remoterepos = self.parent.repos
|
remoterepos = self.parent.repos
|
||||||
@ -504,9 +514,9 @@ class IdleThread(object):
|
|||||||
remoterepos = account.remoterepos
|
remoterepos = account.remoterepos
|
||||||
statusrepos = account.statusrepos
|
statusrepos = account.statusrepos
|
||||||
remotefolder = remoterepos.getfolder(self.folder)
|
remotefolder = remoterepos.getfolder(self.folder)
|
||||||
offlineimap.accounts.syncfolder(account.name, remoterepos, remotefolder, localrepos, statusrepos, quick=False)
|
offlineimap.accounts.syncfolder(account, remotefolder, quick=False)
|
||||||
ui = getglobalui()
|
ui = getglobalui()
|
||||||
ui.unregisterthread(currentThread())
|
ui.unregisterthread(currentThread()) #syncfolder registered the thread
|
||||||
|
|
||||||
def idle(self):
|
def idle(self):
|
||||||
"""Invoke IDLE mode until timeout or self.stop() is invoked"""
|
"""Invoke IDLE mode until timeout or self.stop() is invoked"""
|
||||||
|
@ -44,7 +44,14 @@ class OfflineImap:
|
|||||||
"""
|
"""
|
||||||
def run(self):
|
def run(self):
|
||||||
"""Parse the commandline and invoke everything"""
|
"""Parse the commandline and invoke everything"""
|
||||||
|
# next line also sets self.config and self.ui
|
||||||
|
options, args = self.parse_cmd_options()
|
||||||
|
if options.diagnostics:
|
||||||
|
self.serverdiagnostics(options)
|
||||||
|
else:
|
||||||
|
self.sync(options)
|
||||||
|
|
||||||
|
def parse_cmd_options(self):
|
||||||
parser = OptionParser(version=offlineimap.__version__,
|
parser = OptionParser(version=offlineimap.__version__,
|
||||||
description="%s.\n\n%s" %
|
description="%s.\n\n%s" %
|
||||||
(offlineimap.__copyright__,
|
(offlineimap.__copyright__,
|
||||||
@ -139,6 +146,13 @@ class OfflineImap:
|
|||||||
"not usable. Possible interface choices are: %s " %
|
"not usable. Possible interface choices are: %s " %
|
||||||
", ".join(UI_LIST.keys()))
|
", ".join(UI_LIST.keys()))
|
||||||
|
|
||||||
|
parser.add_option("--info",
|
||||||
|
action="store_true", dest="diagnostics",
|
||||||
|
default=False,
|
||||||
|
help="Output information on the configured email repositories"
|
||||||
|
". Useful for debugging and bug reporting. Use in conjunction wit"
|
||||||
|
"h the -a option to limit the output to a single account")
|
||||||
|
|
||||||
(options, args) = parser.parse_args()
|
(options, args) = parser.parse_args()
|
||||||
|
|
||||||
#read in configuration file
|
#read in configuration file
|
||||||
@ -146,6 +160,7 @@ class OfflineImap:
|
|||||||
|
|
||||||
config = CustomConfigParser()
|
config = CustomConfigParser()
|
||||||
if not os.path.exists(configfilename):
|
if not os.path.exists(configfilename):
|
||||||
|
# TODO, initialize and make use of chosen ui for logging
|
||||||
logging.error(" *** Config file '%s' does not exist; aborting!" %
|
logging.error(" *** Config file '%s' does not exist; aborting!" %
|
||||||
configfilename)
|
configfilename)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
@ -154,14 +169,17 @@ class OfflineImap:
|
|||||||
#profile mode chosen?
|
#profile mode chosen?
|
||||||
if options.profiledir:
|
if options.profiledir:
|
||||||
if not options.singlethreading:
|
if not options.singlethreading:
|
||||||
|
# TODO, make use of chosen ui for logging
|
||||||
logging.warn("Profile mode: Forcing to singlethreaded.")
|
logging.warn("Profile mode: Forcing to singlethreaded.")
|
||||||
options.singlethreading = True
|
options.singlethreading = True
|
||||||
if os.path.exists(options.profiledir):
|
if os.path.exists(options.profiledir):
|
||||||
|
# TODO, make use of chosen ui for logging
|
||||||
logging.warn("Profile mode: Directory '%s' already exists!" %
|
logging.warn("Profile mode: Directory '%s' already exists!" %
|
||||||
options.profiledir)
|
options.profiledir)
|
||||||
else:
|
else:
|
||||||
os.mkdir(options.profiledir)
|
os.mkdir(options.profiledir)
|
||||||
threadutil.ExitNotifyThread.set_profiledir(options.profiledir)
|
threadutil.ExitNotifyThread.set_profiledir(options.profiledir)
|
||||||
|
# TODO, make use of chosen ui for logging
|
||||||
logging.warn("Profile mode: Potentially large data will be "
|
logging.warn("Profile mode: Potentially large data will be "
|
||||||
"created in '%s'" % options.profiledir)
|
"created in '%s'" % options.profiledir)
|
||||||
|
|
||||||
@ -183,37 +201,39 @@ class OfflineImap:
|
|||||||
if '.' in ui_type:
|
if '.' in ui_type:
|
||||||
#transform Curses.Blinkenlights -> Blinkenlights
|
#transform Curses.Blinkenlights -> Blinkenlights
|
||||||
ui_type = ui_type.split('.')[-1]
|
ui_type = ui_type.split('.')[-1]
|
||||||
|
# TODO, make use of chosen ui for logging
|
||||||
logging.warning('Using old interface name, consider using one '
|
logging.warning('Using old interface name, consider using one '
|
||||||
'of %s' % ', '.join(UI_LIST.keys()))
|
'of %s' % ', '.join(UI_LIST.keys()))
|
||||||
try:
|
try:
|
||||||
# create the ui class
|
# create the ui class
|
||||||
ui = UI_LIST[ui_type.lower()](config)
|
self.ui = UI_LIST[ui_type.lower()](config)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
logging.error("UI '%s' does not exist, choose one of: %s" % \
|
logging.error("UI '%s' does not exist, choose one of: %s" % \
|
||||||
(ui_type,', '.join(UI_LIST.keys())))
|
(ui_type,', '.join(UI_LIST.keys())))
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
setglobalui(ui)
|
setglobalui(self.ui)
|
||||||
|
|
||||||
#set up additional log files
|
#set up additional log files
|
||||||
if options.logfile:
|
if options.logfile:
|
||||||
ui.setlogfd(open(options.logfile, 'wt'))
|
self.ui.setlogfile(options.logfile)
|
||||||
|
|
||||||
#welcome blurb
|
#welcome blurb
|
||||||
ui.init_banner()
|
self.ui.init_banner()
|
||||||
|
|
||||||
if options.debugtype:
|
if options.debugtype:
|
||||||
|
self.ui.logger.setLevel(logging.DEBUG)
|
||||||
if options.debugtype.lower() == 'all':
|
if options.debugtype.lower() == 'all':
|
||||||
options.debugtype = 'imap,maildir,thread'
|
options.debugtype = 'imap,maildir,thread'
|
||||||
#force single threading?
|
#force single threading?
|
||||||
if not ('thread' in options.debugtype.split(',') \
|
if not ('thread' in options.debugtype.split(',') \
|
||||||
and not options.singlethreading):
|
and not options.singlethreading):
|
||||||
ui._msg("Debug mode: Forcing to singlethreaded.")
|
self.ui._msg("Debug mode: Forcing to singlethreaded.")
|
||||||
options.singlethreading = True
|
options.singlethreading = True
|
||||||
|
|
||||||
debugtypes = options.debugtype.split(',') + ['']
|
debugtypes = options.debugtype.split(',') + ['']
|
||||||
for type in debugtypes:
|
for type in debugtypes:
|
||||||
type = type.strip()
|
type = type.strip()
|
||||||
ui.add_debug(type)
|
self.ui.add_debug(type)
|
||||||
if type.lower() == 'imap':
|
if type.lower() == 'imap':
|
||||||
imaplib.Debug = 5
|
imaplib.Debug = 5
|
||||||
|
|
||||||
@ -241,57 +261,15 @@ class OfflineImap:
|
|||||||
config.set(section, "folderfilter", folderfilter)
|
config.set(section, "folderfilter", folderfilter)
|
||||||
config.set(section, "folderincludes", folderincludes)
|
config.set(section, "folderincludes", folderincludes)
|
||||||
|
|
||||||
self.config = config
|
|
||||||
|
|
||||||
def sigterm_handler(signum, frame):
|
|
||||||
# die immediately
|
|
||||||
ui = getglobalui()
|
|
||||||
ui.terminate(errormsg="terminating...")
|
|
||||||
|
|
||||||
signal.signal(signal.SIGTERM,sigterm_handler)
|
|
||||||
|
|
||||||
try:
|
|
||||||
pidfd = open(config.getmetadatadir() + "/pid", "w")
|
|
||||||
pidfd.write(str(os.getpid()) + "\n")
|
|
||||||
pidfd.close()
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
try:
|
|
||||||
if options.logfile:
|
if options.logfile:
|
||||||
sys.stderr = ui.logfile
|
sys.stderr = self.ui.logfile
|
||||||
|
|
||||||
socktimeout = config.getdefaultint("general", "socktimeout", 0)
|
socktimeout = config.getdefaultint("general", "socktimeout", 0)
|
||||||
if socktimeout > 0:
|
if socktimeout > 0:
|
||||||
socket.setdefaulttimeout(socktimeout)
|
socket.setdefaulttimeout(socktimeout)
|
||||||
|
|
||||||
activeaccounts = config.get("general", "accounts")
|
|
||||||
if options.accounts:
|
|
||||||
activeaccounts = options.accounts
|
|
||||||
activeaccounts = activeaccounts.replace(" ", "")
|
|
||||||
activeaccounts = activeaccounts.split(",")
|
|
||||||
allaccounts = accounts.AccountHashGenerator(config)
|
|
||||||
|
|
||||||
syncaccounts = []
|
|
||||||
for account in activeaccounts:
|
|
||||||
if account not in allaccounts:
|
|
||||||
if len(allaccounts) == 0:
|
|
||||||
errormsg = 'The account "%s" does not exist because no accounts are defined!'%account
|
|
||||||
else:
|
|
||||||
errormsg = 'The account "%s" does not exist. Valid accounts are:'%account
|
|
||||||
for name in allaccounts.keys():
|
|
||||||
errormsg += '\n%s'%name
|
|
||||||
ui.terminate(1, errortitle = 'Unknown Account "%s"'%account, errormsg = errormsg)
|
|
||||||
if account not in syncaccounts:
|
|
||||||
syncaccounts.append(account)
|
|
||||||
|
|
||||||
server = None
|
|
||||||
remoterepos = None
|
|
||||||
localrepos = None
|
|
||||||
|
|
||||||
threadutil.initInstanceLimit('ACCOUNTLIMIT',
|
threadutil.initInstanceLimit('ACCOUNTLIMIT',
|
||||||
config.getdefaultint('general',
|
config.getdefaultint('general', 'maxsyncaccounts', 1))
|
||||||
'maxsyncaccounts', 1))
|
|
||||||
|
|
||||||
for reposname in config.getsectionlist('Repository'):
|
for reposname in config.getsectionlist('Repository'):
|
||||||
for instancename in ["FOLDER_" + reposname,
|
for instancename in ["FOLDER_" + reposname,
|
||||||
@ -302,20 +280,65 @@ class OfflineImap:
|
|||||||
threadutil.initInstanceLimit(instancename,
|
threadutil.initInstanceLimit(instancename,
|
||||||
config.getdefaultint('Repository ' + reposname,
|
config.getdefaultint('Repository ' + reposname,
|
||||||
'maxconnections', 2))
|
'maxconnections', 2))
|
||||||
|
self.config = config
|
||||||
|
return (options, args)
|
||||||
|
|
||||||
|
def sync(self, options):
|
||||||
|
"""Invoke the correct single/multithread syncing
|
||||||
|
|
||||||
|
self.config is supposed to have been correctly initialized
|
||||||
|
already."""
|
||||||
|
try:
|
||||||
|
pidfd = open(self.config.getmetadatadir() + "/pid", "w")
|
||||||
|
pidfd.write(str(os.getpid()) + "\n")
|
||||||
|
pidfd.close()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
activeaccounts = self.config.get("general", "accounts")
|
||||||
|
if options.accounts:
|
||||||
|
activeaccounts = options.accounts
|
||||||
|
activeaccounts = activeaccounts.replace(" ", "")
|
||||||
|
activeaccounts = activeaccounts.split(",")
|
||||||
|
allaccounts = accounts.AccountHashGenerator(self.config)
|
||||||
|
|
||||||
|
syncaccounts = []
|
||||||
|
for account in activeaccounts:
|
||||||
|
if account not in allaccounts:
|
||||||
|
if len(allaccounts) == 0:
|
||||||
|
errormsg = "The account '%s' does not exist because no"\
|
||||||
|
" accounts are defined!" % account
|
||||||
|
else:
|
||||||
|
errormsg = "The account '%s' does not exist. Valid ac"\
|
||||||
|
"counts are: " % account
|
||||||
|
errormsg += ", ".join(allaccounts.keys())
|
||||||
|
self.ui.terminate(1, errormsg = errormsg)
|
||||||
|
if account not in syncaccounts:
|
||||||
|
syncaccounts.append(account)
|
||||||
|
|
||||||
def sig_handler(sig, frame):
|
def sig_handler(sig, frame):
|
||||||
if sig == signal.SIGUSR1 or sig == signal.SIGHUP:
|
if sig == signal.SIGUSR1 or sig == signal.SIGHUP:
|
||||||
# tell each account to stop sleeping
|
# tell each account to stop sleeping
|
||||||
accounts.Account.set_abort_event(self.config, 1)
|
accounts.Account.set_abort_event(self.config, 1)
|
||||||
elif sig == signal.SIGUSR2:
|
elif sig == signal.SIGUSR2:
|
||||||
# tell each account to stop looping
|
# tell each account to stop looping
|
||||||
|
getglobalui().warn("Terminating after this sync...")
|
||||||
accounts.Account.set_abort_event(self.config, 2)
|
accounts.Account.set_abort_event(self.config, 2)
|
||||||
|
elif sig == signal.SIGTERM or sig == signal.SIGINT:
|
||||||
|
# tell each account to ABORT ASAP (ctrl-c)
|
||||||
|
getglobalui().warn("Terminating NOW (this may "\
|
||||||
|
"take a few seconds)...")
|
||||||
|
accounts.Account.set_abort_event(self.config, 3)
|
||||||
|
|
||||||
signal.signal(signal.SIGHUP,sig_handler)
|
signal.signal(signal.SIGHUP,sig_handler)
|
||||||
signal.signal(signal.SIGUSR1,sig_handler)
|
signal.signal(signal.SIGUSR1,sig_handler)
|
||||||
signal.signal(signal.SIGUSR2,sig_handler)
|
signal.signal(signal.SIGUSR2,sig_handler)
|
||||||
|
signal.signal(signal.SIGTERM, sig_handler)
|
||||||
|
signal.signal(signal.SIGINT, sig_handler)
|
||||||
|
|
||||||
#various initializations that need to be performed:
|
#various initializations that need to be performed:
|
||||||
offlineimap.mbnames.init(config, syncaccounts)
|
offlineimap.mbnames.init(self.config, syncaccounts)
|
||||||
|
|
||||||
#TODO: keep legacy lock for a few versions, then remove.
|
#TODO: keep legacy lock for a few versions, then remove.
|
||||||
self._legacy_lock = open(self.config.getmetadatadir() + "/lock",
|
self._legacy_lock = open(self.config.getmetadatadir() + "/lock",
|
||||||
@ -331,34 +354,39 @@ class OfflineImap:
|
|||||||
|
|
||||||
if options.singlethreading:
|
if options.singlethreading:
|
||||||
#singlethreaded
|
#singlethreaded
|
||||||
self.sync_singlethreaded(syncaccounts, config)
|
self.sync_singlethreaded(syncaccounts)
|
||||||
else:
|
else:
|
||||||
# multithreaded
|
# multithreaded
|
||||||
t = threadutil.ExitNotifyThread(target=syncmaster.syncitall,
|
t = threadutil.ExitNotifyThread(target=syncmaster.syncitall,
|
||||||
name='Sync Runner',
|
name='Sync Runner',
|
||||||
kwargs = {'accounts': syncaccounts,
|
kwargs = {'accounts': syncaccounts,
|
||||||
'config': config})
|
'config': self.config})
|
||||||
t.setDaemon(1)
|
|
||||||
t.start()
|
t.start()
|
||||||
threadutil.exitnotifymonitorloop(threadutil.threadexited)
|
threadutil.exitnotifymonitorloop(threadutil.threadexited)
|
||||||
|
self.ui.terminate()
|
||||||
ui.terminate()
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
ui.terminate(1, errormsg = 'CTRL-C pressed, aborting...')
|
|
||||||
return
|
|
||||||
except (SystemExit):
|
except (SystemExit):
|
||||||
raise
|
raise
|
||||||
except Exception, e:
|
except Exception, e:
|
||||||
ui.error(e)
|
self.ui.error(e)
|
||||||
ui.terminate()
|
self.ui.terminate()
|
||||||
|
|
||||||
def sync_singlethreaded(self, accs, config):
|
def sync_singlethreaded(self, accs):
|
||||||
"""Executed if we do not want a separate syncmaster thread
|
"""Executed if we do not want a separate syncmaster thread
|
||||||
|
|
||||||
:param accs: A list of accounts that should be synced
|
:param accs: A list of accounts that should be synced
|
||||||
:param config: The CustomConfig object
|
|
||||||
"""
|
"""
|
||||||
for accountname in accs:
|
for accountname in accs:
|
||||||
account = offlineimap.accounts.SyncableAccount(config, accountname)
|
account = offlineimap.accounts.SyncableAccount(self.config,
|
||||||
|
accountname)
|
||||||
threading.currentThread().name = "Account sync %s" % accountname
|
threading.currentThread().name = "Account sync %s" % accountname
|
||||||
account.syncrunner()
|
account.syncrunner()
|
||||||
|
|
||||||
|
def serverdiagnostics(self, options):
|
||||||
|
activeaccounts = self.config.get("general", "accounts")
|
||||||
|
if options.accounts:
|
||||||
|
activeaccounts = options.accounts
|
||||||
|
activeaccounts = activeaccounts.split(",")
|
||||||
|
allaccounts = accounts.AccountListGenerator(self.config)
|
||||||
|
for account in allaccounts:
|
||||||
|
if account.name not in activeaccounts: continue
|
||||||
|
account.serverdiagnostics()
|
||||||
|
@ -207,9 +207,8 @@ class BaseRepository(object, CustomConfig.ConfigHelperMixin):
|
|||||||
src_repo.makefolder(newsrc_name)
|
src_repo.makefolder(newsrc_name)
|
||||||
src_haschanged = True # Need to refresh list
|
src_haschanged = True # Need to refresh list
|
||||||
except OfflineImapError, e:
|
except OfflineImapError, e:
|
||||||
self.ui.error(e, exc_info()[2],
|
self.ui.error(e, exc_info()[2], "Creating folder %s on "
|
||||||
"Creating folder %s on repository %s" %\
|
"repository %s" % (newsrc_name, src_repo))
|
||||||
(src_name, dst_repo))
|
|
||||||
raise
|
raise
|
||||||
status_repo.makefolder(newsrc_name.replace(
|
status_repo.makefolder(newsrc_name.replace(
|
||||||
src_repo.getsep(), status_repo.getsep()))
|
src_repo.getsep(), status_repo.getsep()))
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
# IMAP repository support
|
# IMAP repository support
|
||||||
# Copyright (C) 2002 John Goerzen
|
# Copyright (C) 2002-2011 John Goerzen & contributors
|
||||||
# <jgoerzen@complete.org>
|
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
@ -75,6 +74,13 @@ class IMAPRepository(BaseRepository):
|
|||||||
return num
|
return num
|
||||||
|
|
||||||
def getsep(self):
|
def getsep(self):
|
||||||
|
"""Return the folder separator for the IMAP repository
|
||||||
|
|
||||||
|
This requires that self.imapserver has been initialized with an
|
||||||
|
acquireconnection() or it will still be `None`"""
|
||||||
|
assert self.imapserver.delim != None, "'%s' " \
|
||||||
|
"repository called getsep() before the folder separator was " \
|
||||||
|
"queried from the server" % self
|
||||||
return self.imapserver.delim
|
return self.imapserver.delim
|
||||||
|
|
||||||
def gethost(self):
|
def gethost(self):
|
||||||
@ -171,7 +177,7 @@ class IMAPRepository(BaseRepository):
|
|||||||
return self.getconf('preauthtunnel', None)
|
return self.getconf('preauthtunnel', None)
|
||||||
|
|
||||||
def getreference(self):
|
def getreference(self):
|
||||||
return self.getconf('reference', '""')
|
return self.getconf('reference', '')
|
||||||
|
|
||||||
def getidlefolders(self):
|
def getidlefolders(self):
|
||||||
localeval = self.localeval
|
localeval = self.localeval
|
||||||
@ -316,14 +322,9 @@ class IMAPRepository(BaseRepository):
|
|||||||
when you are done creating folders yourself.
|
when you are done creating folders yourself.
|
||||||
|
|
||||||
:param foldername: Full path of the folder to be created."""
|
:param foldername: Full path of the folder to be created."""
|
||||||
#TODO: IMHO this existing commented out code is correct and
|
if self.getreference():
|
||||||
#should be enabled, but this would change the behavior for
|
foldername = self.getreference() + self.getsep() + foldername
|
||||||
#existing configurations who have a 'reference' set on a Mapped
|
|
||||||
#IMAP server....:
|
|
||||||
#if self.getreference() != '""':
|
|
||||||
# newname = self.getreference() + self.getsep() + foldername
|
|
||||||
#else:
|
|
||||||
# newname = foldername
|
|
||||||
imapobj = self.imapserver.acquireconnection()
|
imapobj = self.imapserver.acquireconnection()
|
||||||
try:
|
try:
|
||||||
self.ui._msg("Creating new IMAP folder '%s' on server %s" %\
|
self.ui._msg("Creating new IMAP folder '%s' on server %s" %\
|
||||||
|
@ -149,14 +149,12 @@ class MaildirRepository(BaseRepository):
|
|||||||
|
|
||||||
# Iterate over directories in top & top itself.
|
# 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)
|
self.debug(" dirname = %s" % dirname)
|
||||||
if dirname in ['cur', 'new', 'tmp']:
|
if dirname in ['cur', 'new', 'tmp']:
|
||||||
self.debug(" skipping this dir (Maildir special)")
|
self.debug(" skipping this dir (Maildir special)")
|
||||||
# Bypass special files.
|
# Bypass special files.
|
||||||
continue
|
continue
|
||||||
fullname = os.path.join(toppath, dirname)
|
fullname = os.path.join(toppath, dirname)
|
||||||
self.debug(" fullname = %s" % fullname)
|
|
||||||
if not os.path.isdir(fullname):
|
if not os.path.isdir(fullname):
|
||||||
self.debug(" skipping this entry (not a directory)")
|
self.debug(" skipping this entry (not a directory)")
|
||||||
# Not a directory -- not a folder.
|
# Not a directory -- not a folder.
|
||||||
|
@ -25,12 +25,13 @@ def syncaccount(threads, config, accountname):
|
|||||||
thread = InstanceLimitedThread(instancename = 'ACCOUNTLIMIT',
|
thread = InstanceLimitedThread(instancename = 'ACCOUNTLIMIT',
|
||||||
target = account.syncrunner,
|
target = account.syncrunner,
|
||||||
name = "Account sync %s" % accountname)
|
name = "Account sync %s" % accountname)
|
||||||
thread.setDaemon(1)
|
thread.setDaemon(True)
|
||||||
thread.start()
|
thread.start()
|
||||||
threads.add(thread)
|
threads.add(thread)
|
||||||
|
|
||||||
def syncitall(accounts, config):
|
def syncitall(accounts, config):
|
||||||
currentThread().setExitMessage('SYNC_WITH_TIMER_TERMINATE')
|
# Special exit message for SyncRunner thread, so main thread can exit
|
||||||
|
currentThread().exit_message = 'SYNCRUNNER_EXITED_NORMALLY'
|
||||||
threads = threadlist()
|
threads = threadlist()
|
||||||
for accountname in accounts:
|
for accountname in accounts:
|
||||||
syncaccount(threads, config, accountname)
|
syncaccount(threads, config, accountname)
|
||||||
|
@ -28,8 +28,8 @@ from offlineimap.ui import getglobalui
|
|||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
def semaphorereset(semaphore, originalstate):
|
def semaphorereset(semaphore, originalstate):
|
||||||
"""Wait until the semaphore gets back to its original state -- all acquired
|
"""Block until `semaphore` gets back to its original state, ie all acquired
|
||||||
resources released."""
|
resources have been released."""
|
||||||
for i in range(originalstate):
|
for i in range(originalstate):
|
||||||
semaphore.acquire()
|
semaphore.acquire()
|
||||||
# Now release these.
|
# Now release these.
|
||||||
@ -81,6 +81,7 @@ exitthreads = Queue(100)
|
|||||||
def exitnotifymonitorloop(callback):
|
def exitnotifymonitorloop(callback):
|
||||||
"""An infinite "monitoring" loop watching for finished ExitNotifyThread's.
|
"""An infinite "monitoring" loop watching for finished ExitNotifyThread's.
|
||||||
|
|
||||||
|
This one is supposed to run in the main thread.
|
||||||
:param callback: the function to call when a thread terminated. That
|
:param callback: the function to call when a thread terminated. That
|
||||||
function is called with a single argument -- the
|
function is called with a single argument -- the
|
||||||
ExitNotifyThread that has terminated. The monitor will
|
ExitNotifyThread that has terminated. The monitor will
|
||||||
@ -94,41 +95,58 @@ def exitnotifymonitorloop(callback):
|
|||||||
:type callback: a callable function
|
:type callback: a callable function
|
||||||
"""
|
"""
|
||||||
global exitthreads
|
global exitthreads
|
||||||
while 1:
|
do_loop = True
|
||||||
|
while do_loop:
|
||||||
# Loop forever and call 'callback' for each thread that exited
|
# Loop forever and call 'callback' for each thread that exited
|
||||||
try:
|
try:
|
||||||
# we need a timeout in the get() call, so that ctrl-c can throw
|
# we need a timeout in the get() call, so that ctrl-c can throw
|
||||||
# a SIGINT (http://bugs.python.org/issue1360). A timeout with empty
|
# a SIGINT (http://bugs.python.org/issue1360). A timeout with empty
|
||||||
# Queue will raise `Empty`.
|
# Queue will raise `Empty`.
|
||||||
thrd = exitthreads.get(True, 60)
|
thrd = exitthreads.get(True, 60)
|
||||||
callback(thrd)
|
# request to abort when callback returns true
|
||||||
|
do_loop = (callback(thrd) != True)
|
||||||
except Empty:
|
except Empty:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def threadexited(thread):
|
def threadexited(thread):
|
||||||
"""Called when a thread exits."""
|
"""Called when a thread exits.
|
||||||
|
|
||||||
|
Main thread is aborted when this returns True."""
|
||||||
ui = getglobalui()
|
ui = getglobalui()
|
||||||
if thread.getExitCause() == 'EXCEPTION':
|
if thread.exit_exception:
|
||||||
if isinstance(thread.getExitException(), SystemExit):
|
if isinstance(thread.exit_exception, SystemExit):
|
||||||
# Bring a SystemExit into the main thread.
|
# Bring a SystemExit into the main thread.
|
||||||
# Do not send it back to UI layer right now.
|
# Do not send it back to UI layer right now.
|
||||||
# Maybe later send it to ui.terminate?
|
# Maybe later send it to ui.terminate?
|
||||||
raise SystemExit
|
raise SystemExit
|
||||||
ui.threadException(thread) # Expected to terminate
|
ui.threadException(thread) # Expected to terminate
|
||||||
sys.exit(100) # Just in case...
|
sys.exit(100) # Just in case...
|
||||||
elif thread.getExitMessage() == 'SYNC_WITH_TIMER_TERMINATE':
|
elif thread.exit_message == 'SYNCRUNNER_EXITED_NORMALLY':
|
||||||
ui.terminate()
|
return True
|
||||||
# Just in case...
|
|
||||||
sys.exit(100)
|
|
||||||
else:
|
else:
|
||||||
ui.threadExited(thread)
|
ui.threadExited(thread)
|
||||||
|
return False
|
||||||
|
|
||||||
class ExitNotifyThread(Thread):
|
class ExitNotifyThread(Thread):
|
||||||
"""This class is designed to alert a "monitor" to the fact that a thread has
|
"""This class is designed to alert a "monitor" to the fact that a
|
||||||
exited and to provide for the ability for it to find out why."""
|
thread has exited and to provide for the ability for it to find out
|
||||||
|
why. All instances are made daemon threads (setDaemon(True), so we
|
||||||
|
bail out when the mainloop dies.
|
||||||
|
|
||||||
|
The thread can set instance variables self.exit_message for a human
|
||||||
|
readable reason of the thread exit."""
|
||||||
profiledir = None
|
profiledir = None
|
||||||
"""class variable that is set to the profile directory if required"""
|
"""class variable that is set to the profile directory if required"""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(ExitNotifyThread, self).__init__(*args, **kwargs)
|
||||||
|
# These are all child threads that are supposed to go away when
|
||||||
|
# the main thread is killed.
|
||||||
|
self.setDaemon(True)
|
||||||
|
self.exit_message = None
|
||||||
|
self._exit_exc = None
|
||||||
|
self._exit_stacktrace = None
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
global exitthreads
|
global exitthreads
|
||||||
self.threadid = get_ident()
|
self.threadid = get_ident()
|
||||||
@ -147,49 +165,31 @@ class ExitNotifyThread(Thread):
|
|||||||
pass
|
pass
|
||||||
prof.dump_stats(os.path.join(ExitNotifyThread.profiledir,
|
prof.dump_stats(os.path.join(ExitNotifyThread.profiledir,
|
||||||
"%s_%s.prof" % (self.threadid, self.getName())))
|
"%s_%s.prof" % (self.threadid, self.getName())))
|
||||||
except:
|
except Exception, e:
|
||||||
self.setExitCause('EXCEPTION')
|
# Thread exited with Exception, store it
|
||||||
if sys:
|
|
||||||
self.setExitException(sys.exc_info()[1])
|
|
||||||
tb = traceback.format_exc()
|
tb = traceback.format_exc()
|
||||||
self.setExitStackTrace(tb)
|
self.set_exit_exception(e, tb)
|
||||||
else:
|
|
||||||
self.setExitCause('NORMAL')
|
|
||||||
if not hasattr(self, 'exitmessage'):
|
|
||||||
self.setExitMessage(None)
|
|
||||||
|
|
||||||
if exitthreads:
|
if exitthreads:
|
||||||
exitthreads.put(self, True)
|
exitthreads.put(self, True)
|
||||||
|
|
||||||
def setExitCause(self, cause):
|
def set_exit_exception(self, exc, st=None):
|
||||||
self.exitcause = cause
|
"""Sets Exception and stacktrace of a thread, so that other
|
||||||
def getExitCause(self):
|
threads can query its exit status"""
|
||||||
|
self._exit_exc = exc
|
||||||
|
self._exit_stacktrace = st
|
||||||
|
|
||||||
|
@property
|
||||||
|
def exit_exception(self):
|
||||||
"""Returns the cause of the exit, one of:
|
"""Returns the cause of the exit, one of:
|
||||||
'EXCEPTION' -- the thread aborted because of an exception
|
Exception() -- the thread aborted with this exception
|
||||||
'NORMAL' -- normal termination."""
|
None -- normal termination."""
|
||||||
return self.exitcause
|
return self._exit_exc
|
||||||
def setExitException(self, exc):
|
|
||||||
self.exitexception = exc
|
@property
|
||||||
def getExitException(self):
|
def exit_stacktrace(self):
|
||||||
"""If getExitCause() is 'EXCEPTION', holds the value from
|
"""Returns a string representing the stack trace if set"""
|
||||||
sys.exc_info()[1] for this exception."""
|
return self._exit_stacktrace
|
||||||
return self.exitexception
|
|
||||||
def setExitStackTrace(self, st):
|
|
||||||
self.exitstacktrace = st
|
|
||||||
def getExitStackTrace(self):
|
|
||||||
"""If getExitCause() is 'EXCEPTION', returns a string representing
|
|
||||||
the stack trace for this exception."""
|
|
||||||
return self.exitstacktrace
|
|
||||||
def setExitMessage(self, msg):
|
|
||||||
"""Sets the exit message to be fetched by a subsequent call to
|
|
||||||
getExitMessage. This message may be any object or type except
|
|
||||||
None."""
|
|
||||||
self.exitmessage = msg
|
|
||||||
def getExitMessage(self):
|
|
||||||
"""For any exit cause, returns the message previously set by
|
|
||||||
a call to setExitMessage(), or None if there was no such message
|
|
||||||
set."""
|
|
||||||
return self.exitmessage
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def set_profiledir(cls, directory):
|
def set_profiledir(cls, directory):
|
||||||
|
@ -1,148 +0,0 @@
|
|||||||
# Blinkenlights base classes
|
|
||||||
# Copyright (C) 2003 John Goerzen
|
|
||||||
# <jgoerzen@complete.org>
|
|
||||||
#
|
|
||||||
# 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
|
|
||||||
# the Free Software Foundation; either version 2 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
|
||||||
|
|
||||||
from threading import RLock, currentThread
|
|
||||||
from offlineimap.ui.UIBase import UIBase
|
|
||||||
from thread import get_ident # python < 2.6 support
|
|
||||||
|
|
||||||
class BlinkenBase:
|
|
||||||
"""This is a mix-in class that should be mixed in with either UIBase
|
|
||||||
or another appropriate base class. The Tk interface, for instance,
|
|
||||||
will probably mix it in with VerboseUI."""
|
|
||||||
|
|
||||||
def acct(s, accountname):
|
|
||||||
s.gettf().setcolor('purple')
|
|
||||||
s.__class__.__bases__[-1].acct(s, accountname)
|
|
||||||
|
|
||||||
def connecting(s, hostname, port):
|
|
||||||
s.gettf().setcolor('gray')
|
|
||||||
s.__class__.__bases__[-1].connecting(s, hostname, port)
|
|
||||||
|
|
||||||
def syncfolders(s, srcrepos, destrepos):
|
|
||||||
s.gettf().setcolor('blue')
|
|
||||||
s.__class__.__bases__[-1].syncfolders(s, srcrepos, destrepos)
|
|
||||||
|
|
||||||
def syncingfolder(s, srcrepos, srcfolder, destrepos, destfolder):
|
|
||||||
s.gettf().setcolor('cyan')
|
|
||||||
s.__class__.__bases__[-1].syncingfolder(s, srcrepos, srcfolder, destrepos, destfolder)
|
|
||||||
|
|
||||||
def skippingfolder(s, folder):
|
|
||||||
s.gettf().setcolor('cyan')
|
|
||||||
s.__class__.__bases__[-1].skippingfolder(s, folder)
|
|
||||||
|
|
||||||
def loadmessagelist(s, repos, folder):
|
|
||||||
s.gettf().setcolor('green')
|
|
||||||
s._msg("Scanning folder [%s/%s]" % (s.getnicename(repos),
|
|
||||||
folder.getvisiblename()))
|
|
||||||
|
|
||||||
def syncingmessages(s, sr, sf, dr, df):
|
|
||||||
s.gettf().setcolor('blue')
|
|
||||||
s.__class__.__bases__[-1].syncingmessages(s, sr, sf, dr, df)
|
|
||||||
|
|
||||||
def copyingmessage(s, uid, num, num_to_copy, src, destfolder):
|
|
||||||
s.gettf().setcolor('orange')
|
|
||||||
s.__class__.__bases__[-1].copyingmessage(s, uid, num, num_to_copy, src,
|
|
||||||
destfolder)
|
|
||||||
|
|
||||||
def deletingmessages(s, uidlist, destlist):
|
|
||||||
s.gettf().setcolor('red')
|
|
||||||
s.__class__.__bases__[-1].deletingmessages(s, uidlist, destlist)
|
|
||||||
|
|
||||||
def deletingmessage(s, uid, destlist):
|
|
||||||
s.gettf().setcolor('red')
|
|
||||||
s.__class__.__bases__[-1].deletingmessage(s, uid, destlist)
|
|
||||||
|
|
||||||
def addingflags(s, uidlist, flags, dest):
|
|
||||||
s.gettf().setcolor('yellow')
|
|
||||||
s.__class__.__bases__[-1].addingflags(s, uidlist, flags, dest)
|
|
||||||
|
|
||||||
def deletingflags(s, uidlist, flags, dest):
|
|
||||||
s.gettf().setcolor('pink')
|
|
||||||
s.__class__.__bases__[-1].deletingflags(s, uidlist, flags, dest)
|
|
||||||
|
|
||||||
def warn(s, msg, minor = 0):
|
|
||||||
if minor:
|
|
||||||
s.gettf().setcolor('pink')
|
|
||||||
else:
|
|
||||||
s.gettf().setcolor('red')
|
|
||||||
s.__class__.__bases__[-1].warn(s, msg, minor)
|
|
||||||
|
|
||||||
def init_banner(s):
|
|
||||||
s.availablethreadframes = {}
|
|
||||||
s.threadframes = {}
|
|
||||||
#tflock protects the s.threadframes manipulation to only happen from 1 thread
|
|
||||||
s.tflock = RLock()
|
|
||||||
|
|
||||||
def threadExited(s, thread):
|
|
||||||
threadid = thread.threadid
|
|
||||||
accountname = s.getthreadaccount(thread)
|
|
||||||
s.tflock.acquire()
|
|
||||||
try:
|
|
||||||
if threadid in s.threadframes[accountname]:
|
|
||||||
tf = s.threadframes[accountname][threadid]
|
|
||||||
del s.threadframes[accountname][threadid]
|
|
||||||
s.availablethreadframes[accountname].append(tf)
|
|
||||||
tf.setthread(None)
|
|
||||||
finally:
|
|
||||||
s.tflock.release()
|
|
||||||
|
|
||||||
UIBase.threadExited(s, thread)
|
|
||||||
|
|
||||||
def gettf(s):
|
|
||||||
threadid = get_ident()
|
|
||||||
accountname = s.getthreadaccount()
|
|
||||||
|
|
||||||
s.tflock.acquire()
|
|
||||||
|
|
||||||
try:
|
|
||||||
if not accountname in s.threadframes:
|
|
||||||
s.threadframes[accountname] = {}
|
|
||||||
|
|
||||||
if threadid in s.threadframes[accountname]:
|
|
||||||
return s.threadframes[accountname][threadid]
|
|
||||||
|
|
||||||
if not accountname in s.availablethreadframes:
|
|
||||||
s.availablethreadframes[accountname] = []
|
|
||||||
|
|
||||||
if len(s.availablethreadframes[accountname]):
|
|
||||||
tf = s.availablethreadframes[accountname].pop(0)
|
|
||||||
tf.setthread(currentThread())
|
|
||||||
else:
|
|
||||||
tf = s.getaccountframe().getnewthreadframe()
|
|
||||||
s.threadframes[accountname][threadid] = tf
|
|
||||||
return tf
|
|
||||||
finally:
|
|
||||||
s.tflock.release()
|
|
||||||
|
|
||||||
def callhook(s, msg):
|
|
||||||
s.gettf().setcolor('white')
|
|
||||||
s.__class__.__bases__[-1].callhook(s, msg)
|
|
||||||
|
|
||||||
def sleep(s, sleepsecs, account):
|
|
||||||
s.gettf().setcolor('red')
|
|
||||||
s.getaccountframe().startsleep(sleepsecs)
|
|
||||||
return UIBase.sleep(s, sleepsecs, account)
|
|
||||||
|
|
||||||
def sleeping(s, sleepsecs, remainingsecs):
|
|
||||||
if remainingsecs and s.gettf().getcolor() == 'black':
|
|
||||||
s.gettf().setcolor('red')
|
|
||||||
else:
|
|
||||||
s.gettf().setcolor('black')
|
|
||||||
return s.getaccountframe().sleeping(sleepsecs, remainingsecs)
|
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,4 @@
|
|||||||
# Copyright (C) 2007 John Goerzen
|
# Copyright (C) 2007-2011 John Goerzen & contributors
|
||||||
# <jgoerzen@complete.org>
|
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
@ -18,52 +17,34 @@
|
|||||||
import urllib
|
import urllib
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
import logging
|
||||||
from UIBase import UIBase
|
from UIBase import UIBase
|
||||||
from threading import currentThread, Lock
|
from threading import currentThread
|
||||||
import offlineimap
|
import offlineimap
|
||||||
|
|
||||||
protocol = '6.0.0'
|
protocol = '7.0.0'
|
||||||
|
|
||||||
class MachineUI(UIBase):
|
class MachineUI(UIBase):
|
||||||
def __init__(s, config, verbose = 0):
|
def __init__(self, config, loglevel = logging.INFO):
|
||||||
UIBase.__init__(s, config, verbose)
|
super(MachineUI, self).__init__(config, loglevel)
|
||||||
s.safechars=" ;,./-_=+()[]"
|
self._log_con_handler.createLock()
|
||||||
s.iswaiting = 0
|
"""lock needed to block on password input"""
|
||||||
s.outputlock = Lock()
|
|
||||||
s._printData('__init__', protocol)
|
|
||||||
|
|
||||||
def isusable(s):
|
def _printData(self, command, msg):
|
||||||
return True
|
self.logger.info("%s:%s:%s:%s" % (
|
||||||
|
'msg', command, currentThread().getName(), msg))
|
||||||
|
|
||||||
def _printData(s, command, data, dolock = True):
|
def _msg(s, msg):
|
||||||
s._printDataOut('msg', command, data, dolock)
|
|
||||||
|
|
||||||
def _printWarn(s, command, data, dolock = True):
|
|
||||||
s._printDataOut('warn', command, data, dolock)
|
|
||||||
|
|
||||||
def _printDataOut(s, datatype, command, data, dolock = True):
|
|
||||||
if dolock:
|
|
||||||
s.outputlock.acquire()
|
|
||||||
try:
|
|
||||||
print "%s:%s:%s:%s" % \
|
|
||||||
(datatype,
|
|
||||||
urllib.quote(command, s.safechars),
|
|
||||||
urllib.quote(currentThread().getName(), s.safechars),
|
|
||||||
urllib.quote(data, s.safechars))
|
|
||||||
sys.stdout.flush()
|
|
||||||
finally:
|
|
||||||
if dolock:
|
|
||||||
s.outputlock.release()
|
|
||||||
|
|
||||||
def _display(s, msg):
|
|
||||||
s._printData('_display', msg)
|
s._printData('_display', msg)
|
||||||
|
|
||||||
def warn(s, msg, minor = 0):
|
def warn(s, msg, minor = 0):
|
||||||
s._printData('warn', '%s\n%d' % (msg, int(minor)))
|
# TODO, remove and cleanup the unused minor stuff
|
||||||
|
self.logger.warning("%s:%s:%s:%s" % (
|
||||||
|
'warn', '', currentThread().getName(), msg))
|
||||||
|
|
||||||
def registerthread(s, account):
|
def registerthread(self, account):
|
||||||
UIBase.registerthread(s, account)
|
super(MachineUI, self).registerthread(self, account)
|
||||||
s._printData('registerthread', account)
|
self._printData('registerthread', account)
|
||||||
|
|
||||||
def unregisterthread(s, thread):
|
def unregisterthread(s, thread):
|
||||||
UIBase.unregisterthread(s, thread)
|
UIBase.unregisterthread(s, thread)
|
||||||
@ -116,9 +97,6 @@ class MachineUI(UIBase):
|
|||||||
def folderlist(s, list):
|
def folderlist(s, list):
|
||||||
return ("\f".join(["%s\t%s" % (s.getnicename(x), x.getname()) for x in list]))
|
return ("\f".join(["%s\t%s" % (s.getnicename(x), x.getname()) for x in list]))
|
||||||
|
|
||||||
def deletingmessage(s, uid, destlist):
|
|
||||||
s.deletingmessages(s, [uid], destlist)
|
|
||||||
|
|
||||||
def uidlist(s, list):
|
def uidlist(s, list):
|
||||||
return ("\f".join([str(u) for u in list]))
|
return ("\f".join([str(u) for u in list]))
|
||||||
|
|
||||||
@ -161,19 +139,21 @@ class MachineUI(UIBase):
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def getpass(s, accountname, config, errmsg = None):
|
def getpass(self, accountname, config, errmsg = None):
|
||||||
s.outputlock.acquire()
|
|
||||||
try:
|
|
||||||
if errmsg:
|
if errmsg:
|
||||||
s._printData('getpasserror', "%s\n%s" % (accountname, errmsg),
|
self._printData('getpasserror', "%s\n%s" % (accountname, errmsg),
|
||||||
False)
|
False)
|
||||||
s._printData('getpass', accountname, False)
|
|
||||||
|
self._log_con_handler.acquire() # lock the console output
|
||||||
|
try:
|
||||||
|
self._printData('getpass', accountname, False)
|
||||||
return (sys.stdin.readline()[:-1])
|
return (sys.stdin.readline()[:-1])
|
||||||
finally:
|
finally:
|
||||||
s.outputlock.release()
|
self._log_con_handler.release()
|
||||||
|
|
||||||
def init_banner(s):
|
def init_banner(self):
|
||||||
s._printData('initbanner', offlineimap.banner)
|
self._printData('protocol', protocol)
|
||||||
|
self._printData('initbanner', offlineimap.banner)
|
||||||
|
|
||||||
def callhook(s, msg):
|
def callhook(self, msg):
|
||||||
s._printData('callhook', msg)
|
self._printData('callhook', msg)
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
# Noninteractive UI
|
# Noninteractive UI
|
||||||
# Copyright (C) 2002 John Goerzen
|
# Copyright (C) 2002-2011 John Goerzen & contributors
|
||||||
# <jgoerzen@complete.org>
|
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
@ -16,37 +15,15 @@
|
|||||||
# 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 sys
|
import logging
|
||||||
import time
|
|
||||||
from UIBase import UIBase
|
from UIBase import UIBase
|
||||||
|
|
||||||
class Basic(UIBase):
|
class Basic(UIBase):
|
||||||
def getpass(s, accountname, config, errmsg = None):
|
"""'Quiet' simply sets log level to INFO"""
|
||||||
raise NotImplementedError, "Prompting for a password is not supported in noninteractive mode."
|
def __init__(self, config, loglevel = logging.INFO):
|
||||||
|
return super(Basic, self).__init__(config, loglevel)
|
||||||
|
|
||||||
def _display(s, msg):
|
class Quiet(UIBase):
|
||||||
print msg
|
"""'Quiet' simply sets log level to WARNING"""
|
||||||
sys.stdout.flush()
|
def __init__(self, config, loglevel = logging.WARNING):
|
||||||
|
return super(Quiet, self).__init__(config, loglevel)
|
||||||
def warn(s, msg, minor = 0):
|
|
||||||
warntxt = 'WARNING'
|
|
||||||
if minor:
|
|
||||||
warntxt = 'warning'
|
|
||||||
sys.stderr.write(warntxt + ": " + str(msg) + "\n")
|
|
||||||
|
|
||||||
def sleep(s, sleepsecs, siglistener):
|
|
||||||
if s.verbose >= 0:
|
|
||||||
s._msg("Sleeping for %d:%02d" % (sleepsecs / 60, sleepsecs % 60))
|
|
||||||
return UIBase.sleep(s, sleepsecs, siglistener)
|
|
||||||
|
|
||||||
def sleeping(s, sleepsecs, remainingsecs):
|
|
||||||
if sleepsecs > 0:
|
|
||||||
time.sleep(sleepsecs)
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def locked(s):
|
|
||||||
s.warn("Another OfflineIMAP is running with the same metadatadir; exiting.")
|
|
||||||
|
|
||||||
class Quiet(Basic):
|
|
||||||
def __init__(s, config, verbose = -1):
|
|
||||||
Basic.__init__(s, config, verbose)
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
# TTY UI
|
# TTY UI
|
||||||
# Copyright (C) 2002 John Goerzen
|
# Copyright (C) 2002-2011 John Goerzen & contributors
|
||||||
# <jgoerzen@complete.org>
|
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
@ -15,53 +14,73 @@
|
|||||||
# 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
|
||||||
from UIBase import UIBase
|
|
||||||
from getpass import getpass
|
import logging
|
||||||
import sys
|
import sys
|
||||||
from threading import Lock, currentThread
|
from getpass import getpass
|
||||||
|
from offlineimap import banner
|
||||||
|
from offlineimap.ui.UIBase import UIBase
|
||||||
|
|
||||||
|
class TTYFormatter(logging.Formatter):
|
||||||
|
"""Specific Formatter that adds thread information to the log output"""
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(TTYFormatter, self).__init__(*args, **kwargs)
|
||||||
|
self._last_log_thread = None
|
||||||
|
|
||||||
|
def format(self, record):
|
||||||
|
"""Override format to add thread information"""
|
||||||
|
log_str = super(TTYFormatter, self).format(record)
|
||||||
|
# If msg comes from a different thread than our last, prepend
|
||||||
|
# thread info. Most look like 'Account sync foo' or 'Folder
|
||||||
|
# sync foo'.
|
||||||
|
t_name = record.threadName
|
||||||
|
if t_name == 'MainThread':
|
||||||
|
return log_str # main thread doesn't get things prepended
|
||||||
|
if t_name != self._last_log_thread:
|
||||||
|
self._last_log_thread = t_name
|
||||||
|
log_str = "%s:\n %s" % (t_name, log_str)
|
||||||
|
else:
|
||||||
|
log_str = " %s" % log_str
|
||||||
|
return log_str
|
||||||
|
|
||||||
class TTYUI(UIBase):
|
class TTYUI(UIBase):
|
||||||
def __init__(s, config, verbose = 0):
|
def setup_consolehandler(self):
|
||||||
UIBase.__init__(s, config, verbose)
|
"""Backend specific console handler
|
||||||
s.iswaiting = 0
|
|
||||||
s.outputlock = Lock()
|
|
||||||
s._lastThreaddisplay = None
|
|
||||||
|
|
||||||
def isusable(s):
|
Sets up things and adds them to self.logger.
|
||||||
|
:returns: The logging.Handler() for console output"""
|
||||||
|
# create console handler with a higher log level
|
||||||
|
ch = logging.StreamHandler()
|
||||||
|
#ch.setLevel(logging.DEBUG)
|
||||||
|
# create formatter and add it to the handlers
|
||||||
|
self.formatter = TTYFormatter("%(message)s")
|
||||||
|
ch.setFormatter(self.formatter)
|
||||||
|
# add the handlers to the logger
|
||||||
|
self.logger.addHandler(ch)
|
||||||
|
self.logger.info(banner)
|
||||||
|
# init lock for console output
|
||||||
|
ch.createLock()
|
||||||
|
return ch
|
||||||
|
|
||||||
|
def isusable(self):
|
||||||
|
"""TTYUI is reported as usable when invoked on a terminal"""
|
||||||
return sys.stdout.isatty() and sys.stdin.isatty()
|
return sys.stdout.isatty() and sys.stdin.isatty()
|
||||||
|
|
||||||
def _display(s, msg):
|
def getpass(self, accountname, config, errmsg = None):
|
||||||
s.outputlock.acquire()
|
"""TTYUI backend is capable of querying the password"""
|
||||||
try:
|
|
||||||
#if the next output comes from a different thread than our last one
|
|
||||||
#add the info.
|
|
||||||
#Most look like 'account sync foo' or 'Folder sync foo'.
|
|
||||||
threadname = currentThread().getName()
|
|
||||||
if (threadname == s._lastThreaddisplay \
|
|
||||||
or threadname == 'MainThread'):
|
|
||||||
print " %s" % msg
|
|
||||||
else:
|
|
||||||
print "%s:\n %s" % (threadname, msg)
|
|
||||||
s._lastThreaddisplay = threadname
|
|
||||||
|
|
||||||
sys.stdout.flush()
|
|
||||||
finally:
|
|
||||||
s.outputlock.release()
|
|
||||||
|
|
||||||
def getpass(s, accountname, config, errmsg = None):
|
|
||||||
if errmsg:
|
if errmsg:
|
||||||
s._msg("%s: %s" % (accountname, errmsg))
|
self.warn("%s: %s" % (accountname, errmsg))
|
||||||
s.outputlock.acquire()
|
self._log_con_handler.acquire() # lock the console output
|
||||||
try:
|
try:
|
||||||
return getpass("%s: Enter password: " % accountname)
|
return getpass("Enter password for account '%s': " % accountname)
|
||||||
finally:
|
finally:
|
||||||
s.outputlock.release()
|
self._log_con_handler.release()
|
||||||
|
|
||||||
def mainException(s):
|
def mainException(self):
|
||||||
if isinstance(sys.exc_info()[1], KeyboardInterrupt) and \
|
if isinstance(sys.exc_info()[1], KeyboardInterrupt):
|
||||||
s.iswaiting:
|
self.logger.warn("Timer interrupted at user request; program "
|
||||||
sys.stdout.write("Timer interrupted at user request; program terminating. \n")
|
"terminating.\n")
|
||||||
s.terminate()
|
self.terminate()
|
||||||
else:
|
else:
|
||||||
UIBase.mainException(s)
|
UIBase.mainException(self)
|
||||||
|
|
||||||
|
@ -15,12 +15,15 @@
|
|||||||
# 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 logging
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
import sys
|
import sys
|
||||||
|
import os
|
||||||
import traceback
|
import traceback
|
||||||
import threading
|
import threading
|
||||||
from Queue import Queue
|
from Queue import Queue
|
||||||
|
from collections import deque
|
||||||
import offlineimap
|
import offlineimap
|
||||||
|
|
||||||
debugtypes = {'':'Other offlineimap related sync messages',
|
debugtypes = {'':'Other offlineimap related sync messages',
|
||||||
@ -38,54 +41,72 @@ def getglobalui():
|
|||||||
global globalui
|
global globalui
|
||||||
return globalui
|
return globalui
|
||||||
|
|
||||||
class UIBase:
|
class UIBase(object):
|
||||||
def __init__(s, config, verbose = 0):
|
def __init__(self, config, loglevel = logging.INFO):
|
||||||
s.verbose = verbose
|
self.config = config
|
||||||
s.config = config
|
self.debuglist = []
|
||||||
s.debuglist = []
|
"""list of debugtypes we are supposed to log"""
|
||||||
s.debugmessages = {}
|
self.debugmessages = {}
|
||||||
s.debugmsglen = 50
|
"""debugmessages in a deque(v) per thread(k)"""
|
||||||
s.threadaccounts = {}
|
self.debugmsglen = 15
|
||||||
|
self.threadaccounts = {}
|
||||||
"""dict linking active threads (k) to account names (v)"""
|
"""dict linking active threads (k) to account names (v)"""
|
||||||
s.acct_startimes = {}
|
self.acct_startimes = {}
|
||||||
"""linking active accounts with the time.time() when sync started"""
|
"""linking active accounts with the time.time() when sync started"""
|
||||||
s.logfile = None
|
self.logfile = None
|
||||||
s.exc_queue = Queue()
|
self.exc_queue = Queue()
|
||||||
"""saves all occuring exceptions, so we can output them at the end"""
|
"""saves all occuring exceptions, so we can output them at the end"""
|
||||||
|
# create logger with 'OfflineImap' app
|
||||||
|
self.logger = logging.getLogger('OfflineImap')
|
||||||
|
self.logger.setLevel(loglevel)
|
||||||
|
self._log_con_handler = self.setup_consolehandler()
|
||||||
|
"""The console handler (we need access to be able to lock it)"""
|
||||||
|
|
||||||
################################################## UTILS
|
################################################## UTILS
|
||||||
def _msg(s, msg):
|
def setup_consolehandler(self):
|
||||||
"""Generic tool called when no other works."""
|
"""Backend specific console handler
|
||||||
s._log(msg)
|
|
||||||
s._display(msg)
|
|
||||||
|
|
||||||
def _log(s, msg):
|
Sets up things and adds them to self.logger.
|
||||||
"""Log it to disk. Returns true if it wrote something; false
|
:returns: The logging.Handler() for console output"""
|
||||||
otherwise."""
|
# create console handler with a higher log level
|
||||||
if s.logfile:
|
ch = logging.StreamHandler()
|
||||||
s.logfile.write("%s: %s\n" % (threading.currentThread().getName(),
|
#ch.setLevel(logging.DEBUG)
|
||||||
msg))
|
# create formatter and add it to the handlers
|
||||||
return 1
|
self.formatter = logging.Formatter("%(message)s")
|
||||||
return 0
|
ch.setFormatter(self.formatter)
|
||||||
|
# add the handlers to the logger
|
||||||
|
self.logger.addHandler(ch)
|
||||||
|
self.logger.info(offlineimap.banner)
|
||||||
|
return ch
|
||||||
|
|
||||||
def setlogfd(s, logfd):
|
def setlogfile(self, logfile):
|
||||||
s.logfile = logfd
|
"""Create file handler which logs to file"""
|
||||||
logfd.write("This is %s %s\n" % \
|
fh = logging.FileHandler(logfile, 'at')
|
||||||
(offlineimap.__productname__,
|
#fh.setLevel(logging.DEBUG)
|
||||||
offlineimap.__version__))
|
file_formatter = logging.Formatter("%(asctime)s %(levelname)s: "
|
||||||
logfd.write("Python: %s\n" % sys.version)
|
"%(message)s", '%Y-%m-%d %H:%M:%S')
|
||||||
logfd.write("Platform: %s\n" % sys.platform)
|
fh.setFormatter(file_formatter)
|
||||||
logfd.write("Args: %s\n" % sys.argv)
|
self.logger.addHandler(fh)
|
||||||
|
# write out more verbose initial info blurb on the log file
|
||||||
|
p_ver = ".".join([str(x) for x in sys.version_info[0:3]])
|
||||||
|
msg = "OfflineImap %s starting...\n Python: %s Platform: %s\n "\
|
||||||
|
"Args: %s" % (offlineimap.__version__, p_ver, sys.platform,
|
||||||
|
" ".join(sys.argv))
|
||||||
|
record = logging.LogRecord('OfflineImap', logging.INFO, __file__,
|
||||||
|
None, msg, None, None)
|
||||||
|
fh.emit(record)
|
||||||
|
|
||||||
def _display(s, msg):
|
def _msg(self, msg):
|
||||||
"""Display a message."""
|
"""Display a message."""
|
||||||
raise NotImplementedError
|
# TODO: legacy function, rip out.
|
||||||
|
self.info(msg)
|
||||||
|
|
||||||
def warn(s, msg, minor = 0):
|
def info(self, msg):
|
||||||
if minor:
|
"""Display a message."""
|
||||||
s._msg("warning: " + msg)
|
self.logger.info(msg)
|
||||||
else:
|
|
||||||
s._msg("WARNING: " + msg)
|
def warn(self, msg, minor = 0):
|
||||||
|
self.logger.warning(msg)
|
||||||
|
|
||||||
def error(self, exc, exc_traceback=None, msg=None):
|
def error(self, exc, exc_traceback=None, msg=None):
|
||||||
"""Log a message at severity level ERROR
|
"""Log a message at severity level ERROR
|
||||||
@ -139,47 +160,52 @@ class UIBase:
|
|||||||
self.debug('thread', "Unregister thread '%s'" % thr.getName())
|
self.debug('thread', "Unregister thread '%s'" % thr.getName())
|
||||||
|
|
||||||
def getthreadaccount(self, thr = None):
|
def getthreadaccount(self, thr = None):
|
||||||
"""Get name of account for a thread (current if None)"""
|
"""Get Account() for a thread (current if None)
|
||||||
if not thr:
|
|
||||||
|
If no account has been registered with this thread, return 'None'"""
|
||||||
|
if thr == None:
|
||||||
thr = threading.currentThread()
|
thr = threading.currentThread()
|
||||||
if thr in self.threadaccounts:
|
if thr in self.threadaccounts:
|
||||||
return self.threadaccounts[thr]
|
return self.threadaccounts[thr]
|
||||||
return '*Control' # unregistered thread is '*Control'
|
return None
|
||||||
|
|
||||||
def debug(s, debugtype, msg):
|
def debug(self, debugtype, msg):
|
||||||
thisthread = threading.currentThread()
|
cur_thread = threading.currentThread()
|
||||||
if s.debugmessages.has_key(thisthread):
|
if not self.debugmessages.has_key(cur_thread):
|
||||||
s.debugmessages[thisthread].append("%s: %s" % (debugtype, msg))
|
# deque(..., self.debugmsglen) would be handy but was
|
||||||
else:
|
# introduced in p2.6 only, so we'll need to work around and
|
||||||
s.debugmessages[thisthread] = ["%s: %s" % (debugtype, msg)]
|
# shorten our debugmsg list manually :-(
|
||||||
|
self.debugmessages[cur_thread] = deque()
|
||||||
|
self.debugmessages[cur_thread].append("%s: %s" % (debugtype, msg))
|
||||||
|
|
||||||
while len(s.debugmessages[thisthread]) > s.debugmsglen:
|
# Shorten queue if needed
|
||||||
s.debugmessages[thisthread] = s.debugmessages[thisthread][1:]
|
if len(self.debugmessages[cur_thread]) > self.debugmsglen:
|
||||||
|
self.debugmessages[cur_thread].popleft()
|
||||||
|
|
||||||
if debugtype in s.debuglist:
|
if debugtype in self.debuglist: # log if we are supposed to do so
|
||||||
if not s._log("DEBUG[%s]: %s" % (debugtype, msg)):
|
self.logger.debug("[%s]: %s" % (debugtype, msg))
|
||||||
s._display("DEBUG[%s]: %s" % (debugtype, msg))
|
|
||||||
|
|
||||||
def add_debug(s, debugtype):
|
def add_debug(self, debugtype):
|
||||||
global debugtypes
|
global debugtypes
|
||||||
if debugtype in debugtypes:
|
if debugtype in debugtypes:
|
||||||
if not debugtype in s.debuglist:
|
if not debugtype in self.debuglist:
|
||||||
s.debuglist.append(debugtype)
|
self.debuglist.append(debugtype)
|
||||||
s.debugging(debugtype)
|
self.debugging(debugtype)
|
||||||
else:
|
else:
|
||||||
s.invaliddebug(debugtype)
|
self.invaliddebug(debugtype)
|
||||||
|
|
||||||
def debugging(s, debugtype):
|
def debugging(self, debugtype):
|
||||||
global debugtypes
|
global debugtypes
|
||||||
s._msg("Now debugging for %s: %s" % (debugtype, debugtypes[debugtype]))
|
self.logger.debug("Now debugging for %s: %s" % (debugtype,
|
||||||
|
debugtypes[debugtype]))
|
||||||
|
|
||||||
def invaliddebug(s, debugtype):
|
def invaliddebug(self, debugtype):
|
||||||
s.warn("Invalid debug type: %s" % debugtype)
|
self.warn("Invalid debug type: %s" % debugtype)
|
||||||
|
|
||||||
def locked(s):
|
def locked(s):
|
||||||
raise Exception, "Another OfflineIMAP is running with the same metadatadir; exiting."
|
raise Exception, "Another OfflineIMAP is running with the same metadatadir; exiting."
|
||||||
|
|
||||||
def getnicename(s, object):
|
def getnicename(self, object):
|
||||||
"""Return the type of a repository or Folder as string
|
"""Return the type of a repository or Folder as string
|
||||||
|
|
||||||
(IMAP, Gmail, Maildir, etc...)"""
|
(IMAP, Gmail, Maildir, etc...)"""
|
||||||
@ -187,61 +213,73 @@ class UIBase:
|
|||||||
# Strip off extra stuff.
|
# Strip off extra stuff.
|
||||||
return re.sub('(Folder|Repository)', '', prelimname)
|
return re.sub('(Folder|Repository)', '', prelimname)
|
||||||
|
|
||||||
def isusable(s):
|
def isusable(self):
|
||||||
"""Returns true if this UI object is usable in the current
|
"""Returns true if this UI object is usable in the current
|
||||||
environment. For instance, an X GUI would return true if it's
|
environment. For instance, an X GUI would return true if it's
|
||||||
being run in X with a valid DISPLAY setting, and false otherwise."""
|
being run in X with a valid DISPLAY setting, and false otherwise."""
|
||||||
return 1
|
return True
|
||||||
|
|
||||||
################################################## INPUT
|
################################################## INPUT
|
||||||
|
|
||||||
def getpass(s, accountname, config, errmsg = None):
|
def getpass(self, accountname, config, errmsg = None):
|
||||||
raise NotImplementedError
|
raise NotImplementedError("Prompting for a password is not supported"\
|
||||||
|
" in this UI backend.")
|
||||||
|
|
||||||
def folderlist(s, list):
|
def folderlist(self, list):
|
||||||
return ', '.join(["%s[%s]" % (s.getnicename(x), x.getname()) for x in list])
|
return ', '.join(["%s[%s]" % \
|
||||||
|
(self.getnicename(x), x.getname()) for x in list])
|
||||||
|
|
||||||
################################################## WARNINGS
|
################################################## WARNINGS
|
||||||
def msgtoreadonly(s, destfolder, uid, content, flags):
|
def msgtoreadonly(self, destfolder, uid, content, flags):
|
||||||
if not (s.config.has_option('general', 'ignore-readonly') and s.config.getboolean("general", "ignore-readonly")):
|
if self.config.has_option('general', 'ignore-readonly') and \
|
||||||
s.warn("Attempted to synchronize message %d to folder %s[%s], but that folder is read-only. The message will not be copied to that folder." % \
|
self.config.getboolean('general', 'ignore-readonly'):
|
||||||
(uid, s.getnicename(destfolder), destfolder.getname()))
|
return
|
||||||
|
self.warn("Attempted to synchronize message %d to folder %s[%s], "
|
||||||
|
"but that folder is read-only. The message will not be "
|
||||||
|
"copied to that folder." % (
|
||||||
|
uid, self.getnicename(destfolder), destfolder))
|
||||||
|
|
||||||
def flagstoreadonly(s, destfolder, uidlist, flags):
|
def flagstoreadonly(self, destfolder, uidlist, flags):
|
||||||
if not (s.config.has_option('general', 'ignore-readonly') and s.config.getboolean("general", "ignore-readonly")):
|
if self.config.has_option('general', 'ignore-readonly') and \
|
||||||
s.warn("Attempted to modify flags for messages %s in folder %s[%s], but that folder is read-only. No flags have been modified for that message." % \
|
self.config.getboolean('general', 'ignore-readonly'):
|
||||||
(str(uidlist), s.getnicename(destfolder), destfolder.getname()))
|
return
|
||||||
|
self.warn("Attempted to modify flags for messages %s in folder %s[%s], "
|
||||||
|
"but that folder is read-only. No flags have been modified "
|
||||||
|
"for that message." % (
|
||||||
|
str(uidlist), self.getnicename(destfolder), destfolder))
|
||||||
|
|
||||||
def deletereadonly(s, destfolder, uidlist):
|
def deletereadonly(self, destfolder, uidlist):
|
||||||
if not (s.config.has_option('general', 'ignore-readonly') and s.config.getboolean("general", "ignore-readonly")):
|
if self.config.has_option('general', 'ignore-readonly') and \
|
||||||
s.warn("Attempted to delete messages %s in folder %s[%s], but that folder is read-only. No messages have been deleted in that folder." % \
|
self.config.getboolean('general', 'ignore-readonly'):
|
||||||
(str(uidlist), s.getnicename(destfolder), destfolder.getname()))
|
return
|
||||||
|
self.warn("Attempted to delete messages %s in folder %s[%s], but that "
|
||||||
|
"folder is read-only. No messages have been deleted in that "
|
||||||
|
"folder." % (str(uidlist), self.getnicename(destfolder),
|
||||||
|
destfolder))
|
||||||
|
|
||||||
################################################## MESSAGES
|
################################################## MESSAGES
|
||||||
|
|
||||||
def init_banner(s):
|
def init_banner(self):
|
||||||
"""Called when the UI starts. Must be called before any other UI
|
"""Called when the UI starts. Must be called before any other UI
|
||||||
call except isusable(). Displays the copyright banner. This is
|
call except isusable(). Displays the copyright banner. This is
|
||||||
where the UI should do its setup -- TK, for instance, would
|
where the UI should do its setup -- TK, for instance, would
|
||||||
create the application window here."""
|
create the application window here."""
|
||||||
if s.verbose >= 0:
|
pass
|
||||||
s._msg(offlineimap.banner)
|
|
||||||
|
|
||||||
def connecting(s, hostname, port):
|
def connecting(self, hostname, port):
|
||||||
"""Log 'Establishing connection to'"""
|
"""Log 'Establishing connection to'"""
|
||||||
if s.verbose < 0: return
|
if not self.logger.isEnabledFor(logging.info): return
|
||||||
displaystr = ''
|
displaystr = ''
|
||||||
hostname = hostname if hostname else ''
|
hostname = hostname if hostname else ''
|
||||||
port = "%s" % port if port else ''
|
port = "%s" % port if port else ''
|
||||||
if hostname:
|
if hostname:
|
||||||
displaystr = ' to %s:%s' % (hostname, port)
|
displaystr = ' to %s:%s' % (hostname, port)
|
||||||
s._msg("Establishing connection%s" % displaystr)
|
self.logger.info("Establishing connection%s" % displaystr)
|
||||||
|
|
||||||
def acct(self, account):
|
def acct(self, account):
|
||||||
"""Output that we start syncing an account (and start counting)"""
|
"""Output that we start syncing an account (and start counting)"""
|
||||||
self.acct_startimes[account] = time.time()
|
self.acct_startimes[account] = time.time()
|
||||||
if self.verbose >= 0:
|
self.logger.info("*** Processing account %s" % account)
|
||||||
self._msg("*** Processing account %s" % account)
|
|
||||||
|
|
||||||
def acctdone(self, account):
|
def acctdone(self, account):
|
||||||
"""Output that we finished syncing an account (in which time)"""
|
"""Output that we finished syncing an account (in which time)"""
|
||||||
@ -252,141 +290,175 @@ class UIBase:
|
|||||||
|
|
||||||
def syncfolders(self, src_repo, dst_repo):
|
def syncfolders(self, src_repo, dst_repo):
|
||||||
"""Log 'Copying folder structure...'"""
|
"""Log 'Copying folder structure...'"""
|
||||||
if self.verbose < 0: return
|
if self.logger.isEnabledFor(logging.DEBUG):
|
||||||
self.debug('', "Copying folder structure from %s to %s" % \
|
self.debug('', "Copying folder structure from %s to %s" %\
|
||||||
(src_repo, dst_repo))
|
(src_repo, dst_repo))
|
||||||
|
|
||||||
############################## Folder syncing
|
############################## Folder syncing
|
||||||
def syncingfolder(s, srcrepos, srcfolder, destrepos, destfolder):
|
def syncingfolder(self, srcrepos, srcfolder, destrepos, destfolder):
|
||||||
"""Called when a folder sync operation is started."""
|
"""Called when a folder sync operation is started."""
|
||||||
if s.verbose >= 0:
|
self.logger.info("Syncing %s: %s -> %s" % (srcfolder,
|
||||||
s._msg("Syncing %s: %s -> %s" % (srcfolder.getname(),
|
self.getnicename(srcrepos),
|
||||||
s.getnicename(srcrepos),
|
self.getnicename(destrepos)))
|
||||||
s.getnicename(destrepos)))
|
|
||||||
|
|
||||||
def skippingfolder(s, folder):
|
def skippingfolder(self, folder):
|
||||||
"""Called when a folder sync operation is started."""
|
"""Called when a folder sync operation is started."""
|
||||||
if s.verbose >= 0:
|
self.logger.info("Skipping %s (not changed)" % folder)
|
||||||
s._msg("Skipping %s (not changed)" % folder.getname())
|
|
||||||
|
|
||||||
def validityproblem(s, folder):
|
def validityproblem(self, folder):
|
||||||
s.warn("UID validity problem for folder %s (repo %s) (saved %d; got %d); skipping it" % \
|
self.logger.warning("UID validity problem for folder %s (repo %s) "
|
||||||
(folder.getname(), folder.getrepository().getname(),
|
"(saved %d; got %d); skipping it. Please see FAQ "
|
||||||
|
"and manual how to handle this." % \
|
||||||
|
(folder, folder.getrepository(),
|
||||||
folder.getsaveduidvalidity(), folder.getuidvalidity()))
|
folder.getsaveduidvalidity(), folder.getuidvalidity()))
|
||||||
|
|
||||||
def loadmessagelist(s, repos, folder):
|
def loadmessagelist(self, repos, folder):
|
||||||
if s.verbose > 0:
|
self.logger.debug("Loading message list for %s[%s]" % (
|
||||||
s._msg("Loading message list for %s[%s]" % (s.getnicename(repos),
|
self.getnicename(repos),
|
||||||
folder.getname()))
|
folder))
|
||||||
|
|
||||||
def messagelistloaded(s, repos, folder, count):
|
def messagelistloaded(self, repos, folder, count):
|
||||||
if s.verbose > 0:
|
self.logger.debug("Message list for %s[%s] loaded: %d messages" % (
|
||||||
s._msg("Message list for %s[%s] loaded: %d messages" % \
|
self.getnicename(repos), folder, count))
|
||||||
(s.getnicename(repos), folder.getname(), count))
|
|
||||||
|
|
||||||
############################## Message syncing
|
############################## Message syncing
|
||||||
|
|
||||||
def syncingmessages(s, sr, sf, dr, df):
|
def syncingmessages(self, sr, srcfolder, dr, dstfolder):
|
||||||
if s.verbose > 0:
|
self.logger.debug("Syncing messages %s[%s] -> %s[%s]" % (
|
||||||
s._msg("Syncing messages %s[%s] -> %s[%s]" % (s.getnicename(sr),
|
self.getnicename(sr), srcfolder,
|
||||||
sf.getname(),
|
self.getnicename(dr), dstfolder))
|
||||||
s.getnicename(dr),
|
|
||||||
df.getname()))
|
|
||||||
|
|
||||||
def copyingmessage(self, uid, num, num_to_copy, src, destfolder):
|
def copyingmessage(self, uid, num, num_to_copy, src, destfolder):
|
||||||
"""Output a log line stating which message we copy"""
|
"""Output a log line stating which message we copy"""
|
||||||
if self.verbose < 0: return
|
self.logger.info("Copy message %s (%d of %d) %s:%s -> %s" % (
|
||||||
self._msg("Copy message %s (%d of %d) %s:%s -> %s" % (uid, num,
|
uid, num, num_to_copy, src.repository, src,
|
||||||
num_to_copy, src.repository, src, destfolder.repository))
|
destfolder.repository))
|
||||||
|
|
||||||
def deletingmessage(s, uid, destlist):
|
def deletingmessages(self, uidlist, destlist):
|
||||||
if s.verbose >= 0:
|
ds = self.folderlist(destlist)
|
||||||
ds = s.folderlist(destlist)
|
self.logger.info("Deleting %d messages (%s) in %s" % (
|
||||||
s._msg("Deleting message %d in %s" % (uid, ds))
|
len(uidlist),
|
||||||
|
offlineimap.imaputil.uid_sequence(uidlist), ds))
|
||||||
|
|
||||||
def deletingmessages(s, uidlist, destlist):
|
def addingflags(self, uidlist, flags, dest):
|
||||||
if s.verbose >= 0:
|
self.logger.info("Adding flag %s to %d messages on %s" % (
|
||||||
ds = s.folderlist(destlist)
|
", ".join(flags), len(uidlist), dest))
|
||||||
s._msg("Deleting %d messages (%s) in %s" % \
|
|
||||||
(len(uidlist),
|
|
||||||
offlineimap.imaputil.uid_sequence(uidlist),
|
|
||||||
ds))
|
|
||||||
|
|
||||||
def addingflags(s, uidlist, flags, dest):
|
def deletingflags(self, uidlist, flags, dest):
|
||||||
if s.verbose >= 0:
|
self.logger.info("Deleting flag %s from %d messages on %s" % (
|
||||||
s._msg("Adding flag %s to %d messages on %s" % \
|
", ".join(flags), len(uidlist), dest))
|
||||||
(", ".join(flags), len(uidlist), dest))
|
|
||||||
|
|
||||||
def deletingflags(s, uidlist, flags, dest):
|
def serverdiagnostics(self, repository, type):
|
||||||
if s.verbose >= 0:
|
"""Connect to repository and output useful information for debugging"""
|
||||||
s._msg("Deleting flag %s from %d messages on %s" % \
|
conn = None
|
||||||
(", ".join(flags), len(uidlist), dest))
|
self._msg("%s repository '%s': type '%s'" % (type, repository.name,
|
||||||
|
self.getnicename(repository)))
|
||||||
|
try:
|
||||||
|
if hasattr(repository, 'gethost'): # IMAP
|
||||||
|
self._msg("Host: %s Port: %s SSL: %s" % (repository.gethost(),
|
||||||
|
repository.getport(),
|
||||||
|
repository.getssl()))
|
||||||
|
try:
|
||||||
|
conn = repository.imapserver.acquireconnection()
|
||||||
|
except OfflineImapError, e:
|
||||||
|
self._msg("Failed to connect. Reason %s" % e)
|
||||||
|
else:
|
||||||
|
if 'ID' in conn.capabilities:
|
||||||
|
self._msg("Server supports ID extension.")
|
||||||
|
#TODO: Debug and make below working, it hangs Gmail
|
||||||
|
#res_type, response = conn.id((
|
||||||
|
# 'name', offlineimap.__productname__,
|
||||||
|
# 'version', offlineimap.__version__))
|
||||||
|
#self._msg("Server ID: %s %s" % (res_type, response[0]))
|
||||||
|
self._msg("Server welcome string: %s" % str(conn.welcome))
|
||||||
|
self._msg("Server capabilities: %s\n" % str(conn.capabilities))
|
||||||
|
repository.imapserver.releaseconnection(conn)
|
||||||
|
if type != 'Status':
|
||||||
|
folderfilter = repository.getconf('folderfilter', None)
|
||||||
|
if folderfilter:
|
||||||
|
self._msg("folderfilter= %s\n" % folderfilter)
|
||||||
|
folderincludes = repository.getconf('folderincludes', None)
|
||||||
|
if folderincludes:
|
||||||
|
self._msg("folderincludes= %s\n" % folderincludes)
|
||||||
|
nametrans = repository.getconf('nametrans', None)
|
||||||
|
if nametrans:
|
||||||
|
self._msg("nametrans= %s\n" % nametrans)
|
||||||
|
|
||||||
|
folders = repository.getfolders()
|
||||||
|
foldernames = [(f.name, f.getvisiblename()) for f in folders]
|
||||||
|
folders = []
|
||||||
|
for name, visiblename in foldernames:
|
||||||
|
if name == visiblename: folders.append(name)
|
||||||
|
else: folders.append("%s -> %s" % (name, visiblename))
|
||||||
|
self._msg("Folderlist: %s\n" % str(folders))
|
||||||
|
finally:
|
||||||
|
if conn: #release any existing IMAP connection
|
||||||
|
repository.imapserver.close()
|
||||||
|
|
||||||
################################################## Threads
|
################################################## Threads
|
||||||
|
|
||||||
def getThreadDebugLog(s, thread):
|
def getThreadDebugLog(self, thread):
|
||||||
if s.debugmessages.has_key(thread):
|
if self.debugmessages.has_key(thread):
|
||||||
message = "\nLast %d debug messages logged for %s prior to exception:\n"\
|
message = "\nLast %d debug messages logged for %s prior to exception:\n"\
|
||||||
% (len(s.debugmessages[thread]), thread.getName())
|
% (len(self.debugmessages[thread]), thread.getName())
|
||||||
message += "\n".join(s.debugmessages[thread])
|
message += "\n".join(self.debugmessages[thread])
|
||||||
else:
|
else:
|
||||||
message = "\nNo debug messages were logged for %s." % \
|
message = "\nNo debug messages were logged for %s." % \
|
||||||
thread.getName()
|
thread.getName()
|
||||||
return message
|
return message
|
||||||
|
|
||||||
def delThreadDebugLog(s, thread):
|
def delThreadDebugLog(self, thread):
|
||||||
if s.debugmessages.has_key(thread):
|
if thread in self.debugmessages:
|
||||||
del s.debugmessages[thread]
|
del self.debugmessages[thread]
|
||||||
|
|
||||||
def getThreadExceptionString(s, thread):
|
def getThreadExceptionString(self, thread):
|
||||||
message = "Thread '%s' terminated with exception:\n%s" % \
|
message = "Thread '%s' terminated with exception:\n%s" % \
|
||||||
(thread.getName(), thread.getExitStackTrace())
|
(thread.getName(), thread.exit_stacktrace)
|
||||||
message += "\n" + s.getThreadDebugLog(thread)
|
message += "\n" + self.getThreadDebugLog(thread)
|
||||||
return message
|
return message
|
||||||
|
|
||||||
def threadException(s, thread):
|
def threadException(self, thread):
|
||||||
"""Called when a thread has terminated with an exception.
|
"""Called when a thread has terminated with an exception.
|
||||||
The argument is the ExitNotifyThread that has so terminated."""
|
The argument is the ExitNotifyThread that has so terminated."""
|
||||||
s._msg(s.getThreadExceptionString(thread))
|
self.warn(self.getThreadExceptionString(thread))
|
||||||
s.delThreadDebugLog(thread)
|
self.delThreadDebugLog(thread)
|
||||||
s.terminate(100)
|
self.terminate(100)
|
||||||
|
|
||||||
def terminate(self, exitstatus = 0, errortitle = None, errormsg = None):
|
def terminate(self, exitstatus = 0, errortitle = None, errormsg = None):
|
||||||
"""Called to terminate the application."""
|
"""Called to terminate the application."""
|
||||||
#print any exceptions that have occurred over the run
|
#print any exceptions that have occurred over the run
|
||||||
if not self.exc_queue.empty():
|
if not self.exc_queue.empty():
|
||||||
self._msg("\nERROR: Exceptions occurred during the run!")
|
self.warn("ERROR: Exceptions occurred during the run!")
|
||||||
while not self.exc_queue.empty():
|
while not self.exc_queue.empty():
|
||||||
msg, exc, exc_traceback = self.exc_queue.get()
|
msg, exc, exc_traceback = self.exc_queue.get()
|
||||||
if msg:
|
if msg:
|
||||||
self._msg("ERROR: %s\n %s" % (msg, exc))
|
self.warn("ERROR: %s\n %s" % (msg, exc))
|
||||||
else:
|
else:
|
||||||
self._msg("ERROR: %s" % (exc))
|
self.warn("ERROR: %s" % (exc))
|
||||||
if exc_traceback:
|
if exc_traceback:
|
||||||
self._msg("\nTraceback:\n%s" %"".join(
|
self.warn("\nTraceback:\n%s" %"".join(
|
||||||
traceback.format_tb(exc_traceback)))
|
traceback.format_tb(exc_traceback)))
|
||||||
|
|
||||||
if errormsg and errortitle:
|
if errormsg and errortitle:
|
||||||
sys.stderr.write('ERROR: %s\n\n%s\n'%(errortitle, errormsg))
|
self.warn('ERROR: %s\n\n%s\n'%(errortitle, errormsg))
|
||||||
elif errormsg:
|
elif errormsg:
|
||||||
sys.stderr.write('%s\n' % errormsg)
|
self.warn('%s\n' % errormsg)
|
||||||
sys.exit(exitstatus)
|
sys.exit(exitstatus)
|
||||||
|
|
||||||
def threadExited(s, thread):
|
def threadExited(self, thread):
|
||||||
"""Called when a thread has exited normally. Many UIs will
|
"""Called when a thread has exited normally. Many UIs will
|
||||||
just ignore this."""
|
just ignore this."""
|
||||||
s.delThreadDebugLog(thread)
|
self.delThreadDebugLog(thread)
|
||||||
s.unregisterthread(thread)
|
self.unregisterthread(thread)
|
||||||
|
|
||||||
################################################## Hooks
|
################################################## Hooks
|
||||||
|
|
||||||
def callhook(s, msg):
|
def callhook(self, msg):
|
||||||
if s.verbose >= 0:
|
self.info(msg)
|
||||||
s._msg(msg)
|
|
||||||
|
|
||||||
################################################## Other
|
################################################## Other
|
||||||
|
|
||||||
def sleep(s, sleepsecs, account):
|
def sleep(self, sleepsecs, account):
|
||||||
"""This function does not actually output anything, but handles
|
"""This function does not actually output anything, but handles
|
||||||
the overall sleep, dealing with updates as necessary. It will,
|
the overall sleep, dealing with updates as necessary. It will,
|
||||||
however, call sleeping() which DOES output something.
|
however, call sleeping() which DOES output something.
|
||||||
@ -399,12 +471,12 @@ class UIBase:
|
|||||||
if account.get_abort_event():
|
if account.get_abort_event():
|
||||||
abortsleep = True
|
abortsleep = True
|
||||||
else:
|
else:
|
||||||
abortsleep = s.sleeping(10, sleepsecs)
|
abortsleep = self.sleeping(10, sleepsecs)
|
||||||
sleepsecs -= 10
|
sleepsecs -= 10
|
||||||
s.sleeping(0, 0) # Done sleeping.
|
self.sleeping(0, 0) # Done sleeping.
|
||||||
return abortsleep
|
return abortsleep
|
||||||
|
|
||||||
def sleeping(s, sleepsecs, remainingsecs):
|
def sleeping(self, sleepsecs, remainingsecs):
|
||||||
"""Sleep for sleepsecs, display remainingsecs to go.
|
"""Sleep for sleepsecs, display remainingsecs to go.
|
||||||
|
|
||||||
Does nothing if sleepsecs <= 0.
|
Does nothing if sleepsecs <= 0.
|
||||||
@ -416,6 +488,7 @@ class UIBase:
|
|||||||
"""
|
"""
|
||||||
if sleepsecs > 0:
|
if sleepsecs > 0:
|
||||||
if remainingsecs//60 != (remainingsecs-sleepsecs)//60:
|
if remainingsecs//60 != (remainingsecs-sleepsecs)//60:
|
||||||
s._msg("Next refresh in %.1f minutes" % (remainingsecs/60.0))
|
self.logger.info("Next refresh in %.1f minutes" % (
|
||||||
|
remainingsecs/60.0))
|
||||||
time.sleep(sleepsecs)
|
time.sleep(sleepsecs)
|
||||||
return 0
|
return 0
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# UI module
|
# UI module
|
||||||
# Copyright (C) 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>
|
# Copyright (C) 2010-2011 Sebastian Spaeth & contributors
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
Loading…
x
Reference in New Issue
Block a user