[PATCH 4/4] learn unicode support
Nicolas Sebrecht
nicolas.s-dev at laposte.net
Wed Feb 11 15:09:28 GMT 2015
This feature is NOT WORKING. This is EXPERIMENTAL WIP.
Also, such change is very intrusive/invasive by nature. While the full ASCII
mode is still the default, it is impossible to not impact legacy code when run
without unicode support.
Signed-off-by: Nicolas Sebrecht <nicolas.s-dev at laposte.net>
---
offlineimap.conf | 8 +-
offlineimap/CustomConfig.py | 61 ++++---
offlineimap/__init__.py | 2 +-
offlineimap/accounts.py | 173 +++++++++++--------
offlineimap/folder/Base.py | 198 +++++++++++++---------
offlineimap/folder/Gmail.py | 95 +++++++----
offlineimap/folder/GmailMaildir.py | 85 ++++++----
offlineimap/folder/IMAP.py | 190 ++++++++++++---------
offlineimap/folder/LocalStatus.py | 70 +++++---
offlineimap/folder/LocalStatusSQLite.py | 141 +++++++++-------
offlineimap/folder/Maildir.py | 174 +++++++++++--------
offlineimap/folder/UIDMaps.py | 56 ++++---
offlineimap/imaplibutil.py | 32 ++--
offlineimap/imapserver.py | 277 ++++++++++++++++++-------------
offlineimap/imaputil.py | 20 ++-
offlineimap/init.py | 272 +++++++++++++++++++-----------
offlineimap/localeval.py | 21 ++-
offlineimap/mbnames.py | 96 ++++++++---
offlineimap/repository/Base.py | 121 +++++++++-----
offlineimap/repository/Gmail.py | 24 ++-
offlineimap/repository/IMAP.py | 285 ++++++++++++++++++--------------
offlineimap/repository/LocalStatus.py | 61 ++++---
offlineimap/repository/Maildir.py | 175 +++++++++++++-------
offlineimap/repository/__init__.py | 35 ++--
offlineimap/syncmaster.py | 24 ++-
offlineimap/threadutil.py | 25 ++-
offlineimap/ui/Curses.py | 24 ++-
offlineimap/ui/Machine.py | 95 ++++++-----
offlineimap/ui/TTY.py | 47 ++++--
offlineimap/ui/UIBase.py | 251 ++++++++++++++++------------
offlineimap/ui/debuglock.py | 26 ++-
offlineimap/utils/const.py | 9 +-
offlineimap/utils/distro.py | 16 +-
33 files changed, 1968 insertions(+), 1221 deletions(-)
diff --git a/offlineimap.conf b/offlineimap.conf
index cfafb23..0ad3bcc 100644
--- a/offlineimap.conf
+++ b/offlineimap.conf
@@ -5,8 +5,14 @@
# More details can be found in the included user documention, which is
# also available at: http://docs.offlineimap.org/en/latest/
+# NOTE 0: This file is read with UTF-8 encoding when option is enabled from
+# command line. This means iso-8859-1 and ASCII charsets are natively
+# supported. Otherwise, this file is expected to be full ASCII.
+#
+# If any other charset is used somewhere with success, it's just because you're
+# lucky.
-# NOTE 1: Settings generally support python interpolation. This means
+# NOTE1: Settings generally support python interpolation. This means
# values can contain python format strings which refer to other values
# in the same section, or values in a special DEFAULT section. This
# allows you for example to use common settings for multiple accounts:
diff --git a/offlineimap/CustomConfig.py b/offlineimap/CustomConfig.py
index 44cfcab..6b5d780 100644
--- a/offlineimap/CustomConfig.py
+++ b/offlineimap/CustomConfig.py
@@ -20,9 +20,25 @@ from sys import exc_info
try:
from ConfigParser import SafeConfigParser, Error
-except ImportError: #python3
+except ImportError: # python3
from configparser import SafeConfigParser, Error
+
from offlineimap.localeval import LocalEval
+from offlineimap.utils.uni import uniString, noneString
+
+
+#
+# UNICODE: it would be much better to not use the uni module at all,
+# here. This way, we could consider the classes as a low-level driver.
+#
+# Sadly, the config classes make more than just config-related tasks and
+# supports hooks.
+#
+# Hence, the Unicode strategy here is to use the uni module for the tasks not
+# related to configuration parsing. In order to keep the whole thing consistent,
+# strings are returned with low-level Python types, NOT uni objects.
+#
+
class CustomConfigParser(SafeConfigParser):
def __init__(self):
@@ -75,7 +91,7 @@ class CustomConfigParser(SafeConfigParser):
val = self.get(section, option).strip()
return re.split(separator_re, val)
except re.error as e:
- raise Error("Bad split regexp '%s': %s" % \
+ raise Error("Bad split regexp '%s': %s"%
(separator_re, e)), None, exc_info()[2]
def getdefaultlist(self, section, option, default, separator_re):
@@ -88,12 +104,14 @@ class CustomConfigParser(SafeConfigParser):
return default
def getmetadatadir(self):
+ """Returns metadatadir."""
+
xforms = [os.path.expanduser, os.path.expandvars]
- d = self.getdefault("general", "metadata", "~/.offlineimap")
- metadatadir = self.apply_xforms(d, xforms)
- if not os.path.exists(metadatadir):
- os.mkdir(metadatadir, 0o700)
- return metadatadir
+ d = uniString(self.getdefault("general", "metadata", "~/.offlineimap"))
+ metadatadir = uniString(self.apply_xforms(d.uni, xforms))
+ if not os.path.exists(metadatadir.fs):
+ os.mkdir(metadatadir.fs, 0o700)
+ return metadatadir.uni
def getlocaleval(self):
# We already loaded pythonfile, so return this copy.
@@ -102,10 +120,10 @@ class CustomConfigParser(SafeConfigParser):
xforms = [os.path.expanduser, os.path.expandvars]
if self.has_option("general", "pythonfile"):
- path = self.get("general", "pythonfile")
- path = self.apply_xforms(path, xforms)
+ path = uniString(self.apply_xforms(
+ uniString(self.get("general", "pythonfile")).uni, xforms))
else:
- path = None
+ path = noneString()
self.localeval = LocalEval(path)
return self.localeval
@@ -118,7 +136,7 @@ class CustomConfigParser(SafeConfigParser):
For instance, for "Account Test", returns "Test"."""
- key = key + ' '
+ key = key + u' '
return [x[len(key):] for x in self.sections() \
if x.startswith(key)]
@@ -132,22 +150,27 @@ class CustomConfigParser(SafeConfigParser):
self.set(section, option, value)
- def apply_xforms(self, string, transforms):
- """Applies set of transformations to a string.
+ # FIXME: it could be possible that this method is used from outside, either
+ # today or later. That's why it currently fully works with the uni
+ # objects. The best would be to put this function outside of this module.
+ #
+ # TODO: use the '_' prefix in the name or move it outside.
+ def apply_xforms(self, s, transforms):
+ """Applies set of transformations to a uni object.
Arguments:
- - string: source string; if None, then no processing will
+ - s: source uni String; if None, then no processing will
take place.
- transforms: iterable that returns transformation function
on each turn.
- Returns transformed string."""
+ Returns (unicode) transformed string."""
- if string == None:
+ if s == None:
return None
for f in transforms:
- string = f(string)
- return string
+ s = f(s)
+ return s
@@ -247,7 +270,7 @@ class ConfigHelperMixin:
"""
value = self.getconf(option, default)
- return self.getconfig().apply_xforms(value, xforms)
+ return (self.getconfig().apply_xforms(value, xforms))
def getconfboolean(self, option, default = CustomConfigDefault):
diff --git a/offlineimap/__init__.py b/offlineimap/__init__.py
index 441af44..1126781 100644
--- a/offlineimap/__init__.py
+++ b/offlineimap/__init__.py
@@ -9,7 +9,7 @@ __author__ = "John Goerzen"
__author_email__= "john at complete.org"
__description__ = "Disconnected Universal IMAP Mail Synchronization/Reader Support"
__license__ = "Licensed under the GNU GPL v2 or any later version (with an OpenSSL exception)"
-__bigcopyright__ = """%(__productname__)s %(__bigversion__)s
+__bigcopyright__ = u"""%(__productname__)s %(__bigversion__)s
%(__license__)s""" % locals()
__homepage__ = "http://offlineimap.org"
diff --git a/offlineimap/accounts.py b/offlineimap/accounts.py
index 62ed5c3..b620bcf 100644
--- a/offlineimap/accounts.py
+++ b/offlineimap/accounts.py
@@ -14,6 +14,10 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+#
+# FIXME: we are acutally working with CustomConfig.blah all over the place.
+# Change the filename and class name!
+#
from subprocess import Popen, PIPE
from threading import Event
import os
@@ -25,32 +29,39 @@ from offlineimap import globals
from offlineimap.repository import Repository
from offlineimap.ui import getglobalui
from offlineimap.threadutil import InstanceLimitedThread
+from offlineimap.utils.uni import uniString, fsString, isASCII
+from offlineimap.utils import hack
try:
import fcntl
except:
pass # ok if this fails, we can do without
-# FIXME: spaghetti code alert!
+# FIXME: Bip, Bip! spaghetti code alert!
def getaccountlist(customconfig):
- # Account names in a list.
- return customconfig.getsectionlist('Account')
-
-# FIXME: spaghetti code alert!
+ # Account sections in a list.
+ sections = customconfig.getsectionlist('Account')
+ for section in sections:
+ # section type is unicode and has ASCII restrictions.
+ isASCII(section, exception_msg="section names must be plain ASCII")
+ return sections
+
+# FIXME: Bip, Bip! spaghetti code alert!
def AccountListGenerator(customconfig):
"""Returns a list of instanciated Account class, one per account name."""
- return [Account(customconfig, accountname)
- for accountname in getaccountlist(customconfig)]
+ return [Account(customconfig, uniString(uni_accountname))
+ for uni_accountname in getaccountlist(customconfig)]
-# FIXME: spaghetti code alert!
+# FIXME: Bip, Bip! spaghetti code alert!
+# FIMXE: what a poor function name!
def AccountHashGenerator(customconfig):
- """Returns a dict of instanciated Account class with the account name as
- key."""
+ """Returns a dict of instanciated Account class with the account (unicode)
+ name as key."""
retval = {}
- for item in AccountListGenerator(customconfig):
- retval[item.getname()] = item
+ for inst_account in AccountListGenerator(customconfig):
+ retval[inst_account.getname().uni] = inst_account
return retval
@@ -71,13 +82,13 @@ class Account(CustomConfig.ConfigHelperMixin):
:param config: Representing the offlineimap configuration file.
:type config: :class:`offlineimap.CustomConfig.CustomConfigParser`
- :param name: A (str) string denoting the name of the Account
- as configured.
+ :param name: A (unicode encoded) string denoting the name of the
+ Account as configured.
"""
self.config = config
self.name = name
- self.metadatadir = config.getmetadatadir()
+ self.metadatadir = uniString(config.getmetadatadir())
self.localeval = config.getlocaleval()
# current :mod:`offlineimap.ui`, can be used for logging:
self.ui = getglobalui()
@@ -99,14 +110,19 @@ class Account(CustomConfig.ConfigHelperMixin):
return self.name
def __str__(self):
- return self.name
+ # We should warn to never use this.
+ return self.name.std
def getaccountmeta(self):
- return os.path.join(self.metadatadir, 'Account-' + self.name)
+ return fsString(os.path.join(
+ self.metadatadir.fs, 'Account-' + self.name.fs))
- # Interface from CustomConfig.ConfigHelperMixin
+ # Interface from CustomConfig.ConfigHelperMixin Return unicode string as
+ # CustomConfig.blah are doing. This is more consistent to return the same
+ # type/encoding when working on the same kind of data.
+ # FIXME: why this? Why having a method if it's used once?
def getsection(self):
- return 'Account ' + self.getname()
+ return u'Account ' + self.name.uni
@classmethod
def set_abort_event(cls, config, signum):
@@ -125,8 +141,8 @@ class Account(CustomConfig.ConfigHelperMixin):
if signum == 1:
# resync signal, set config option for all accounts
- for acctsection in getaccountlist(config):
- config.set('Account ' + acctsection, "skipsleep", '1')
+ for uni_acctsection in getaccountlist(config):
+ config.set('Account ' + uni_acctsection, "skipsleep", '1')
elif signum == 2:
# don't autorefresh anymore
cls.abort_soon_signal.set()
@@ -208,13 +224,14 @@ class SyncableAccount(Account):
def __init__(self, *args, **kwargs):
Account.__init__(self, *args, **kwargs)
self._lockfd = None
- self._lockfilepath = os.path.join(
- self.config.getmetadatadir(), "%s.lock"% self)
+ self._lockfilepath = fsString(os.path.join(
+ uniString(self.config.getmetadatadir()).fs,
+ "%s.lock"% self.name.fs))
def __lock(self):
"""Lock the account, throwing an exception if it is locked already."""
- self._lockfd = open(self._lockfilepath, 'w')
+ self._lockfd = open(self._lockfilepath.fs, 'w')
try:
fcntl.lockf(self._lockfd, fcntl.LOCK_EX|fcntl.LOCK_NB)
except NameError:
@@ -223,7 +240,7 @@ class SyncableAccount(Account):
except IOError:
self._lockfd.close()
raise OfflineImapError("Could not lock account %s. Is another "
- "instance using this account?"% self,
+ "instance using this account?"% self.name.fs,
OfflineImapError.ERROR.REPO), None, exc_info()[2]
def _unlock(self):
@@ -233,7 +250,7 @@ class SyncableAccount(Account):
if self._lockfd and not self._lockfd.closed:
self._lockfd.close()
try:
- os.unlink(self._lockfilepath)
+ os.unlink(self._lockfilepath.fs)
except OSError:
pass # Failed to delete for some reason.
@@ -241,8 +258,8 @@ class SyncableAccount(Account):
self.ui.registerthread(self)
try:
accountmetadata = self.getaccountmeta()
- if not os.path.exists(accountmetadata):
- os.mkdir(accountmetadata, 0o700)
+ if not os.path.exists(accountmetadata.fs):
+ os.mkdir(accountmetadata.fs, 0o700)
self.remoterepos = Repository(self, 'remote')
self.localrepos = Repository(self, 'local')
@@ -272,7 +289,7 @@ class SyncableAccount(Account):
self.ui.error(e, exc_info()[2])
except Exception as e:
self.ui.error(e, exc_info()[2], msg=
- "While attempting to sync account '%s'"% self)
+ "While attempting to sync account '%s'"% self.getname().fs)
else:
# after success sync, reset the looping counter to 3
if self.refreshperiod:
@@ -286,9 +303,10 @@ class SyncableAccount(Account):
def get_local_folder(self, remotefolder):
"""Return the corresponding local folder for a given remotefolder."""
- return self.localrepos.getfolder(
- remotefolder.getvisiblename().
- replace(self.remoterepos.getsep(), self.localrepos.getsep()))
+ visiblename = remotefolder.getvisiblename()
+ visiblename.uni = visiblename.uni.replace(
+ self.remoterepos.getsep().uni, self.localrepos.getsep().uni)
+ return self.localrepos.getfolder(visiblename)
def __sync(self):
"""Synchronize the account once, then return.
@@ -299,7 +317,7 @@ class SyncableAccount(Account):
folderthreads = []
- hook = self.getconf('presynchook', '')
+ hook = uniString(self.getconf('presynchook', ''))
self.callhook(hook)
quickconfig = self.getconfint('quick', 0)
@@ -336,20 +354,36 @@ class SyncableAccount(Account):
if Account.abort_NOW_signal.is_set(): break
if not remotefolder.sync_this:
- self.ui.debug('', "Not syncing filtered folder '%s'"
- "[%s]"% (remotefolder, remoterepos))
+ self.ui.debug('', u"Not syncing filtered folder '%s'[%s]"%
+ (remotefolder.getname().uni, remoterepos.getname().uni))
continue # Ignore filtered folder
localfolder = self.get_local_folder(remotefolder)
if not localfolder.sync_this:
- self.ui.debug('', "Not syncing filtered folder '%s'"
- "[%s]"% (localfolder, localfolder.repository))
+ self.ui.debug('', u"Not syncing filtered folder '%s'[%s]"%
+ (localfolder.getname().uni,
+ localfolder.getrepository().getname().uni))
continue # Ignore filtered folder
if not globals.options.singlethreading:
- thread = InstanceLimitedThread(\
- instancename = 'FOLDER_' + self.remoterepos.getname(),
- target = syncfolder,
- name = "Folder %s [acc: %s]"% (remotefolder.getexplainedname(), self),
- args = (self, remotefolder, quick))
+ reponame = self.remoterepos.getname()
+ remotefoldername = remotefolder.getexplainedname()
+ # UNICODE: we have to encode Unicode because Thread module
+ # has str/ASII type assumptions.
+ #
+ # Also, it wouldn't much be a problem if these strings were
+ # not used later for logging. As a consequence, encoding
+ # these variable here is wrong in the sense that we encode
+ # far too much early and the logging might suffer from
+ # replaced non-ASCII characters.
+ #
+ # To make it right, parameters for logging and those
+ # dedicated to the Thread class should be uncoupled from each
+ # other.
+ thread = InstanceLimitedThread(
+ instancename='FOLDER_%s'% reponame, # OK for uni object.
+ target=syncfolder,
+ name="Folder %s [acc: %s]"% (remotefoldername.std,
+ self.getname().std),
+ args=(self, remotefolder, quick))
thread.start()
folderthreads.append(thread)
else:
@@ -363,35 +397,37 @@ class SyncableAccount(Account):
localrepos.forgetfolders()
remoterepos.forgetfolders()
except:
- #error while syncing. Drop all connections that we have, they
- #might be bogus by now (e.g. after suspend)
+ # Error while syncing. Drop all connections that we have, they
+ # might be bogus by now (e.g. after suspend).
localrepos.dropconnections()
remoterepos.dropconnections()
raise
else:
- # sync went fine. Hold or drop depending on config
+ # Sync went fine. Hold or drop depending on config.
localrepos.holdordropconnections()
remoterepos.holdordropconnections()
- hook = self.getconf('postsynchook', '')
+ hook = uniString(self.getconf('postsynchook', ''))
self.callhook(hook)
def callhook(self, cmd):
# check for CTRL-C or SIGTERM and run postsynchook
if Account.abort_NOW_signal.is_set():
return
- if not cmd:
+ # TODO: improve: "not" operation does not makes sense. Explicitly say
+ # what value it could be.
+ if not cmd.uni:
return
try:
- self.ui.callhook("Calling hook: " + cmd)
+ self.ui.callhook(u"Calling hook: "+ cmd.uni)
if self.dryrun: # don't if we are in dry-run mode
return
- p = Popen(cmd, shell=True,
+ p = Popen(cmd.fs, shell=True,
stdin=PIPE, stdout=PIPE, stderr=PIPE,
close_fds=True)
- r = p.communicate()
- self.ui.callhook("Hook stdout: %s\nHook stderr:%s\n"% r)
- self.ui.callhook("Hook return code: %d"% p.returncode)
+ r = fsString(p.communicate())
+ self.ui.callhook(u"Hook stdout: %s\nHook stderr:%s\n"% r.uni)
+ self.ui.callhook(u"Hook return code: %d"% p.returncode)
except (KeyboardInterrupt, SystemExit):
raise
except Exception as e:
@@ -400,7 +436,8 @@ class SyncableAccount(Account):
def syncfolder(account, remotefolder, quick):
"""Synchronizes given remote folder for the specified account.
- Filtered folders on the remote side will not invoke this function."""
+ Filtered folders on the remote side will not invoke this function.
+ This method is called by each account thread."""
remoterepos = account.remoterepos
localrepos = account.localrepos
@@ -413,12 +450,13 @@ def syncfolder(account, remotefolder, quick):
localfolder = account.get_local_folder(remotefolder)
# Write the mailboxes
- mbnames.add(account.name, localfolder.getname(),
+ mbnames.add(account.getname(), localfolder.getname(),
localrepos.getlocalroot())
# Load status folder.
- statusfolder = statusrepos.getfolder(remotefolder.getvisiblename().
- replace(remoterepos.getsep(), statusrepos.getsep()))
+ statusfolder = statusrepos.getfolder(uniString(
+ remotefolder.getvisiblename().uni.replace(
+ remoterepos.getsep().uni, statusrepos.getsep().uni)))
if localfolder.get_uidvalidity() == None:
# This is a new folder, so delete the status cache to be
@@ -464,24 +502,24 @@ def syncfolder(account, remotefolder, quick):
# Load remote folder.
ui.loadmessagelist(remoterepos, remotefolder)
remotefolder.cachemessagelist()
- ui.messagelistloaded(remoterepos, remotefolder,
- remotefolder.getmessagecount())
+ ui.messagelistloaded(
+ remoterepos, remotefolder, remotefolder.getmessagecount())
# Synchronize remote changes.
if not localrepos.getconfboolean('readonly', False):
ui.syncingmessages(remoterepos, remotefolder, localrepos, localfolder)
remotefolder.syncmessagesto(localfolder, statusfolder)
else:
- ui.debug('imap', "Not syncing to read-only repository '%s'" \
- % localrepos.getname())
+ ui.debug('imap', u"Not syncing to read-only repository '%s'"%
+ localrepos.getname().uni)
# Synchronize local changes
if not remoterepos.getconfboolean('readonly', False):
ui.syncingmessages(localrepos, localfolder, remoterepos, remotefolder)
localfolder.syncmessagesto(remotefolder, statusfolder)
else:
- ui.debug('', "Not syncing to read-only repository '%s'" \
- % remoterepos.getname())
+ ui.debug('', u"Not syncing to read-only repository '%s'"%
+ remoterepos.getname().uni)
statusfolder.save()
localrepos.restore_atime()
@@ -492,11 +530,14 @@ def syncfolder(account, remotefolder, quick):
if e.severity > OfflineImapError.ERROR.FOLDER:
raise
else:
- ui.error(e, exc_info()[2], msg = "Aborting sync, folder '%s' "
- "[acc: '%s']" % (localfolder, account))
+ msg = (u"Aborting sync, folder '%s' [acc: '%s']"%
+ (localfolder.getname().uni, account.getname().uni))
+ ui.error(e, exc_info()[2], msg)
except Exception as e:
- ui.error(e, msg = "ERROR in syncfolder for %s folder %s: %s"%
- (account, remotefolder.getvisiblename(), traceback.format_exc()))
+ ui.error(e, msg=u"ERROR in syncfolder for %s folder %s: %s"% (
+ account.getname().fs,
+ remotefolder.getvisiblename().fs,
+ traceback.format_exc()))
finally:
for folder in ["statusfolder", "localfolder", "remotefolder"]:
if folder in locals():
diff --git a/offlineimap/folder/Base.py b/offlineimap/folder/Base.py
index 3a04ef6..c513857 100644
--- a/offlineimap/folder/Base.py
+++ b/offlineimap/folder/Base.py
@@ -23,13 +23,15 @@ from offlineimap import threadutil, emailutil
from offlineimap import globals
from offlineimap.ui import getglobalui
from offlineimap.error import OfflineImapError
+from offlineimap.utils.uni import noneString, uniString, fsString
import offlineimap.accounts
class BaseFolder(object):
def __init__(self, name, repository):
"""
- :param name: Path & name of folder minus root or reference
+ :param name: Path & name (unicode) of folder minus
+ root or reference
:param repository: Repository() in which the folder is.
"""
@@ -37,47 +39,55 @@ class BaseFolder(object):
# Save original name for folderfilter operations
self.ffilter_name = name
# Top level dir name is always ''
- self.root = None
- self.name = name if not name == self.getsep() else ''
+ self.root = noneString()
+ # XXX: Explain why we accept getsep() as folder name. root folder?
+ self.name = name if not name == self.getsep() else uniString(u'')
self.repository = repository
self.visiblename = repository.nametrans(name)
# In case the visiblename becomes '.' or '/' (top-level) we use
# '' as that is the name that e.g. the Maildir scanning will
# return for the top-level dir.
if self.visiblename == self.getsep():
- self.visiblename = ''
+ self.visiblename = uniString(u'')
self.config = repository.getconfig()
utime_from_message_global = self.config.getdefaultboolean(
"general", "utime_from_message", False)
- repo = "Repository " + repository.name
- self._utime_from_message = self.config.getdefaultboolean(repo,
+ repo = uniString(u"Repository ") + repository.getname()
+ self._utime_from_message = self.config.getdefaultboolean(repo.uni,
"utime_from_message", utime_from_message_global)
# Determine if we're running static or dynamic folder filtering
# and check filtering status
self._dynamic_folderfilter = self.config.getdefaultboolean(
- repo, "dynamic_folderfilter", False)
+ repo.uni, "dynamic_folderfilter", False)
self._sync_this = repository.should_sync_folder(self.ffilter_name)
if self._dynamic_folderfilter:
- self.ui.debug('', "Running dynamic folder filtering on '%s'[%s]"%
- (self.ffilter_name, repository))
+ self.ui.debug('',
+ u"Running dynamic folder filtering on '%s'[%s]"%
+ (self.ffilter_name.uni, repository.getname().uni))
elif not self._sync_this:
- self.ui.debug('', "Filtering out '%s'[%s] due to folderfilter"%
- (self.ffilter_name, repository))
+ self.ui.debug('',
+ u"Filtering out '%s'[%s] due to folderfilter"%
+ (self.ffilter_name.uni, repository.getname().uni))
# Passes for syncmessagesto
- self.syncmessagesto_passes = [('copying messages' , self.__syncmessagesto_copy),
- ('deleting messages' , self.__syncmessagesto_delete),
- ('syncing flags' , self.__syncmessagesto_flags)]
+ self.syncmessagesto_passes = [
+ ('copying messages' , self.__syncmessagesto_copy),
+ ('deleting messages', self.__syncmessagesto_delete),
+ ('syncing flags' , self.__syncmessagesto_flags),
+ ]
def getname(self):
- """Returns name"""
+ """Returns name (unicode)."""
+
return self.name
def __str__(self):
- # FIMXE: remove calls of this. We have getname().
- return self.name
+ """Returns name (filesystem encoded)."""
+
+ # XXX: Warn we should not use this because encoding is made implicit.
+ return self.name.fs
@property
def accountname(self):
@@ -96,16 +106,19 @@ class BaseFolder(object):
@property
def utime_from_message(self):
+
return self._utime_from_message
def suggeststhreads(self):
"""Returns true if this folder suggests using threads for actions;
false otherwise. Probably only IMAP will return true."""
+
return 0
def waitforthread(self):
"""Implements method that waits for thread to be usable.
Should be implemented only for folders that suggest threads."""
+
raise NotImplementedError
# XXX: we may need someting like supports_quickstatus() to check
@@ -145,7 +158,9 @@ class BaseFolder(object):
if self.name == self.visiblename:
return self.name
else:
- return "%s [remote name %s]"% (self.visiblename, self.name)
+ name = uniString(u"%s [remote name %s]"%
+ (self.visiblename.uni, self.name.uni))
+ return name
def getrepository(self):
"""Returns the repository object that this folder is within."""
@@ -164,20 +179,22 @@ class BaseFolder(object):
def getfullname(self):
if self.getroot():
- return self.getroot() + self.getsep() + self.getname()
+ return uniString(
+ self.getroot() + self.getsep() + self.getname())
else:
return self.getname()
def getfolderbasename(self):
- """Return base file name of file to store Status/UID info in."""
+ """Return base file name (unicode) of file to store Status/UID info
+ in."""
- if not self.name:
- basename = '.'
+ if not self.name.uni:
+ basename = uniString(u'.')
else: # Avoid directory hierarchies and file names such as '/'.
- basename = self.name.replace('/', '.')
+ basename = uniString(self.name.replace('/', '.'))
# Replace with literal 'dot' if final path name is '.' as '.' is
# an invalid file name.
- basename = re.sub('(^|\/)\.$','\\1dot', basename)
+ basename = uniString(re.sub('(^|\/)\.$','\\1dot', basename.uni))
return basename
def check_uidvalidity(self):
@@ -199,8 +216,15 @@ class BaseFolder(object):
def _getuidfilename(self):
"""provides UIDVALIDITY cache filename for class internal purposes.
- return os.path.join(self.repository.getuiddir(),
- self.getfolderbasename())
+ It is str type encoded to filesystem."""
+
+ uiddir = self.repository.getuiddir()
+ folderbasename = self.getfolderbasename()
+ # Fix the filename to IMAP UTF-7 encoding. This prevent from playing
+ # with multiple cache files representative of the same remote folder.
+ uidfilename = fsString(os.path.join(uiddir.fs, folderbasename.imap))
+
+ return uidfilename
def get_saveduidvalidity(self):
"""Return the previously cached UIDVALIDITY value
@@ -211,12 +235,13 @@ class BaseFolder(object):
if hasattr(self, '_base_saved_uidvalidity'):
return self._base_saved_uidvalidity
uidfilename = self._getuidfilename()
- if not os.path.exists(uidfilename):
+ if not os.path.exists(uidfilename.fs):
self._base_saved_uidvalidity = None
else:
- file = open(uidfilename, "rt")
+ file = open(uidfilename.fs, "rt")
self._base_saved_uidvalidity = long(file.readline().strip())
file.close()
+
return self._base_saved_uidvalidity
def save_uidvalidity(self):
@@ -228,9 +253,9 @@ class BaseFolder(object):
newval = self.get_uidvalidity()
uidfilename = self._getuidfilename()
- with open(uidfilename + ".tmp", "wt") as file:
- file.write("%d\n"% newval)
- os.rename(uidfilename + ".tmp", uidfilename)
+ with open(uidfilename.fs + ".tmp", "wt") as fd:
+ fd.write("%d\n"% newval)
+ os.rename(uidfilename.fs + ".tmp", uidfilename.fs)
self._base_saved_uidvalidity = newval
def get_uidvalidity(self):
@@ -249,7 +274,8 @@ class BaseFolder(object):
raise NotImplementedError
def dropmessagelistcache(self):
- raise NotImplementedException
+
+ raise NotImplementedError
def getmessagelist(self):
"""Gets the current message list.
@@ -433,9 +459,14 @@ class BaseFolder(object):
for uid in uidlist:
self.deletemessagelabels(uid, labels)
+ # XXX: logic should move outside... emailutil?
def addmessageheader(self, content, linebreak, headername, headervalue):
"""Adds new header to the provided message.
+ UNICODE: This method is working on email headers. Unicode support in
+ headers is supported in a full ASCII fashion. Do not work with uni
+ objects here.
+
WARNING: This function is a bit tricky, and modifying it in the wrong way,
may easily lead to data-loss.
@@ -493,20 +524,25 @@ class BaseFolder(object):
next line\n
"""
- self.ui.debug('', 'addmessageheader: called to add %s: %s'%
+ self.ui.debug('', u'addmessageheader: called to add %s: %s'%
(headername, headervalue))
insertionpoint = content.find(linebreak * 2)
if insertionpoint == -1:
- self.ui.debug('', 'addmessageheader: headers were missing')
+ self.ui.debug('', u'addmessageheader: headers were missing')
else:
- self.ui.debug('', 'addmessageheader: headers end at position %d' % insertionpoint)
+ self.ui.debug('', u'addmessageheader: headers end at position %d'%
+ insertionpoint)
mark = '==>EOH<=='
contextstart = max(0, insertionpoint - 100)
contextend = min(len(content), insertionpoint + 100)
- self.ui.debug('', 'addmessageheader: header/body transition context (marked by %s): %s' %
- (mark, repr(content[contextstart:insertionpoint]) + \
- mark + repr(content[insertionpoint:contextend])))
+ self.ui.debug('', u'addmessageheader: header/body transition '
+ 'context (marked by %s): %s%s%s'% (
+ mark,
+ repr(content[contextstart:insertionpoint]),
+ mark,
+ repr(content[insertionpoint:contextend]))
+ )
# Hoping for case #4
prefix = linebreak
@@ -528,14 +564,14 @@ class BaseFolder(object):
if content[0:len(linebreak)] != linebreak:
suffix = suffix + linebreak
- self.ui.debug('', 'addmessageheader: insertionpoint = %d'% insertionpoint)
+ self.ui.debug('', u'addmessageheader: insertionpoint = %d'% insertionpoint)
headers = content[0:insertionpoint]
- self.ui.debug('', 'addmessageheader: headers = %s'% repr(headers))
+ self.ui.debug('', u'addmessageheader: headers = %s'% repr(headers))
new_header = prefix + ("%s: %s" % (headername, headervalue)) + suffix
- self.ui.debug('', 'addmessageheader: new_header = ' + repr(new_header))
+ self.ui.debug('', u'addmessageheader: new_header = %s'% repr(new_header))
return headers + new_header + content[insertionpoint:]
-
+ # XXX: logic should move outside... emailutil?
def __find_eoh(self, content):
""" Searches for the point where mail headers end.
Either double '\n', or end of string.
@@ -543,7 +579,8 @@ class BaseFolder(object):
Arguments:
- content: contents of the message to search in
Returns: position of the first non-header byte.
- """
+
+ UNICODE: do not work with uni objects here."""
eoh_cr = content.find('\n\n')
if eoh_cr == -1:
@@ -552,6 +589,7 @@ class BaseFolder(object):
return eoh_cr
+ # XXX: logic should move outside... emailutil?
def getmessageheader(self, content, name):
"""Searches for the first occurence of the given header and returns
its value. Header name is case-insensitive.
@@ -561,21 +599,22 @@ class BaseFolder(object):
- name: name of the header to be searched
Returns: header value or None if no such header was found
- """
- self.ui.debug('', 'getmessageheader: called to get %s'% name)
+ UNICODE: do not work with uni objects here."""
+
+ self.ui.debug('', u'getmessageheader: called to get %s'% name)
eoh = self.__find_eoh(content)
- self.ui.debug('', 'getmessageheader: eoh = %d'% eoh)
+ self.ui.debug('', u'getmessageheader: eoh = %d'% eoh)
headers = content[0:eoh]
- self.ui.debug('', 'getmessageheader: headers = %s'% repr(headers))
+ self.ui.debug('', u'getmessageheader: headers = %s'% repr(headers))
- m = re.search('^%s:(.*)$' % name, headers, flags = re.MULTILINE | re.IGNORECASE)
+ m = re.search(u'^%s:(.*)$'% name, headers, flags = re.MULTILINE | re.IGNORECASE)
if m:
return m.group(1).strip()
else:
return None
-
+ # XXX: logic should move outside... emailutil?
def getmessageheaderlist(self, content, name):
"""Searches for the given header and returns a list of values for
that header.
@@ -585,17 +624,18 @@ class BaseFolder(object):
- name: name of the header to be searched
Returns: list of header values or emptylist if no such header was found
- """
- self.ui.debug('', 'getmessageheaderlist: called to get %s' % name)
+ UNICODE: do not work with uni objects here."""
+
+ self.ui.debug('', u'getmessageheaderlist: called to get %s'% name)
eoh = self.__find_eoh(content)
- self.ui.debug('', 'getmessageheaderlist: eoh = %d' % eoh)
+ self.ui.debug('', u'getmessageheaderlist: eoh = %d'% eoh)
headers = content[0:eoh]
- self.ui.debug('', 'getmessageheaderlist: headers = %s' % repr(headers))
-
- return re.findall('^%s:(.*)$' % name, headers, flags = re.MULTILINE | re.IGNORECASE)
+ self.ui.debug('', u'getmessageheaderlist: headers = %s'% repr(headers))
+ return re.findall(u'^%s:(.*)$'% name, headers, flags = re.MULTILINE | re.IGNORECASE)
+ # XXX: logic should move outside... emailutil?
def deletemessageheaders(self, content, header_list):
"""Deletes headers in the given list from the message content.
@@ -608,15 +648,16 @@ class BaseFolder(object):
if type(header_list) != type([]):
header_list = [header_list]
- self.ui.debug('', 'deletemessageheaders: called to delete %s'% (header_list))
+ self.ui.debug('', u'deletemessageheaders: called to delete %s'%
+ (header_list))
if not len(header_list): return content
eoh = self.__find_eoh(content)
- self.ui.debug('', 'deletemessageheaders: end of headers = %d'% eoh)
+ self.ui.debug('', u'deletemessageheaders: end of headers = %d'% eoh)
headers = content[0:eoh]
rest = content[eoh:]
- self.ui.debug('', 'deletemessageheaders: headers = %s'% repr(headers))
+ self.ui.debug('', u'deletemessageheaders: headers = %s'% repr(headers))
new_headers = []
for h in headers.split('\n'):
keep_it = True
@@ -628,9 +669,6 @@ class BaseFolder(object):
return ('\n'.join(new_headers) + rest)
-
-
-
def change_message_uid(self, uid, new_uid):
"""Change the message from existing uid to new_uid
@@ -657,7 +695,7 @@ class BaseFolder(object):
for uid in uidlist:
self.deletemessage(uid)
- def copymessageto(self, uid, dstfolder, statusfolder, register = 1):
+ def copymessageto(self, uid, dstfolder, statusfolder, register=1):
"""Copies a message from self to dst if needed, updating the status
Note that this function does not check against dryrun settings,
@@ -718,8 +756,9 @@ class BaseFolder(object):
self.deletemessage(uid)
else:
raise OfflineImapError("Trying to save msg (uid %d) on folder "
- "%s returned invalid uid %d"% (uid, dstfolder.getvisiblename(),
- new_uid), OfflineImapError.ERROR.MESSAGE)
+ "%s returned invalid uid %d"%
+ (uid, dstfolder.getvisiblename().fs, new_uid),
+ OfflineImapError.ERROR.MESSAGE)
except (KeyboardInterrupt): # bubble up CTRL-C
raise
except OfflineImapError as e:
@@ -727,8 +766,8 @@ class BaseFolder(object):
raise # bubble severe errors up
self.ui.error(e, exc_info()[2])
except Exception as e:
- self.ui.error(e, exc_info()[2],
- msg = "Copying message %s [acc: %s]"% (uid, self.accountname))
+ self.ui.error(e, exc_info()[2], msg=
+ u"Copying message %s [acc: %s]"% (uid, self.accountname.uni))
raise #raise on unknown errors, so we can fix those
def __syncmessagesto_copy(self, dstfolder, statusfolder):
@@ -750,8 +789,11 @@ class BaseFolder(object):
self.getmessageuidlist())
num_to_copy = len(copylist)
if num_to_copy and self.repository.account.dryrun:
- self.ui.info("[DRYRUN] Copy {0} messages from {1}[{2}] to {3}".format(
- num_to_copy, self, self.repository, dstfolder.repository))
+ self.ui.info(
+ u"[DRYRUN] Copy {0} messages from {1}[{2}] to {3}".format(
+ num_to_copy, self.getname().uni,
+ self.repository.getname().uni,
+ dstfolder.repository.getname().uni))
return
for num, uid in enumerate(copylist):
# bail out on CTRL-C or SIGTERM
@@ -761,16 +803,17 @@ class BaseFolder(object):
# exceptions are caught in copymessageto()
if self.suggeststhreads() and not globals.options.singlethreading:
self.waitforthread()
+ fs_name = "Copy message from %s:%s"% (
+ self.repository.getname().fs, self.getname().fs)
thread = threadutil.InstanceLimitedThread(\
self.getcopyinstancelimit(),
- target = self.copymessageto,
- name = "Copy message from %s:%s" % (self.repository, self),
- args = (uid, dstfolder, statusfolder))
+ target=self.copymessageto,
+ name=fs_name,
+ args=(uid, dstfolder, statusfolder))
thread.start()
threads.append(thread)
else:
- self.copymessageto(uid, dstfolder, statusfolder,
- register = 0)
+ self.copymessageto(uid, dstfolder, statusfolder, register=0)
for thread in threads:
thread.join()
@@ -840,14 +883,14 @@ class BaseFolder(object):
for flag, uids in addflaglist.items():
self.ui.addingflags(uids, flag, dstfolder)
if self.repository.account.dryrun:
- continue #don't actually add in a dryrun
+ continue # Don't actually add in a dryrun.
dstfolder.addmessagesflags(uids, set(flag))
statusfolder.addmessagesflags(uids, set(flag))
for flag,uids in delflaglist.items():
self.ui.deletingflags(uids, flag, dstfolder)
if self.repository.account.dryrun:
- continue #don't actually remove in a dryrun
+ continue # Don't actually remove in a dryrun.
dstfolder.deletemessagesflags(uids, set(flag))
statusfolder.deletemessagesflags(uids, set(flag))
@@ -899,8 +942,9 @@ class BaseFolder(object):
raise
self.ui.error(e, exc_info()[2])
except Exception as e:
- self.ui.error(e, exc_info()[2], "Syncing folder %s [acc: %s]" %\
- (self, self.accountname))
+ self.ui.error(e, exc_info()[2],
+ u"Syncing folder %s [acc: %s]"%
+ (self.name.fs, self.accountname.fs))
raise # raise unknown Exceptions so we can fix them
def __eq__(self, other):
diff --git a/offlineimap/folder/Gmail.py b/offlineimap/folder/Gmail.py
index 1afbe47..9d33cc4 100644
--- a/offlineimap/folder/Gmail.py
+++ b/offlineimap/folder/Gmail.py
@@ -21,11 +21,14 @@ from sys import exc_info
from offlineimap import imaputil, OfflineImapError
from offlineimap import imaplibutil
+from offlineimap.utils.uni import uniString, isASCII
+from offlineimap.utils import uni
import offlineimap.accounts
from .IMAP import IMAPFolder
"""Folder implementation to support features of the Gmail IMAP server."""
+
class GmailFolder(IMAPFolder):
"""Folder implementation to support features of the Gmail IMAP server.
@@ -45,23 +48,35 @@ class GmailFolder(IMAPFolder):
def __init__(self, imapserver, name, repository):
super(GmailFolder, self).__init__(imapserver, name, repository)
self.trash_folder = repository.gettrashfolder(name)
- # Gmail will really delete messages upon EXPUNGE in these folders
- self.real_delete_folders = [ self.trash_folder, repository.getspamfolder() ]
+ # Gmail will really delete messages upon EXPUNGE in these folders.
+ self.real_delete_folders = [
+ self.trash_folder, repository.getspamfolder() ]
- # The header under which labels are stored
- self.labelsheader = self.repository.account.getconf('labelsheader', 'X-Keywords')
+ # The header under which labels are stored.
+ self.labelsheader = uniString(
+ self.repository.account.getconf('labelsheader', 'X-Keywords'))
+ # labelsheader is expected plain ASCII.
+ isASCII(self.labelsheader.uni, exception_msg=
+ "labelsheader must be plain ASCII")
# enables / disables label sync
- self.synclabels = self.repository.account.getconfboolean('synclabels', False)
+ self.synclabels = \
+ self.repository.account.getconfboolean('synclabels', False)
- # if synclabels is enabled, add a 4th pass to sync labels
+ # If synclabels is enabled, add a 4th pass to sync labels.
if self.synclabels:
self.imap_query.insert(0, 'X-GM-LABELS')
- self.syncmessagesto_passes.append(('syncing labels', self.syncmessagesto_labels))
+ self.syncmessagesto_passes.append(
+ ('syncing labels', self.syncmessagesto_labels))
- # Labels to be left alone
- ignorelabels = self.repository.account.getconf('ignorelabels', '')
- self.ignorelabels = set([l for l in re.split(r'\s*,\s*', ignorelabels) if len(l)])
+ # Labels to be left alone.
+ ignorelabels = uniString(
+ self.repository.account.getconf('ignorelabels', ''))
+ # ignorelabels is expected plain ASCII.
+ isASCII(ignorelabels, exception_msg=
+ "ignorelabels must be plain ASCII")
+ self.ignorelabels = set(
+ [l for l in re.split(r'\s*,\s*', ignorelabels.dbytes) if len(l)])
def getmessage(self, uid):
@@ -71,13 +86,16 @@ class GmailFolder(IMAPFolder):
:returns: the message body or throws and OfflineImapError
(probably severity MESSAGE) if e.g. no message with
this UID could be found.
- """
+
+ UNICODE: this expects raw data. Don't introduce uni objects."""
+
imapobj = self.imapserver.acquireconnection()
try:
data = self._fetch_from_imap(imapobj, str(uid), 2)
finally:
self.imapserver.releaseconnection(imapobj)
+ # XXX: logic on labels should go to emailutil.
# data looks now e.g.
#[('320 (X-GM-LABELS (...) UID 17061 BODY[] {2565}','msgbody....')]
# we only asked for one message, and that msg is in data[0].
@@ -88,11 +106,13 @@ class GmailFolder(IMAPFolder):
if self.synclabels:
m = re.search('X-GM-LABELS\s*\(([^\)]*)\)', data[0][0])
if m:
- labels = set([imaputil.dequote(lb) for lb in imaputil.imapsplit(m.group(1))])
+ labels = set(
+ [imaputil.dequote(lb) for lb in imaputil.imapsplit(m.group(1))])
else:
labels = set()
labels = labels - self.ignorelabels
- labels_str = imaputil.format_labels_string(self.labelsheader, sorted(labels))
+ labels_str = imaputil.format_labels_string(
+ self.labelsheader, sorted(labels))
# First remove old label headers that may be in the message content retrieved
# from gmail Then add a labels header with current gmail labels.
@@ -104,7 +124,7 @@ class GmailFolder(IMAPFolder):
else:
dbg_output = body
- self.ui.debug('imap', "Returned object from fetching %d: '%s'"%
+ self.ui.debug('imap', u"Returned object from fetching %d: '%s'"%
(uid, dbg_output))
return body
@@ -141,40 +161,44 @@ class GmailFolder(IMAPFolder):
res_type, response = imapobj.fetch("'%s'"% msgsToFetch,
'(FLAGS X-GM-LABELS UID)')
if res_type != 'OK':
- raise OfflineImapError("FETCHING UIDs in folder [%s]%s failed. " % \
- (self.getrepository(), self) + \
- "Server responded '[%s] %s'" % \
+ raise OfflineImapError("FETCHING UIDs in folder [%s]%s failed. "%
+ (self.getrepository().getname().fs, self.name.fs) +
+ "Server responded '[%s] %s'"%
(res_type, response), OfflineImapError.ERROR.FOLDER), \
None, exc_info()[2]
finally:
self.imapserver.releaseconnection(imapobj)
for messagestr in response:
- # looks like: '1 (FLAGS (\\Seen Old) X-GM-LABELS (\\Inbox \\Favorites) UID 4807)' or None if no msg
+ # looks like:
+ # '1 (FLAGS (\\Seen Old) X-GM-LABELS (\\Inbox \\Favorites) UID 4807)'
+ # or
+ # None if no msg
# Discard initial message number.
if messagestr == None:
continue
messagestr = messagestr.split(' ', 1)[1]
options = imaputil.flags2hash(messagestr)
if not 'UID' in options:
- self.ui.warn('No UID in message with options %s' %\
- str(options),
- minor = 1)
+ self.ui.warn(u'No UID in message with options %s'%
+ str(options), minor=1)
else:
uid = long(options['UID'])
self.messagelist[uid] = self.msglist_item_initializer(uid)
flags = imaputil.flagsimap2maildir(options['FLAGS'])
m = re.search('\(([^\)]*)\)', options['X-GM-LABELS'])
if m:
- labels = set([imaputil.dequote(lb) for lb in imaputil.imapsplit(m.group(1))])
+ labels = set(
+ [imaputil.dequote(lb) for lb in imaputil.imapsplit(m.group(1))])
else:
labels = set()
labels = labels - self.ignorelabels
rtime = imaplibutil.Internaldate2epoch(messagestr)
- self.messagelist[uid] = {'uid': uid, 'flags': flags, 'labels': labels, 'time': rtime}
+ self.messagelist[uid] = \
+ {'uid': uid, 'flags': flags, 'labels': labels, 'time': rtime}
def savemessage(self, uid, content, flags, rtime):
- """Save the message on the Server
+ """Save the message on the Server.
This backend always assigns a new uid, so the uid arg is ignored.
@@ -203,13 +227,15 @@ class GmailFolder(IMAPFolder):
return ret
def _messagelabels_aux(self, arg, uidlist, labels):
- """Common code to savemessagelabels and addmessagelabels"""
+ """Common code to savemessagelabels and addmessagelabels."""
+
labels = labels - self.ignorelabels
uidlist = [uid for uid in uidlist if uid > 0]
if len(uidlist) > 0:
imapobj = self.imapserver.acquireconnection()
try:
- labels_str = '(' + ' '.join([imaputil.quote(lb) for lb in labels]) + ')'
+ labels_str = '(' + ' '.join(
+ [imaputil.quote(lb) for lb in labels]) + ')'
# Coalesce uid's into ranges
uid_str = imaputil.uid_sequence(uidlist)
result = self._store_to_imap(imapobj, uid_str, arg, labels_str)
@@ -222,8 +248,10 @@ class GmailFolder(IMAPFolder):
self.imapserver.releaseconnection(imapobj)
if result:
- retlabels = imaputil.flags2hash(imaputil.imapsplit(result)[1])['X-GM-LABELS']
- retlabels = set([imaputil.dequote(lb) for lb in imaputil.imapsplit(retlabels)])
+ retlabels = imaputil.flags2hash(
+ imaputil.imapsplit(result)[1])['X-GM-LABELS']
+ retlabels = set(
+ [imaputil.dequote(lb) for lb in imaputil.imapsplit(retlabels)])
return retlabels
return None
@@ -232,6 +260,7 @@ class GmailFolder(IMAPFolder):
Note that this function does not check against dryrun settings,
so you need to ensure that it is never called in a dryrun mode."""
+
if uid in self.messagelist and 'labels' in self.messagelist[uid]:
oldlabels = self.messagelist[uid]['labels']
else:
@@ -310,6 +339,7 @@ class GmailFolder(IMAPFolder):
This function checks and protects us from action in dryrun mode.
"""
+
# This applies the labels message by message, as this makes more sense for a
# Maildir target. If applied with an other Gmail IMAP target it would not be
# the fastest thing in the world though...
@@ -353,10 +383,12 @@ class GmailFolder(IMAPFolder):
statuslabels = set()
if selflabels != statuslabels:
- self.ui.settinglabels(uid, i+1, len(uidlist), sorted(selflabels), dstfolder)
+ self.ui.settinglabels(
+ uid, i+1, len(uidlist), sorted(selflabels), dstfolder)
if self.repository.account.dryrun:
continue #don't actually add in a dryrun
- dstfolder.savemessagelabels(uid, selflabels, ignorelabels = self.ignorelabels)
+ dstfolder.savemessagelabels(
+ uid, selflabels, ignorelabels=self.ignorelabels)
mtime = dstfolder.getmessagemtime(uid)
mtimes[uid] = mtime
labels[uid] = selflabels
@@ -367,4 +399,5 @@ class GmailFolder(IMAPFolder):
statusfolder.savemessagesmtimebulk(mtimes)
except NotImplementedError:
- self.ui.warn("Can't sync labels. You need to configure a local repository of type GmailMaildir")
+ self.ui.warn(u"Can't sync labels. You need to "
+ "configure a local repository of type GmailMaildir")
diff --git a/offlineimap/folder/GmailMaildir.py b/offlineimap/folder/GmailMaildir.py
index 5ca0e1f..e21152a 100644
--- a/offlineimap/folder/GmailMaildir.py
+++ b/offlineimap/folder/GmailMaildir.py
@@ -15,22 +15,28 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
-
import os
from sys import exc_info
+
from .Maildir import MaildirFolder
from offlineimap import OfflineImapError
import offlineimap.accounts
from offlineimap import imaputil
+from offlineimap.utils.uni import fsString, uniString, isASCII
+
class GmailMaildirFolder(MaildirFolder):
- """Folder implementation to support adding labels to messages in a Maildir.
- """
+ """Folder implementation to support adding labels to messages in a Maildir."""
+
def __init__(self, root, name, sep, repository):
super(GmailMaildirFolder, self).__init__(root, name, sep, repository)
- # The header under which labels are stored
- self.labelsheader = self.repository.account.getconf('labelsheader', 'X-Keywords')
+ # The header under which labels are stored.
+ self.labelsheader = uniString(
+ self.repository.account.getconf('labelsheader', 'X-Keywords'))
+ # ...are expected plain ASCII.
+ isASCII(self.labelsheader, exception_msg=
+ "labelsheader must be plain ASCII")
# enables / disables label sync
self.synclabels = self.repository.account.getconfboolean('synclabels', 0)
@@ -40,22 +46,25 @@ class GmailMaildirFolder(MaildirFolder):
self.syncmessagesto_passes.append(('syncing labels', self.syncmessagesto_labels))
def quickchanged(self, statusfolder):
- """Returns True if the Maildir has changed. Checks uids, flags and mtimes"""
+ """Returns True if the Maildir has changed. Checks uids, flags and mtimes."""
self.cachemessagelist()
# Folder has different uids than statusfolder => TRUE
if sorted(self.getmessageuidlist()) != \
sorted(statusfolder.getmessageuidlist()):
return True
+
# check for flag changes, it's quick on a Maildir
for (uid, message) in self.getmessagelist().iteritems():
if message['flags'] != statusfolder.getmessageflags(uid):
return True
+
# check for newer mtimes. it is also fast
for (uid, message) in self.getmessagelist().iteritems():
if message['mtime'] > statusfolder.getmessagemtime(uid):
return True
- return False #Nope, nothing changed
+
+ return False # Nope, nothing changed.
# Interface from BaseFolder
@@ -68,25 +77,27 @@ class GmailMaildirFolder(MaildirFolder):
if self.messagelist is None:
self.messagelist = self._scanfolder()
- # Get mtimes
+ # Get mtimes.
if self.synclabels:
for uid, msg in self.messagelist.items():
- filepath = os.path.join(self.getfullname(), msg['filename'])
- msg['mtime'] = long(os.stat(filepath).st_mtime)
+ fs_filepath = os.path.join(
+ self.getfullname().fs, msg['filename'].fs)
+ msg['mtime'] = long(os.stat(fs_filepath).st_mtime)
def getmessagelabels(self, uid):
# Labels are not cached in cachemessagelist because it is too slow.
if not self.messagelist[uid]['labels_cached']:
filename = self.messagelist[uid]['filename']
- filepath = os.path.join(self.getfullname(), filename)
+ filepath = fsString(
+ os.path.join(self.getfullname().fs, filename.fs))
- if not os.path.exists(filepath):
+ if not os.path.exists(filepath.fs):
return set()
- file = open(filepath, 'rt')
- content = file.read()
- file.close()
+ f = open(filepath.fs, 'rt')
+ content = f.read()
+ f.close()
self.messagelist[uid]['labels'] = set()
for hstr in self.getmessageheaderlist(content, self.labelsheader):
@@ -121,9 +132,10 @@ class GmailMaildirFolder(MaildirFolder):
# Update the mtime and labels
filename = self.messagelist[uid]['filename']
- filepath = os.path.join(self.getfullname(), filename)
- self.messagelist[uid]['mtime'] = long(os.stat(filepath).st_mtime)
+ fs_filepath = os.path.join(self.getfullname().fs, filename.fs)
+ self.messagelist[uid]['mtime'] = long(os.stat(fs_filepath).st_mtime)
self.messagelist[uid]['labels'] = labels
+
return ret
def savemessagelabels(self, uid, labels, ignorelabels=set()):
@@ -133,11 +145,11 @@ class GmailMaildirFolder(MaildirFolder):
so you need to ensure that it is never called in a dryrun mode."""
filename = self.messagelist[uid]['filename']
- filepath = os.path.join(self.getfullname(), filename)
+ filepath = fsString(os.path.join(self.getfullname().fs, filename.fs))
- file = open(filepath, 'rt')
- content = file.read()
- file.close()
+ f = open(filepath.fs, 'rt')
+ content = f.read()
+ f.close()
oldlabels = set()
for hstr in self.getmessageheaderlist(content, self.labelsheader):
@@ -164,21 +176,22 @@ class GmailMaildirFolder(MaildirFolder):
# write file with new labels to a unique file in tmp
messagename = self.new_message_filename(uid, set())
tmpname = self.save_to_tmp_file(messagename, content)
- tmppath = os.path.join(self.getfullname(), tmpname)
+ tmppath = fsString(os.path.join(self.getfullname().fs, tmpname.fs))
# move to actual location
try:
- os.rename(tmppath, filepath)
+ os.rename(tmppath.fs, filepath.fs)
except OSError as e:
- raise OfflineImapError("Can't rename file '%s' to '%s': %s" % \
- (tmppath, filepath, e[1]), OfflineImapError.ERROR.FOLDER), \
+ raise OfflineImapError("Can't rename file '%s' to '%s': %s"%
+ (tmppath.fs, filepath.fs, e[1]),
+ OfflineImapError.ERROR.FOLDER), \
None, exc_info()[2]
if rtime != None:
- os.utime(filepath, (rtime, rtime))
+ os.utime(filepath.fs, (rtime, rtime))
# save the new mtime and labels
- self.messagelist[uid]['mtime'] = long(os.stat(filepath).st_mtime)
+ self.messagelist[uid]['mtime'] = long(os.stat(filepath.fs).st_mtime)
self.messagelist[uid]['labels'] = labels
def copymessageto(self, uid, dstfolder, statusfolder, register = 1):
@@ -198,7 +211,8 @@ class GmailMaildirFolder(MaildirFolder):
realcopy = uid > 0 and not dstfolder.uidexists(uid)
# first copy the message
- super(GmailMaildirFolder, self).copymessageto(uid, dstfolder, statusfolder, register)
+ super(GmailMaildirFolder, self).copymessageto(
+ uid, dstfolder, statusfolder, register)
# sync labels and mtime now when the message is new (the embedded labels are up to date,
# and have already propagated to the remote server.
@@ -207,7 +221,8 @@ class GmailMaildirFolder(MaildirFolder):
if realcopy and self.synclabels:
try:
labels = dstfolder.getmessagelabels(uid)
- statusfolder.savemessagelabels(uid, labels, mtime=self.getmessagemtime(uid))
+ statusfolder.savemessagelabels(
+ uid, labels, mtime=self.getmessagemtime(uid))
# dstfolder is not GmailMaildir.
except NotImplementedError:
@@ -226,6 +241,7 @@ class GmailMaildirFolder(MaildirFolder):
This function checks and protects us from action in ryrun mode.
"""
+
# For each label, we store a list of uids to which it should be
# added. Then, we can call addmessageslabels() to apply them in
# bulk, rather than one call per message.
@@ -256,7 +272,7 @@ class GmailMaildirFolder(MaildirFolder):
uidlist.append(uid)
- self.ui.collectingdata(uidlist, self)
+ self.ui.collectingdata(uidlist, self.getname())
# This can be slow if there is a lot of modified files
for uid in uidlist:
# bail out on CTRL-C or SIGTERM
@@ -290,7 +306,7 @@ class GmailMaildirFolder(MaildirFolder):
self.ui.addinglabels(uids, lb, dstfolder)
if self.repository.account.dryrun:
- continue #don't actually add in a dryrun
+ continue # don't actually add in a dryrun
dstfolder.addmessageslabels(uids, set([lb]))
statusfolder.addmessageslabels(uids, set([lb]))
@@ -317,11 +333,12 @@ class GmailMaildirFolder(MaildirFolder):
continue #don't actually update statusfolder
filename = self.messagelist[uid]['filename']
- filepath = os.path.join(self.getfullname(), filename)
- mtimes[uid] = long(os.stat(filepath).st_mtime)
+ fs_filepath = os.path.join(self.getfullname().fs, filename.fs)
+ mtimes[uid] = long(os.stat(fs_filepath).st_mtime)
# finally update statusfolder in a single DB transaction
statusfolder.savemessagesmtimebulk(mtimes)
except NotImplementedError:
- self.ui.warn("Can't sync labels. You need to configure a remote repository of type Gmail.")
+ self.ui.warn(u"Can't sync labels. You need to configure a remote "
+ "repository of type Gmail.")
diff --git a/offlineimap/folder/IMAP.py b/offlineimap/folder/IMAP.py
index c7e6516..1f93e11 100644
--- a/offlineimap/folder/IMAP.py
+++ b/offlineimap/folder/IMAP.py
@@ -22,8 +22,8 @@ import time
from sys import exc_info
from .Base import BaseFolder
-from offlineimap import imaputil, imaplibutil, emailutil, OfflineImapError
-from offlineimap import globals
+from offlineimap import imaputil, imaplibutil, emailutil, OfflineImapError, globals
+from offlineimap.utils.uni import noneString, uniString, imapString
from offlineimap.imaplib2 import MonthNames
@@ -42,19 +42,22 @@ class IMAPFolder(BaseFolder):
def __init__(self, imapserver, name, repository):
# FIXME: decide if unquoted name is from the responsability of the
# caller or not, but not both.
- name = imaputil.dequote(name)
- self.sep = imapserver.delim
+ # TODO: dequoting has definetly to be done as soon as possible: who
+ # wants to work with quoted strings?
+ name = imapString(imaputil.dequote(name.imap))
+ self.sep = imapserver.getdelim()
super(IMAPFolder, self).__init__(name, repository)
self.expunge = repository.getexpunge()
- self.root = None # imapserver.root
+ self.root = noneString() # imapserver.root
self.imapserver = imapserver
self.messagelist = None
self.randomgenerator = random.Random()
- #self.ui is set in BaseFolder
+ # UNICODE: this is IMAP query string in a list, don't use uni objects.
self.imap_query = ['BODY.PEEK[]']
+ # self.ui is set in BaseFolder
- fh_conf = self.repository.account.getconf('filterheaders', '')
- self.filterheaders = [h for h in re.split(r'\s*,\s*', fh_conf) if h]
+ uni_fh_conf = self.repository.account.getconf('filterheaders', '')
+ self.filterheaders = [h for h in re.split(r'\s*,\s*', uni_fh_conf) if h]
def __selectro(self, imapobj, force=False):
@@ -66,10 +69,12 @@ class IMAPFolder(BaseFolder):
.. todo: Still valid? Needs verification
:param: Enforce new SELECT even if we are on that folder already.
:returns: raises :exc:`OfflineImapError` severity FOLDER on error"""
+
try:
- imapobj.select(self.getfullname(), force = force)
+ fullname = self.getfullname()
+ imapobj.select(fullname.imap, force=force)
except imapobj.readonly:
- imapobj.select(self.getfullname(), readonly = True, force = force)
+ imapobj.select(fullname.imap, readonly=True, force=force)
# Interface from BaseFolder
def suggeststhreads(self):
@@ -81,7 +86,9 @@ class IMAPFolder(BaseFolder):
# Interface from BaseFolder
def getcopyinstancelimit(self):
- return 'MSGCOPY_' + self.repository.getname()
+ # XXX: has to be documented.
+ # I think to remember about that in init.py. Should check.
+ return uniString(u'MSGCOPY_' + self.repository.getname().uni)
# Interface from BaseFolder
def get_uidvalidity(self):
@@ -116,8 +123,8 @@ class IMAPFolder(BaseFolder):
imapobj = self.imapserver.acquireconnection()
try:
# Select folder and get number of messages
- restype, imapdata = imapobj.select(self.getfullname(), True,
- True)
+ restype, imapdata = imapobj.select(
+ self.getfullname().imap, True, True)
self.imapserver.releaseconnection(imapobj)
except OfflineImapError as e:
# retry on dropped connections, raise otherwise
@@ -156,25 +163,26 @@ class IMAPFolder(BaseFolder):
Returns: range(s) for messages or None if no messages
are to be fetched."""
- res_type, imapdata = imapobj.select(self.getfullname(), True, True)
+ res_type, imapdata = imapobj.select(
+ self.getfullname().imap, True, True)
if imapdata == [None] or imapdata[0] == '0':
# Empty folder, no need to populate message list
return None
- # By default examine all messages in this folder
+ # By default examine all messages in this folder.
msgsToFetch = '1:*'
maxage = self.config.getdefaultint(
- "Account %s"% self.accountname, "maxage", -1)
+ uniString(u"Account %s").uni % self.accountname.uni, "maxage", -1)
maxsize = self.config.getdefaultint(
- "Account %s"% self.accountname, "maxsize", -1)
+ uniString(u"Account %s").uni % self.accountname.uni, "maxsize", -1)
- # Build search condition
+ # Build search condition.
if (maxage != -1) | (maxsize != -1):
search_cond = "(";
if(maxage != -1):
- #find out what the oldest message is that we should look at
+ # Find out what the oldest message is that we should look at.
oldest_struct = time.gmtime(time.time() - (60*60*24*maxage))
if oldest_struct[0] < 1900:
raise OfflineImapError("maxage setting led to year %d. "
@@ -196,7 +204,8 @@ class IMAPFolder(BaseFolder):
if res_type != 'OK':
raise OfflineImapError("SEARCH in folder [%s]%s failed. "
"Search string was '%s'. Server responded '[%s] %s'"% (
- self.getrepository(), self, search_cond, res_type, res_data),
+ self.getrepository().fs, self.getname().fs,
+ search_cond, res_type, res_data),
OfflineImapError.ERROR.FOLDER)
# Resulting MSN are separated by space, coalesce into ranges
@@ -223,9 +232,11 @@ class IMAPFolder(BaseFolder):
# imaplib2 from quoting the sequence.
res_type, response = imapobj.fetch("'%s'"%
msgsToFetch, '(FLAGS UID)')
+ response = imapString(response)
if res_type != 'OK':
raise OfflineImapError("FETCHING UIDs in folder [%s]%s failed. "
- "Server responded '[%s] %s'"% (self.getrepository(), self,
+ "Server responded '[%s] %s'"% (
+ self.getrepository().getname().fs, self.name.fs,
res_type, response), OfflineImapError.ERROR.FOLDER)
finally:
self.imapserver.releaseconnection(imapobj)
@@ -238,9 +249,8 @@ class IMAPFolder(BaseFolder):
messagestr = messagestr.split(' ', 1)[1]
options = imaputil.flags2hash(messagestr)
if not 'UID' in options:
- self.ui.warn('No UID in message with options %s'% \
- str(options),
- minor = 1)
+ self.ui.warn(u'No UID in message with options %s'%
+ str(options), minor=1)
else:
uid = long(options['UID'])
self.messagelist[uid] = self.msglist_item_initializer(uid)
@@ -282,8 +292,8 @@ class IMAPFolder(BaseFolder):
else:
dbg_output = data
- self.ui.debug('imap', "Returned object from fetching %d: '%s'"%
- (uid, dbg_output))
+ self.ui.debug('imap', u"Returned object from fetching %d: '%s'"%
+ (uid, dbg_output))
return data
@@ -324,7 +334,8 @@ class IMAPFolder(BaseFolder):
def __savemessage_searchforheader(self, imapobj, headername, headervalue):
- self.ui.debug('imap', '__savemessage_searchforheader called for %s: %s'% \
+ self.ui.debug('imap',
+ u'__savemessage_searchforheader called for %s: %s'%
(headername, headervalue))
# Now find the UID it got.
headervalue = imapobj._quote(headervalue)
@@ -333,21 +344,27 @@ class IMAPFolder(BaseFolder):
headername, headervalue)[1][0]
except imapobj.error as err:
# IMAP server doesn't implement search or had a problem.
- self.ui.debug('imap', "__savemessage_searchforheader: got IMAP error '%s' while attempting to UID SEARCH for message with header %s"% (err, headername))
+ self.ui.debug('imap', u"__savemessage_searchforheader: "
+ "got IMAP error '%s' while attempting to UID SEARCH "
+ "for message with header %s"% (err, headername))
return 0
- self.ui.debug('imap', '__savemessage_searchforheader got initial matchinguids: ' + repr(matchinguids))
+ self.ui.debug('imap', u'__savemessage_searchforheader got '
+ 'initial matchinguids: %s'% repr(matchinguids))
if matchinguids == '':
- self.ui.debug('imap', "__savemessage_searchforheader: UID SEARCH for message with header %s yielded no results"% headername)
+ self.ui.debug('imap', u"__savemessage_searchforheader: "
+ "UID SEARCH for message with header %s yielded no results"%
+ headername)
return 0
matchinguids = matchinguids.split(' ')
- self.ui.debug('imap', '__savemessage_searchforheader: matchinguids now ' + \
- repr(matchinguids))
+ self.ui.debug('imap', u'__savemessage_searchforheader: '
+ 'matchinguids now %s'% repr(matchinguids))
if len(matchinguids) != 1 or matchinguids[0] == None:
raise ValueError("While attempting to find UID for message with "
- "header %s, got wrong-sized matchinguids of %s"%\
- (headername, str(matchinguids)))
+ "header %s, got wrong-sized matchinguids of %s"%
+ (headername, str(matchinguids)))
+
return long(matchinguids[0])
def __savemessage_fetchheaders(self, imapobj, headername, headervalue):
@@ -373,8 +390,8 @@ class IMAPFolder(BaseFolder):
Returns UID when found, 0 when not found."""
- self.ui.debug('imap', '__savemessage_fetchheaders called for %s: %s'% \
- (headername, headervalue))
+ self.ui.debug('imap', u'__savemessage_fetchheaders called for %s: %s'%
+ (headername, headervalue))
# run "fetch X:* rfc822.header"
# since we stored the mail we are looking for just recently, it would
@@ -395,17 +412,18 @@ class IMAPFolder(BaseFolder):
result = imapobj.uid('FETCH', bytearray('%d:*'% start), 'rfc822.header')
if result[0] != 'OK':
- raise OfflineImapError('Error fetching mail headers: %s'%
- '. '.join(result[1]), OfflineImapError.ERROR.MESSAGE)
+ joint_result = imapString('. '.join(result[1]))
+ raise OfflineImapError("Error fetching mail headers: %s"%
+ joint_result.fs, OfflineImapError.ERROR.MESSAGE)
+ # UNICODE: this block works on raw IMAP data.
result = result[1]
-
found = 0
for item in result:
if found == 0 and type(item) == type( () ):
# Walk just tuples
- if re.search("(?:^|\\r|\\n)%s:\s*%s(?:\\r|\\n)"% (headername, headervalue),
- item[1], flags=re.IGNORECASE):
+ if re.search("(?:^|\\r|\\n)%s:\s*%s(?:\\r|\\n)"%
+ (headername, headervalue), item[1], flags=re.IGNORECASE):
found = 1
elif found == 1:
if type(item) == type (""):
@@ -413,14 +431,18 @@ class IMAPFolder(BaseFolder):
if uid:
return int(uid.group(1))
else:
- self.ui.warn("Can't parse FETCH response, can't find UID: %s", result.__repr__())
+ self.ui.warn(
+ u"Can't parse FETCH response, can't find UID: %s",
+ result.__repr__())
else:
- self.ui.warn("Can't parse FETCH response, we awaited string: %s", result.__repr__())
+ self.ui.warn(
+ u"Can't parse FETCH response, we awaited string: %s",
+ result.__repr__())
return 0
def __getmessageinternaldate(self, content, rtime=None):
- """Parses mail and returns an INTERNALDATE string
+ """Parses mail and returns an INTERNALDATE string.
It will use information in the following order, falling back as an
attempt fails:
@@ -471,7 +493,7 @@ class IMAPFolder(BaseFolder):
# or something. Argh. It seems that Time2Internaldate
# will rause a ValueError if the year is 0102 but not 1902,
# but some IMAP servers nonetheless choke on 1902.
- self.ui.debug('imap', "Message with invalid date %s. "
+ self.ui.debug('imap', u"Message with invalid date %s. "
"Server will use local time."% datetuple)
return None
@@ -550,20 +572,22 @@ class IMAPFolder(BaseFolder):
# insert a random unique header that we can fetch later
(headername, headervalue) = self.__generate_randomheader(
content)
- self.ui.debug('imap', 'savemessage: header is: %s: %s'%
+ self.ui.debug('imap', u'savemessage: header is: %s: %s'%
(headername, headervalue))
- content = self.addmessageheader(content, CRLF, headername, headervalue)
+ content = self.addmessageheader(
+ content, CRLF, headername, headervalue)
if len(content)>200:
dbg_output = "%s...%s"% (content[:150], content[-50:])
else:
dbg_output = content
- self.ui.debug('imap', "savemessage: date: %s, content: '%s'"%
+ self.ui.debug('imap', u"savemessage: date: %s, content: '%s'"%
(date, dbg_output))
+ fullname = self.getfullname()
try:
# Select folder for append and make the box READ-WRITE
- imapobj.select(self.getfullname())
+ imapobj.select(fullname.imap)
except imapobj.readonly:
# readonly exception. Return original uid to notify that
# we did not save the message. (see savemessage in Base.py)
@@ -572,7 +596,7 @@ class IMAPFolder(BaseFolder):
#Do the APPEND
try:
- (typ, dat) = imapobj.append(self.getfullname(),
+ (typ, dat) = imapobj.append(fullname.imap,
imaputil.flagsmaildir2imap(flags), date, content)
# This should only catch 'NO' responses since append()
# will raise an exception for 'BAD' responses:
@@ -584,9 +608,11 @@ class IMAPFolder(BaseFolder):
# In this case, we should immediately abort the repository sync
# and continue with the next account.
msg = \
- "Saving msg (%s) in folder '%s', repository '%s' failed (abort). " \
+ "Saving msg (%s) in folder '%s', " \
+ "repository '%s' failed (abort). " \
"Server responded: %s %s\n"% \
- (msg_id, self, self.getrepository(), typ, dat)
+ (msg_id, self.name.fs,
+ self.getrepository().getname().fs, typ, dat)
raise OfflineImapError(msg, OfflineImapError.ERROR.REPO)
retry_left = 0 # Mark as success
except imapobj.abort as e:
@@ -596,11 +622,13 @@ class IMAPFolder(BaseFolder):
imapobj = self.imapserver.acquireconnection()
if not retry_left:
raise OfflineImapError("Saving msg (%s) in folder '%s', "
- "repository '%s' failed (abort). Server responded: %s\n"
- "Message content was: %s"%
- (msg_id, self, self.getrepository(), str(e), dbg_output),
- OfflineImapError.ERROR.MESSAGE), \
- None, exc_info()[2]
+ "repository '%s' failed (abort). Server responded: %s\n"
+ "Message content was: %s"% (msg_id,
+ self.name.fs,
+ self.getrepository().getname().fs,
+ str(e), dbg_output),
+ OfflineImapError.ERROR.MESSAGE), \
+ None, exc_info()[2]
# XXX: is this still needed?
self.ui.error(e, exc_info()[2])
except imapobj.error as e: # APPEND failed
@@ -611,12 +639,13 @@ class IMAPFolder(BaseFolder):
imapobj = None
raise OfflineImapError("Saving msg (%s) folder '%s', repo '%s'"
"failed (error). Server responded: %s\nMessage content was: "
- "%s" % (msg_id, self, self.getrepository(), str(e), dbg_output),
- OfflineImapError.ERROR.MESSAGE), None, exc_info()[2]
+ "%s"% (msg_id, self.name.fs,
+ self.getrepository().getname().fs, str(e), dbg_output),
+ OfflineImapError.ERROR.MESSAGE), None, exc_info()[2]
# Checkpoint. Let it write out stuff, etc. Eg searches for
# just uploaded messages won't work if we don't do this.
- (typ,dat) = imapobj.check()
- assert(typ == 'OK')
+ (typ, dat) = imapobj.check()
+ assert (typ == 'OK')
# get the new UID, do we use UIDPLUS?
if use_uidplus:
@@ -628,12 +657,12 @@ class IMAPFolder(BaseFolder):
# data. TODO
resp = imapobj._get_untagged_response('APPENDUID')
if resp == [None] or resp is None:
- self.ui.warn("Server supports UIDPLUS but got no APPENDUID "
+ self.ui.warn(u"Server supports UIDPLUS but got no APPENDUID "
"appending a message.")
return 0
uid = long(resp[-1].split(' ')[1])
if uid == 0:
- self.ui.warn("savemessage: Server supports UIDPLUS, but"
+ self.ui.warn(u"savemessage: Server supports UIDPLUS, but"
" we got no usable uid back. APPENDUID reponse was "
"'%s'"% str(resp))
else:
@@ -643,11 +672,11 @@ class IMAPFolder(BaseFolder):
# See docs for savemessage in Base.py for explanation
# of this and other return values
if uid == 0:
- self.ui.debug('imap', 'savemessage: attempt to get new UID '
+ self.ui.debug('imap', u'savemessage: attempt to get new UID '
'UID failed. Search headers manually.')
- uid = self.__savemessage_fetchheaders(imapobj, headername,
- headervalue)
- self.ui.warn('imap', "savemessage: Searching mails for new "
+ uid = self.__savemessage_fetchheaders(
+ imapobj, headername, headervalue)
+ self.ui.warn('imap', u"savemessage: Searching mails for new "
"Message-ID failed. Could not determine new UID.")
finally:
if imapobj: self.imapserver.releaseconnection(imapobj)
@@ -656,7 +685,7 @@ class IMAPFolder(BaseFolder):
self.messagelist[uid] = self.msglist_item_initializer(uid)
self.messagelist[uid]['flags'] = flags
- self.ui.debug('imap', 'savemessage: returning new UID %d'% uid)
+ self.ui.debug('imap', u'savemessage: returning new UID %d'% uid)
return uid
@@ -674,7 +703,7 @@ class IMAPFolder(BaseFolder):
fails_left = retry_num # retry on dropped connection
while fails_left:
try:
- imapobj.select(self.getfullname(), readonly = True)
+ imapobj.select(self.getfullname().imap, readonly=True)
res_type, data = imapobj.uid('fetch', uids, query)
fails_left = 0
except imapobj.abort as e:
@@ -690,19 +719,20 @@ class IMAPFolder(BaseFolder):
#IMAP server says bad request or UID does not exist
severity = OfflineImapError.ERROR.MESSAGE
reason = "IMAP server '%s' failed to fetch messages UID '%s'."\
- "Server responded: %s %s"% (self.getrepository(), uids,
- res_type, data)
+ "Server responded: %s %s"% (
+ self.getrepository().getname().fs, uids, res_type, data)
if data == [None]:
#IMAP server did not find a message with this UID
reason = "IMAP server '%s' does not have a message "\
- "with UID '%s'" % (self.getrepository(), uids)
+ "with UID '%s'"% (
+ self.getrepository().getname().fs, uids)
raise OfflineImapError(reason, severity)
return data
def _store_to_imap(self, imapobj, uid, field, data):
- """Stores data to IMAP server
+ """Stores data to IMAP server.
Arguments:
- imapobj: instance of IMAPlib to use
@@ -710,14 +740,16 @@ class IMAPFolder(BaseFolder):
- field: field name to be stored/updated
- data: field contents
"""
- imapobj.select(self.getfullname())
+
+ imapobj.select(self.getfullname().imap)
res_type, retdata = imapobj.uid('store', uid, field, data)
if res_type != 'OK':
severity = OfflineImapError.ERROR.MESSAGE
reason = "IMAP server '%s' failed to store %s for message UID '%d'."\
- "Server responded: %s %s"% (
- self.getrepository(), field, uid, res_type, retdata)
+ "Server responded: %s %s"% (self.getrepository().getname().fs,
+ field, uid, res_type, retdata)
raise OfflineImapError(reason, severity)
+
return retdata[0]
# Interface from BaseFolder
@@ -771,13 +803,13 @@ class IMAPFolder(BaseFolder):
imapobj = self.imapserver.acquireconnection()
try:
try:
- imapobj.select(self.getfullname())
+ imapobj.select(self.getfullname().imap)
except imapobj.readonly:
self.ui.flagstoreadonly(self, uidlist, flags)
return
r = imapobj.uid('store',
imaputil.uid_sequence(uidlist), operation + 'FLAGS',
- imaputil.flagsmaildir2imap(flags))
+ imaputil.flagsmaildir2imap(flags))
assert r[0] == 'OK', 'Error with store: ' + '. '.join(r[1])
r = r[1]
finally:
@@ -845,7 +877,7 @@ class IMAPFolder(BaseFolder):
imapobj = self.imapserver.acquireconnection()
try:
try:
- imapobj.select(self.getfullname())
+ imapobj.select(self.getfullname().imap)
except imapobj.readonly:
self.ui.deletereadonly(self, uidlist)
return
diff --git a/offlineimap/folder/LocalStatus.py b/offlineimap/folder/LocalStatus.py
index c753f63..e6d47ac 100644
--- a/offlineimap/folder/LocalStatus.py
+++ b/offlineimap/folder/LocalStatus.py
@@ -19,6 +19,7 @@ from sys import exc_info
import os
import threading
+from offlineimap.utils.uni import uniString, fsString, isASCII
from .Base import BaseFolder
@@ -29,10 +30,14 @@ class LocalStatusFolder(BaseFolder):
magicline = "OFFLINEIMAP LocalStatus CACHE DATA - DO NOT MODIFY - FORMAT %d"
def __init__(self, name, repository):
- self.sep = '.' #needs to be set before super.__init__()
+ self.sep = uniString(u'.') # needs to be set before super.__init__()
super(LocalStatusFolder, self).__init__(name, repository)
- self.root = repository.root
- self.filename = os.path.join(self.getroot(), self.getfolderbasename())
+ self.root = repository.getlocalroot() # XXX: what root looks like?
+ # Fix the filename to the IMAP UTF-7 encoding version. This prevents
+ # from playing with multiple cache files representative of the same
+ # remote folder.
+ self.filename = fsString(os.path.join(
+ self.root.fs, self.getfolderbasename().imap))
self.messagelist = {}
self.savelock = threading.Lock()
# Should we perform fsyncs as often as possible?
@@ -44,15 +49,16 @@ class LocalStatusFolder(BaseFolder):
return 0
def isnewfolder(self):
- return not os.path.exists(self.filename)
+ return not os.path.exists(self.filename.fs)
# Interface from BaseFolder
def getfullname(self):
return self.filename
def deletemessagelist(self):
+
if not self.isnewfolder():
- os.unlink(self.filename)
+ os.unlink(self.filename.fs)
# Interface from BaseFolder
def msglist_item_initializer(self, uid):
@@ -72,10 +78,16 @@ class LocalStatusFolder(BaseFolder):
uid = long(uid)
flags = set(flags)
except ValueError as e:
- errstr = "Corrupt line '%s' in cache file '%s'" % \
- (line, self.filename)
+ # Mixing encoding:
+ # - line: should be ASCII but unexpected bug could make it
+ # string of bytes with unkown encoding.
+ # - self.filename: filesystem encoded.
+ # Converting from unkown encoding is worse than keeping things
+ # as-is.
+ errstr = uniString(u"Corrupt line '%s' in cache file '%s'"%
+ (line, self.filename.uni))
self.ui.warn(errstr)
- raise ValueError(errstr), None, exc_info()[2]
+ raise ValueError(errstr.fs), None, exc_info()[2]
self.messagelist[uid] = self.msglist_item_initializer(uid)
self.messagelist[uid]['flags'] = flags
@@ -93,12 +105,19 @@ class LocalStatusFolder(BaseFolder):
uid = long(uid)
flags = set(flags)
mtime = long(mtime)
- labels = set([lb.strip() for lb in labels.split(',') if len(lb.strip()) > 0])
+ labels = set([
+ lb.strip() for lb in labels.split(',') if len(lb.strip()) > 0])
except ValueError as e:
- errstr = "Corrupt line '%s' in cache file '%s'"% \
- (line, self.filename)
+ # Mixing encoding:
+ # - line: should be ASCII but unexpected bug could make it
+ # string of bytes with unkown encoding.
+ # - self.filename: filesystem encoded.
+ # Converting from unkown encoding is worse than keeping things
+ # as-is.
+ errstr = uniString(u"Corrupt line '%s' in cache file '%s'"%
+ (line, self.filename.uni))
self.ui.warn(errstr)
- raise ValueError(errstr), None, exc_info()[2]
+ raise ValueError(errstr.fs), None, exc_info()[2]
self.messagelist[uid] = self.msglist_item_initializer(uid)
self.messagelist[uid]['flags'] = flags
self.messagelist[uid]['mtime'] = mtime
@@ -123,8 +142,9 @@ class LocalStatusFolder(BaseFolder):
# Convert from format v1.
elif line == (self.magicline % 1):
- self.ui._msg('Upgrading LocalStatus cache from version 1'
- 'to version 2 for %s:%s'% (self.repository, self))
+ self.ui._msg(u'Upgrading LocalStatus cache from version 1'
+ 'to version 2 for %s:%s'%
+ (self.repository.getname().uni, self.name.uni))
self.readstatus_v1(cachefd)
cachefd.close()
self.save()
@@ -139,14 +159,16 @@ class LocalStatusFolder(BaseFolder):
# Something is wrong.
else:
- errstr = "Unrecognized cache magicline in '%s'" % self.filename
+ # Keep string filesystem encoded.
+ errstr = uniString(
+ u"Unrecognized cache magicline in '%s'"% self.filename.uni)
self.ui.warn(errstr)
- raise ValueError(errstr)
+ raise ValueError(errstr.fs)
if not line:
# The status file is empty - should not have happened,
# but somehow did.
- errstr = "Cache file '%s' is empty."% self.filename
+ errstr = uniString(u"Cache file '%s' is empty."% self.filename.uni)
self.ui.warn(errstr)
cachefd.close()
return
@@ -172,15 +194,23 @@ class LocalStatusFolder(BaseFolder):
for msg in self.messagelist.values():
flags = ''.join(sorted(msg['flags']))
labels = ', '.join(sorted(msg['labels']))
- cachefd.write("%s|%s|%d|%s\n" % (msg['uid'], flags, msg['mtime'], labels))
+ data = u"%s|%s|%d|%s\n"% (msg['uid'], flags, msg['mtime'], labels)
+ # Ensure all data is full ASCII.
+ isASCII(data, exception_msg=
+ "unsupported character was about to go to the cache file %s"%
+ self.filename.fs)
+ cachefd.write(data)
cachefd.flush()
if self.doautosave:
os.fsync(cachefd.fileno())
cachefd.close()
- os.rename(self.filename + ".tmp", self.filename)
+ os.rename(self.filename.fs + ".tmp", self.filename.fs)
if self.doautosave:
- fd = os.open(os.path.dirname(self.filename), os.O_RDONLY)
+ # fsync on directory ensure that a newly created file will exist
+ # in the directory. The filesystem must support this feature.
+ p = os.path.dirname(self.filename.fs)
+ fd = os.open(p, os.O_RDONLY)
os.fsync(fd)
os.close(fd)
diff --git a/offlineimap/folder/LocalStatusSQLite.py b/offlineimap/folder/LocalStatusSQLite.py
index 8a3b9df..f0800f8 100644
--- a/offlineimap/folder/LocalStatusSQLite.py
+++ b/offlineimap/folder/LocalStatusSQLite.py
@@ -20,68 +20,77 @@ from threading import Lock
try:
import sqlite3 as sqlite
except:
- pass #fail only if needed later on, not on import
+ pass # Fail only if needed later on, not on import.
from .Base import BaseFolder
+from offlineimap.utils.uni import uniString, fsString
class LocalStatusSQLiteFolder(BaseFolder):
- """LocalStatus backend implemented with an SQLite database
+ """LocalStatus backend implemented with an SQLite database.
As python-sqlite currently does not allow to access the same sqlite
objects from various threads, we need to open get and close a db
connection and cursor for all operations. This is a big disadvantage
and we might want to investigate if we cannot hold an object open
for a thread somehow."""
- #though. According to sqlite docs, you need to commit() before
- #the connection is closed or your changes will be lost!"""
- #get db connection which autocommits
- #connection = sqlite.connect(self.filename, isolation_level=None)
- #cursor = connection.cursor()
- #return connection, cursor
-
- #current version of our db format
+
+ # Though. According to sqlite docs, you need to commit() before
+ # the connection is closed or your changes will be lost!
+ # Get db connection which autocommits
+ # connection = sqlite.connect(self.filename.fs, isolation_level=None)
+ # cursor = connection.cursor()
+ # return connection, cursor
+
+ # Current version of our db format.
cur_version = 2
def __init__(self, name, repository):
- self.sep = '.' #needs to be set before super.__init__()
+ self.sep = uniString(u'.') # needs to be set before super.__init__()
super(LocalStatusSQLiteFolder, self).__init__(name, repository)
- self.root = repository.root
- self.filename = os.path.join(self.getroot(), self.getfolderbasename())
+ self.root = repository.getlocalroot()
+ folderbasename = self.getfolderbasename()
+ # Fix the filename to the IMAP UTF-7 encoding version. This prevents
+ # from playing with multiple cache files representative of the same
+ # remote folder.
+ self.filename = fsString(os.path.join(
+ self.getroot().fs, self.getfolderbasename().imap))
self.messagelist = {}
-
self._newfolder = False # flag if the folder is new
- dirname = os.path.dirname(self.filename)
- if not os.path.exists(dirname):
- os.makedirs(dirname)
- if not os.path.isdir(dirname):
+ dirname = fsString(os.path.dirname(self.filename.fs))
+ if not os.path.exists(dirname.fs):
+ os.makedirs(dirname.fs)
+ if not os.path.isdir(dirname.fs):
+ # dirname is expected filesystem encoded.
raise UserWarning("SQLite database path '%s' is not a directory."%
- dirname)
+ dirname.fs)
# dblock protects against concurrent writes in same connection
self._dblock = Lock()
- #Try to establish connection, no need for threadsafety in __init__
+ # Try to establish connection, no need for threadsafety in __init__.
try:
- self.connection = sqlite.connect(self.filename, check_same_thread=False)
+ self.connection = sqlite.connect(
+ self.filename.fs, check_same_thread=False)
except NameError:
# sqlite import had failed
raise UserWarning('SQLite backend chosen, but no sqlite python '
'bindings available. Please install.'), None, exc_info()[2]
- #Make sure sqlite is in multithreading SERIALIZE mode
+ # Make sure sqlite is in multithreading SERIALIZE mode.
assert sqlite.threadsafety == 1, 'Your sqlite is not multithreading safe.'
- #Test if db version is current enough and if db is readable.
+ # Test if db version is current enough and if db is readable.
try:
cursor = self.connection.execute(
"SELECT value from metadata WHERE key='db_version'")
except sqlite.DatabaseError:
- #db file missing or corrupt, recreate it.
+ # db file missing or corrupt, recreate it.
+ # XXX: warn the user about that.
self.__create_db()
else:
- # fetch db version and upgrade if needed
+ # fetch db version and upgrade if needed.
version = int(cursor.fetchone()[0])
if version < LocalStatusSQLiteFolder.cur_version:
self.__upgrade_db(version)
@@ -144,27 +153,31 @@ class LocalStatusSQLiteFolder(BaseFolder):
return cursor
def __upgrade_db(self, from_ver):
- """Upgrade the sqlite format from version 'from_ver' to current"""
+ """Upgrade the sqlite format from version 'from_ver' to current."""
if hasattr(self, 'connection'):
- self.connection.close() #close old connections first
+ self.connection.close() # Close old connections first.
self.connection = sqlite.connect(self.filename,
- check_same_thread = False)
+ check_same_thread=False)
# Upgrade from database version 1 to version 2
- # This change adds labels and mtime columns, to be used by Gmail IMAP and Maildir folders.
+ # This change adds labels and mtime columns, to be used by Gmail IMAP
+ # and Maildir folders.
+
if from_ver <= 1:
- self.ui._msg('Upgrading LocalStatus cache from version 1 to version 2 for %s:%s'%
- (self.repository, self))
- self.connection.executescript("""ALTER TABLE status ADD mtime INTEGER DEFAULT 0;
- ALTER TABLE status ADD labels VARCHAR(256) DEFAULT '';
- UPDATE metadata SET value='2' WHERE key='db_version';
- """)
+ self.ui._msg(u'Upgrading LocalStatus cache from version 1 '
+ 'to version 2 for %s:%s'%
+ (self.repository.getname().uni, self.name.uni))
+ self.connection.executescript("""
+ALTER TABLE status ADD mtime INTEGER DEFAULT 0;
+ALTER TABLE status ADD labels VARCHAR(256) DEFAULT '';
+UPDATE metadata SET value='2' WHERE key='db_version';
+ """)
self.connection.commit()
# Future version upgrades come here...
- # if from_ver <= 2: ... #upgrade from 2 to 3
- # if from_ver <= 3: ... #upgrade from 3 to 4
+ # if from_ver <= 2: ... # upgrade from 2 to 3
+ # if from_ver <= 3: ... # upgrade from 3 to 4
def __create_db(self):
@@ -172,12 +185,13 @@ class LocalStatusSQLiteFolder(BaseFolder):
self.connection must point to the opened and valid SQlite
database connection."""
- self.ui._msg('Creating new Local Status db for %s:%s' \
- % (self.repository, self))
+
+ self.ui._msg(u"Creating new Local Status db for %s:%s to '%s'"%
+ (self.repository.getname().uni, self.name.uni, self.filename.uni))
self.connection.executescript("""
- CREATE TABLE metadata (key VARCHAR(50) PRIMARY KEY, value VARCHAR(128));
- INSERT INTO metadata VALUES('db_version', '2');
- CREATE TABLE status (id INTEGER PRIMARY KEY, flags VARCHAR(50), mtime INTEGER, labels VARCHAR(256));
+CREATE TABLE metadata (key VARCHAR(50) PRIMARY KEY, value VARCHAR(128));
+INSERT INTO metadata VALUES('db_version', '2');
+CREATE TABLE status (id INTEGER PRIMARY KEY, flags VARCHAR(50), mtime INTEGER, labels VARCHAR(256));
""")
self.connection.commit()
self._newfolder = True
@@ -223,7 +237,6 @@ class LocalStatusSQLiteFolder(BaseFolder):
'(id,flags,mtime,labels) VALUES (?,?,?,?)',
data, executemany=True)
-
# Following some pure SQLite functions, where we chose to use
# BaseFolder() methods instead. Doing those on the in-memory list is
# quicker anyway. If our db becomes so big that we don't want to
@@ -235,8 +248,10 @@ class LocalStatusSQLiteFolder(BaseFolder):
# with conn:
# cursor.execute('SELECT id FROM status WHERE id=:id',{'id': uid})
# return cursor.fetchone()
+ #
# This would be the pure SQLite solution, use BaseFolder() method,
# to avoid threading with sqlite...
+ #
#def getmessageuidlist(self):
# conn, cursor = self.get_cursor()
# with conn:
@@ -245,16 +260,18 @@ class LocalStatusSQLiteFolder(BaseFolder):
# for row in cursor:
# r.append(row[0])
# return r
+ #
#def getmessagecount(self):
# conn, cursor = self.get_cursor()
# with conn:
# cursor.execute('SELECT count(id) from status');
# return cursor.fetchone()[0]
+ #
#def getmessageflags(self, uid):
# conn, cursor = self.get_cursor()
# with conn:
# cursor.execute('SELECT flags FROM status WHERE id=:id',
- # {'id': uid})
+ # {'id': uid})
# for row in cursor:
# flags = [x for x in row[0]]
# return flags
@@ -286,8 +303,10 @@ class LocalStatusSQLiteFolder(BaseFolder):
self.messagelist[uid] = {'uid': uid, 'flags': flags, 'time': rtime, 'mtime': mtime, 'labels': labels}
flags = ''.join(sorted(flags))
labels = ', '.join(sorted(labels))
- self.__sql_write('INSERT INTO status (id,flags,mtime,labels) VALUES (?,?,?,?)',
- (uid,flags,mtime,labels))
+ self.__sql_write(
+ 'INSERT INTO status (id,flags,mtime,labels) VALUES (?,?,?,?)',
+ (uid,flags,mtime,labels))
+
return uid
@@ -296,7 +315,7 @@ class LocalStatusSQLiteFolder(BaseFolder):
assert self.uidexists(uid)
self.messagelist[uid]['flags'] = flags
flags = ''.join(sorted(flags))
- self.__sql_write('UPDATE status SET flags=? WHERE id=?',(flags,uid))
+ self.__sql_write('UPDATE status SET flags=? WHERE id=?', (flags,uid))
def getmessageflags(self, uid):
@@ -309,18 +328,20 @@ class LocalStatusSQLiteFolder(BaseFolder):
labels = ', '.join(sorted(labels))
if mtime:
- self.__sql_write('UPDATE status SET labels=?, mtime=? WHERE id=?',(labels,mtime,uid))
+ self.__sql_write(
+ 'UPDATE status SET labels=?, mtime=? WHERE id=?',
+ (labels,mtime,uid))
else:
- self.__sql_write('UPDATE status SET labels=? WHERE id=?',(labels,uid))
+ self.__sql_write('UPDATE status SET labels=? WHERE id=?',
+ (labels,uid))
def savemessageslabelsbulk(self, labels):
- """
- Saves labels from a dictionary in a single database operation.
-
- """
+ """Saves labels from a dictionary in a single database operation."""
+
data = [(', '.join(sorted(l)), uid) for uid, l in labels.items()]
- self.__sql_write('UPDATE status SET labels=? WHERE id=?', data, executemany=True)
+ self.__sql_write(
+ 'UPDATE status SET labels=? WHERE id=?', data, executemany=True)
for uid, l in labels.items():
self.messagelist[uid]['labels'] = l
@@ -330,7 +351,8 @@ class LocalStatusSQLiteFolder(BaseFolder):
for uid in uids:
newlabels = self.messagelist[uid]['labels'] | labels
data.append((', '.join(sorted(newlabels)), uid))
- self.__sql_write('UPDATE status SET labels=? WHERE id=?', data, executemany=True)
+ self.__sql_write(
+ 'UPDATE status SET labels=? WHERE id=?', data, executemany=True)
for uid in uids:
self.messagelist[uid]['labels'] = self.messagelist[uid]['labels'] | labels
@@ -340,7 +362,8 @@ class LocalStatusSQLiteFolder(BaseFolder):
for uid in uids:
newlabels = self.messagelist[uid]['labels'] - labels
data.append((', '.join(sorted(newlabels)), uid))
- self.__sql_write('UPDATE status SET labels=? WHERE id=?', data, executemany=True)
+ self.__sql_write(
+ 'UPDATE status SET labels=? WHERE id=?', data, executemany=True)
for uid in uids:
self.messagelist[uid]['labels'] = self.messagelist[uid]['labels'] - labels
@@ -353,7 +376,8 @@ class LocalStatusSQLiteFolder(BaseFolder):
"""Saves mtimes from the mtimes dictionary in a single database operation."""
data = [(mt, uid) for uid, mt in mtimes.items()]
- self.__sql_write('UPDATE status SET mtime=? WHERE id=?', data, executemany=True)
+ self.__sql_write(
+ 'UPDATE status SET mtime=? WHERE id=?', data, executemany=True)
for uid, mt in mtimes.items():
self.messagelist[uid]['mtime'] = mt
@@ -382,6 +406,7 @@ class LocalStatusSQLiteFolder(BaseFolder):
if not len(uidlist):
return
# arg2 needs to be an iterable of 1-tuples [(1,),(2,),...]
- self.__sql_write('DELETE FROM status WHERE id=?', zip(uidlist, ), True)
+ self.__sql_write(
+ 'DELETE FROM status WHERE id=?', zip(uidlist, ), True)
for uid in uidlist:
del(self.messagelist[uid])
diff --git a/offlineimap/folder/Maildir.py b/offlineimap/folder/Maildir.py
index b35bfc2..136209c 100644
--- a/offlineimap/folder/Maildir.py
+++ b/offlineimap/folder/Maildir.py
@@ -32,6 +32,8 @@ except NameError:
from sets import Set as set
from offlineimap import OfflineImapError
+from offlineimap.utils.uni import uniString, fsString
+
# Find the UID in a message filename
re_uidmatch = re.compile(',U=(\d+)')
@@ -57,8 +59,22 @@ def _gettimeseq():
finally:
timelock.release()
+#
+# XXX: some methods do a lot of os.path.join()/os.path.split(). Check if it can
+# be factorized in methods with names explaining WHAT are the returned values.
+# I mean, not a generic "splitted_blah()" thing but rather something like
+# "dirname_tmp_filename()".
+#
+
class MaildirFolder(BaseFolder):
def __init__(self, root, name, sep, repository):
+ """:params:
+ - root: path
+ - name: folder name
+ - sep: sep character.
+ - repository: repository instance.
+ """
+
self.sep = sep # needs to be set before super().__init__
super(MaildirFolder, self).__init__(name, repository)
self.dofsync = self.config.getdefaultboolean("general", "fsync", True)
@@ -66,21 +82,24 @@ class MaildirFolder(BaseFolder):
self.messagelist = None
# check if we should use a different infosep to support Win file systems
self.wincompatible = self.config.getdefaultboolean(
- "Account "+self.accountname, "maildir-windows-compatible", False)
- self.infosep = '!' if self.wincompatible else ':'
+ "Account " + self.accountname.dbytes,
+ "maildir-windows-compatible", False)
+ self.infosep = uniString(u'!') if self.wincompatible else uniString(u':')
"""infosep is the separator between maildir name and flag appendix"""
- self.re_flagmatch = re.compile('%s2,(\w*)'% self.infosep)
+ self.re_flagmatch = re.compile('%s2,(\w*)'% self.infosep.dbytes)
#self.ui is set in BaseFolder.init()
# Everything up to the first comma or colon (or ! if Windows):
- self.re_prefixmatch = re.compile('([^'+ self.infosep + ',]*)')
- #folder's md, so we can match with recorded file md5 for validity
- self._foldermd5 = md5(self.getvisiblename()).hexdigest()
+ self.re_prefixmatch = re.compile('([^'+ self.infosep.dbytes + ',]*)')
+ # folder's md, so we can match with recorded file md5 for validity
+ self._foldermd5 = md5(self.getvisiblename().imap).hexdigest()
# Cache the full folder path, as we use getfullname() very often
- self._fullname = os.path.join(self.getroot(), self.getname())
+ self._fullname = fsString(os.path.join(
+ self.root.fs, self.name.fs))
# Interface from BaseFolder
def getfullname(self):
"""Return the absolute file path to the Maildir folder (sans cur|new)"""
+
return self._fullname
# Interface from BaseFolder
@@ -89,14 +108,15 @@ class MaildirFolder(BaseFolder):
Maildirs have no notion of uidvalidity, so we just return a magic
token."""
+
return 42
- #Checks to see if the given message is within the maximum age according
- #to the maildir name which should begin with a timestamp
+ # Checks to see if the given message is within the maximum age according
+ # to the maildir name which should begin with a timestamp.
def _iswithinmaxage(self, messagename, maxage):
- #In order to have the same behaviour as SINCE in an IMAP search
- #we must convert this to the oldest time and then strip off hrs/mins
- #from that day
+ # In order to have the same behaviour as SINCE in an IMAP search
+ # we must convert this to the oldest time and then strip off hrs/mins
+ # from that day
oldest_time_utc = time.time() - (60*60*24*maxage)
oldest_time_struct = time.gmtime(oldest_time_utc)
oldest_time_today_seconds = ((oldest_time_struct[3] * 3600) \
@@ -132,18 +152,18 @@ class MaildirFolder(BaseFolder):
"""
prefix, uid, fmd5, flags = None, None, None, set()
- prefixmatch = self.re_prefixmatch.match(filename)
+ prefixmatch = self.re_prefixmatch.match(filename.fs)
if prefixmatch:
prefix = prefixmatch.group(1)
folderstr = ',FMD5=%s'% self._foldermd5
- foldermatch = folderstr in filename
+ foldermatch = folderstr in filename.fs
# If there was no folder MD5 specified, or if it mismatches,
# assume it is a foreign (new) message and ret: uid, fmd5 = None, None
if foldermatch:
- uidmatch = re_uidmatch.search(filename)
+ uidmatch = re_uidmatch.search(filename.fs)
if uidmatch:
uid = long(uidmatch.group(1))
- flagmatch = self.re_flagmatch.search(filename)
+ flagmatch = self.re_flagmatch.search(filename.fs)
if flagmatch:
# Filter out all lowercase (custom maildir) flags. We don't
# handle them yet.
@@ -158,44 +178,45 @@ class MaildirFolder(BaseFolder):
:returns: dict that can be used as self.messagelist.
"""
- maxage = self.config.getdefaultint("Account " + self.accountname,
- "maxage", None)
- maxsize = self.config.getdefaultint("Account " + self.accountname,
- "maxsize", None)
+ maxage = self.config.getdefaultint(
+ "Account " + self.accountname.dbytes, "maxage", None)
+ maxsize = self.config.getdefaultint(
+ "Account " + self.accountname.dbytes, "maxsize", None)
retval = {}
files = []
nouidcounter = -1 # Messages without UIDs get negative UIDs.
for dirannex in ['new', 'cur']:
- fulldirname = os.path.join(self.getfullname(), dirannex)
- files.extend((dirannex, filename) for
- filename in os.listdir(fulldirname))
+ fs_fulldirname = os.path.join(self.getfullname().fs, dirannex)
+ files.extend((dirannex, fs_filename) for
+ fs_filename in os.listdir(fs_fulldirname))
- for dirannex, filename in files:
- # We store just dirannex and filename, ie 'cur/123...'
- filepath = os.path.join(dirannex, filename)
+ for dirannex, fs_filename in files:
+ # We store just dirannex and fs_filename, ie 'cur/123...'
+ fs_filepath = os.path.join(dirannex, fs_filename)
# check maxage/maxsize if this message should be considered
- if maxage and not self._iswithinmaxage(filename, maxage):
+ if maxage and not self._iswithinmaxage(fs_filename, maxage):
continue
- if maxsize and (os.path.getsize(os.path.join(
- self.getfullname(), filepath)) > maxsize):
+ if maxsize and (os.path.getsize(
+ os.path.join(self.getfullname().fs, fs_filepath)) > maxsize):
continue
- (prefix, uid, fmd5, flags) = self._parse_filename(filename)
+ (prefix, uid, fmd5, flags) = self._parse_filename(
+ fsString(fs_filename))
if uid is None: # assign negative uid to upload it.
uid = nouidcounter
nouidcounter -= 1
else: # It comes from our folder.
- uidmatch = re_uidmatch.search(filename)
+ uidmatch = re_uidmatch.search(fs_filename)
uid = None
if not uidmatch:
uid = nouidcounter
nouidcounter -= 1
else:
uid = long(uidmatch.group(1))
- # 'filename' is 'dirannex/filename', e.g. cur/123,U=1,FMD5=1:2,S
+ # 'fs_filename' is 'dirannex/fs_filename', e.g. cur/123,U=1,FMD5=1:2,S
retval[uid] = self.msglist_item_initializer(uid)
retval[uid]['flags'] = flags
- retval[uid]['filename'] = filepath
+ retval[uid]['filename'] = fsString(fs_filepath)
return retval
# Interface from BaseFolder
@@ -235,10 +256,10 @@ class MaildirFolder(BaseFolder):
"""Return the content of the message."""
filename = self.messagelist[uid]['filename']
- filepath = os.path.join(self.getfullname(), filename)
- file = open(filepath, 'rt')
- retval = file.read()
- file.close()
+ fs_filepath = os.path.join(self.getfullname().fs, filename.fs)
+ f = open(fs_filepath, 'rt')
+ retval = f.read()
+ f.close()
#TODO: WHY are we replacing \r\n with \n here? And why do we
# read it as text?
return retval.replace("\r\n", "\n")
@@ -246,8 +267,8 @@ class MaildirFolder(BaseFolder):
# Interface from BaseFolder
def getmessagetime(self, uid):
filename = self.messagelist[uid]['filename']
- filepath = os.path.join(self.getfullname(), filename)
- return os.path.getmtime(filepath)
+ fs_filepath = os.path.join(self.getfullname().fs, filename.fs)
+ return os.path.getmtime(fs_filepath)
def new_message_filename(self, uid, flags=set()):
"""Creates a new unique Maildir filename
@@ -273,14 +294,14 @@ class MaildirFolder(BaseFolder):
Returns: relative path to the temporary file
that was created."""
- tmpname = os.path.join('tmp', filename)
+ tmpname = fsString(os.path.join('tmp', filename.fs))
# open file and write it out
tries = 7
while tries:
tries = tries - 1
try:
- fd = os.open(os.path.join(self.getfullname(), tmpname),
- os.O_EXCL|os.O_CREAT|os.O_WRONLY, 0o666)
+ fs_path = os.path.join(self.getfullname().fs, tmpname.fs)
+ fd = os.open(fs_path, os.O_EXCL|os.O_CREAT|os.O_WRONLY, 0o666)
break
except OSError as e:
if e.errno == e.EEXIST:
@@ -288,8 +309,9 @@ class MaildirFolder(BaseFolder):
time.sleep(0.23)
continue
severity = OfflineImapError.ERROR.MESSAGE
- raise OfflineImapError("Unique filename %s already exists." % \
- filename, severity), None, exc_info()[2]
+ # filename is expected filesystem encoded.
+ raise OfflineImapError("Unique filename %s already exists."%
+ filename.fs, severity), None, exc_info()[2]
else:
raise
@@ -303,7 +325,6 @@ class MaildirFolder(BaseFolder):
return tmpname
-
# Interface from BaseFolder
def savemessage(self, uid, content, flags, rtime):
"""Writes a new message, with the specified uid.
@@ -311,9 +332,10 @@ class MaildirFolder(BaseFolder):
See folder/Base for detail. Note that savemessage() does not
check against dryrun settings, so you need to ensure that
savemessage is never called in a dryrun mode."""
+
# This function only ever saves to tmp/,
# but it calls savemessageflags() to actually save to cur/ or new/.
- self.ui.savemessage('maildir', uid, flags, self)
+ self.ui.savemessage('maildir', uid, flags, self.name)
if uid < 0:
# We cannot assign a new uid.
return uid
@@ -325,18 +347,20 @@ class MaildirFolder(BaseFolder):
# Otherwise, save the message in tmp/ and then call savemessageflags()
# to give it a permanent home.
- tmpdir = os.path.join(self.getfullname(), 'tmp')
+ fs_tmpdir = os.path.join(self.getfullname().fs, 'tmp') # FIXME: never used!
messagename = self.new_message_filename(uid, flags)
tmpname = self.save_to_tmp_file(messagename, content)
if rtime != None:
- os.utime(os.path.join(self.getfullname(), tmpname), (rtime, rtime))
+ os.utime(os.path.join(self.getfullname().fs, tmpname.fs),
+ (rtime, rtime))
self.messagelist[uid] = self.msglist_item_initializer(uid)
self.messagelist[uid]['flags'] = flags
self.messagelist[uid]['filename'] = tmpname
# savemessageflags moves msg to 'cur' or 'new' as appropriate
self.savemessageflags(uid, flags)
- self.ui.debug('maildir', 'savemessage: returning uid %d' % uid)
+ self.ui.debug('maildir', u'savemessage: returning uid %d'% uid)
+
return uid
# Interface from BaseFolder
@@ -357,30 +381,32 @@ class MaildirFolder(BaseFolder):
assert uid in self.messagelist
oldfilename = self.messagelist[uid]['filename']
- dir_prefix, filename = os.path.split(oldfilename)
+ # UNICODE: Keep those as Python types.
+ fs_dir_prefix, fs_filename = os.path.split(oldfilename.fs)
# If a message has been seen, it goes into 'cur'
- dir_prefix = 'cur' if 'S' in flags else 'new'
+ fs_dir_prefix = 'cur' if 'S' in flags else 'new'
if flags != self.messagelist[uid]['flags']:
- # Flags have actually changed, construct new filename Strip
+ # Flags have actually changed, construct new filename. Strip
# off existing infostring (possibly discarding small letter
- # flags that dovecot uses TODO)
- infomatch = self.re_flagmatch.search(filename)
+ # flags that dovecot uses TODO).
+ infomatch = self.re_flagmatch.search(fs_filename)
if infomatch:
- filename = filename[:-len(infomatch.group())] #strip off
- infostr = '%s2,%s'% (self.infosep, ''.join(sorted(flags)))
- filename += infostr
+ fs_filename = fs_filename[:-len(infomatch.group())] # strip off
+ infostr = '%s2,%s'% (self.infosep.fs, ''.join(sorted(flags)))
+ fs_filename += infostr
- newfilename = os.path.join(dir_prefix, filename)
+ newfilename = fsString(os.path.join(fs_dir_prefix, fs_filename))
if (newfilename != oldfilename):
try:
- os.rename(os.path.join(self.getfullname(), oldfilename),
- os.path.join(self.getfullname(), newfilename))
+ fs_old = os.path.join(self.getfullname().fs, oldfilename.fs)
+ fs_new = os.path.join(self.getfullname().fs, newfilename.fs)
+ os.rename(fs_old, fs_new)
except OSError as e:
- raise OfflineImapError("Can't rename file '%s' to '%s': %s" % (
- oldfilename, newfilename, e[1]),
- OfflineImapError.ERROR.FOLDER), \
- None, exc_info()[2]
+ raise OfflineImapError("Can't rename file '%s' to '%s': %s"% (
+ oldfilename.fs, newfilename.fs, e[1]),
+ OfflineImapError.ERROR.FOLDER), \
+ None, exc_info()[2]
self.messagelist[uid]['flags'] = flags
self.messagelist[uid]['filename'] = newfilename
@@ -400,12 +426,13 @@ class MaildirFolder(BaseFolder):
if uid == new_uid: return
oldfilename = self.messagelist[uid]['filename']
- dir_prefix, filename = os.path.split(oldfilename)
+ fs_dir_prefix = os.path.split(oldfilename.fs)[0]
flags = self.getmessageflags(uid)
- newfilename = os.path.join(dir_prefix,
- self.new_message_filename(new_uid, flags))
- os.rename(os.path.join(self.getfullname(), oldfilename),
- os.path.join(self.getfullname(), newfilename))
+ newfilename = fsString(os.path.join(
+ fs_dir_prefix, self.new_message_filename(new_uid, flags).fs))
+ fs_old = os.path.join(self.getfullname().fs, oldfilename.fs)
+ fs_new = os.path.join(self.getfullname().fs, newfilename.fs)
+ os.rename(fs_old, fs_new)
self.messagelist[new_uid] = self.messagelist[uid]
self.messagelist[new_uid]['filename'] = newfilename
del self.messagelist[uid]
@@ -419,19 +446,20 @@ class MaildirFolder(BaseFolder):
:return: Nothing, or an Exception if UID but no corresponding file
found.
"""
+
if not self.uidexists(uid):
return
filename = self.messagelist[uid]['filename']
- filepath = os.path.join(self.getfullname(), filename)
+ fs_filepath = os.path.join(self.getfullname().fs, filename.fs)
try:
- os.unlink(filepath)
+ os.unlink(fs_filepath)
except OSError:
# Can't find the file -- maybe already deleted?
newmsglist = self._scanfolder()
if uid in newmsglist: # Nope, try new filename.
filename = newmsglist[uid]['filename']
- filepath = os.path.join(self.getfullname(), filename)
- os.unlink(filepath)
+ fs_filepath = os.path.join(self.getfullname().fs, filename.fs)
+ os.unlink(fs_filepath)
# Yep -- return.
del(self.messagelist[uid])
diff --git a/offlineimap/folder/UIDMaps.py b/offlineimap/folder/UIDMaps.py
index e8ca9a7..c1069f3 100644
--- a/offlineimap/folder/UIDMaps.py
+++ b/offlineimap/folder/UIDMaps.py
@@ -16,10 +16,15 @@
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
from sys import exc_info
+import os.path
+import codecs
from threading import Lock
+
from offlineimap import OfflineImapError
from .IMAP import IMAPFolder
-import os.path
+from offlineimap.utils import uni
+from offlineimap.utils.uni import fsString, uniString
+
class MappedIMAPFolder(IMAPFolder):
"""IMAP class to map between Folder() instances where both side assign a uid
@@ -42,27 +47,27 @@ class MappedIMAPFolder(IMAPFolder):
"""Representing the local IMAP Folder using local UIDs"""
def _getmapfilename(self):
- return os.path.join(self.repository.getmapdir(),
- self.getfolderbasename())
+ return fsString(os.path.join(
+ self.repository.getmapdir().fs, self.getfolderbasename().fs))
def _loadmaps(self):
self.maplock.acquire()
try:
mapfilename = self._getmapfilename()
- if not os.path.exists(mapfilename):
+ if not os.path.exists(mapfilename.fs):
return ({}, {})
- file = open(mapfilename, 'rt')
+ mapfile = open(mapfilename.fs, 'rt')
r2l = {}
l2r = {}
while 1:
- line = file.readline()
+ line = mapfile.readline()
if not len(line):
break
try:
line = line.strip()
except ValueError:
raise Exception("Corrupt line '%s' in UID mapping file '%s'"%
- (line, mapfilename)), None, exc_info()[2]
+ (line, mapfilename.fs)), None, exc_info()[2]
(str1, str2) = line.split(':')
loc = long(str1)
rem = long(str2)
@@ -76,11 +81,16 @@ class MappedIMAPFolder(IMAPFolder):
mapfilename = self._getmapfilename()
if dolock: self.maplock.acquire()
try:
- file = open(mapfilename + ".tmp", 'wt')
+ tmpname = fsString(mapfilename.fs + ".tmp")
+ if uni.use_unicode():
+ mapfile = codecs.open(tmpname.fs, 'wt', uni.ENCODING)
+ else:
+ mapfile = open(tmpname.fs, 'wt')
for (key, value) in self.diskl2r.iteritems():
- file.write("%d:%d\n"% (key, value))
- file.close()
- os.rename(mapfilename + '.tmp', mapfilename)
+ data = uniString("%d:%d\n"% (key, value))
+ mapfile.write(data.dbytes)
+ mapfile.close()
+ os.rename(tmpname.fs, mapfilename.fs)
finally:
if dolock: self.maplock.release()
@@ -90,7 +100,7 @@ class MappedIMAPFolder(IMAPFolder):
except KeyError as e:
raise OfflineImapError("Could not find UID for msg '{0}' (f:'{1}'."
" This is usually a bad thing and should be reported on the ma"
- "iling list.".format(e.args[0], self),
+ "iling list.".format(e.args[0], self.name.fs),
OfflineImapError.ERROR.MESSAGE), None, exc_info()[2]
# Interface from BaseFolder
@@ -131,6 +141,7 @@ class MappedIMAPFolder(IMAPFolder):
# Interface from BaseFolder
def uidexists(self, ruid):
"""Checks if the (remote) UID exists in this Folder"""
+
# This implementation overrides the one in BaseFolder, as it is
# much more efficient for the mapped case.
return ruid in self.r2l
@@ -139,6 +150,7 @@ class MappedIMAPFolder(IMAPFolder):
def getmessageuidlist(self):
"""Gets a list of (remote) UIDs.
You may have to call cachemessagelist() before calling this function!"""
+
# This implementation overrides the one in BaseFolder, as it is
# much more efficient for the mapped case.
return self.r2l.keys()
@@ -147,6 +159,7 @@ class MappedIMAPFolder(IMAPFolder):
def getmessagecount(self):
"""Gets the number of messages in this folder.
You may have to call cachemessagelist() before calling this function!"""
+
# This implementation overrides the one in BaseFolder, as it is
# much more efficient for the mapped case.
return len(self.r2l)
@@ -180,6 +193,7 @@ class MappedIMAPFolder(IMAPFolder):
# Interface from BaseFolder
def getmessage(self, uid):
"""Returns the content of the specified message."""
+
return self._mb.getmessage(self.r2l[uid])
# Interface from BaseFolder
@@ -200,8 +214,8 @@ class MappedIMAPFolder(IMAPFolder):
See folder/Base for details. Note that savemessage() does not
check against dryrun settings, so you need to ensure that
- savemessage is never called in a dryrun mode.
- """
+ savemessage is never called in a dryrun mode."""
+
self.ui.savemessage('imap', uid, flags, self)
# Mapped UID instances require the source to already have a
# positive UID, so simply return here.
@@ -238,11 +252,10 @@ class MappedIMAPFolder(IMAPFolder):
# Interface from BaseFolder
def savemessageflags(self, uid, flags):
- """
-
- Note that this function does not check against dryrun settings,
+ """Note that this function does not check against dryrun settings,
so you need to ensure that it is never called in a
dryrun mode."""
+
self._mb.savemessageflags(self.r2l[uid], flags)
# Interface from BaseFolder
@@ -251,8 +264,8 @@ class MappedIMAPFolder(IMAPFolder):
# Interface from BaseFolder
def addmessagesflags(self, uidlist, flags):
- self._mb.addmessagesflags(self._uidlist(self.r2l, uidlist),
- flags)
+ self._mb.addmessagesflags(
+ self._uidlist(self.r2l, uidlist), flags)
# Interface from BaseFolder
def change_message_uid(self, ruid, new_ruid):
@@ -261,6 +274,7 @@ class MappedIMAPFolder(IMAPFolder):
:param new_uid: The old remote UID will be changed to a new
UID. The UIDMaps case handles this efficiently by simply
changing the mappings file."""
+
if ruid not in self.r2l:
raise OfflineImapError("Cannot change unknown Maildir UID %s"%
ruid, OfflineImapError.ERROR.MESSAGE)
@@ -303,8 +317,8 @@ class MappedIMAPFolder(IMAPFolder):
# Interface from BaseFolder
def deletemessagesflags(self, uidlist, flags):
- self._mb.deletemessagesflags(self._uidlist(self.r2l, uidlist),
- flags)
+ self._mb.deletemessagesflags(
+ self._uidlist(self.r2l, uidlist), flags)
# Interface from BaseFolder
def deletemessage(self, uid):
diff --git a/offlineimap/imaplibutil.py b/offlineimap/imaplibutil.py
index 83ffe9a..dd3c96c 100644
--- a/offlineimap/imaplibutil.py
+++ b/offlineimap/imaplibutil.py
@@ -27,6 +27,11 @@ from offlineimap import OfflineImapError
from offlineimap.imaplib2 import IMAP4, IMAP4_SSL, zlib, InternalDate, Mon2num
+#
+# UNICODE: consider this module to work with IMAP encoded data. Features here
+# are low-level drivers. Do not import uni module.
+#
+
class UsefulIMAPMixIn(object):
def __getselectedfolder(self):
if self.state == 'SELECTED':
@@ -54,12 +59,12 @@ class UsefulIMAPMixIn(object):
except self.abort as e:
# self.abort is raised when we are supposed to retry
errstr = "Server '%s' closed connection, error on SELECT '%s'. Ser"\
- "ver said: %s" % (self.host, mailbox, e.args[0])
+ "ver said: %s"% (self.host, mailbox, e.args[0])
severity = OfflineImapError.ERROR.FOLDER_RETRY
raise OfflineImapError(errstr, severity), None, exc_info()[2]
if result[0] != 'OK':
#in case of error, bail out with OfflineImapError
- errstr = "Error SELECTing mailbox '%s', server reply:\n%s" %\
+ errstr = "Error SELECTing mailbox '%s', server reply:\n%s"%\
(mailbox, result)
severity = OfflineImapError.ERROR.FOLDER
raise OfflineImapError(errstr, severity)
@@ -134,7 +139,8 @@ def new_mesg(self, s, tn=None, secs=None):
if tn is None:
tn = threading.currentThread().getName()
tm = time.strftime('%M:%S', time.localtime(secs))
- getglobalui().debug('imap', ' %s.%02d %s %s' % (tm, (secs*100)%100, tn, s))
+ getglobalui().debug(u'imap', u' %s.%02d %s %s'%
+ (tm, (secs*100)%100, tn, s))
class WrappedIMAP4_SSL(UsefulIMAPMixIn, IMAP4_SSL):
@@ -151,21 +157,23 @@ class WrappedIMAP4_SSL(UsefulIMAPMixIn, IMAP4_SSL):
def open(self, host=None, port=None):
if not self.ca_certs and not self._fingerprint:
raise OfflineImapError("No CA certificates "
- "and no server fingerprints configured. "
- "You must configure at least something, otherwise "
- "having SSL helps nothing.", OfflineImapError.ERROR.REPO)
+ "and no server fingerprints configured. "
+ "You must configure at least something, otherwise "
+ "having SSL helps nothing.",
+ OfflineImapError.ERROR.REPO)
super(WrappedIMAP4_SSL, self).open(host, port)
if self._fingerprint:
# compare fingerprints
fingerprint = sha1(self.sock.getpeercert(True)).hexdigest()
if fingerprint not in self._fingerprint:
raise OfflineImapError("Server SSL fingerprint '%s' "
- "for hostname '%s' "
- "does not match configured fingerprint(s) %s. "
- "Please verify and set 'cert_fingerprint' accordingly "
- "if not set yet."%
- (fingerprint, host, self._fingerprint),
- OfflineImapError.ERROR.REPO)
+ "for hostname '%s' "
+ "does not match configured fingerprint(s) %s. "
+ "Please verify and set 'cert_fingerprint' accordingly "
+ "if not set yet."%
+ (fingerprint, host,
+ self._fingerprint),
+ OfflineImapError.ERROR.REPO)
class WrappedIMAP4(UsefulIMAPMixIn, IMAP4):
diff --git a/offlineimap/imapserver.py b/offlineimap/imapserver.py
index 3d69426..5b55173 100644
--- a/offlineimap/imapserver.py
+++ b/offlineimap/imapserver.py
@@ -15,10 +15,6 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
-from offlineimap import imaplibutil, imaputil, threadutil, OfflineImapError
-from offlineimap.ui import getglobalui
-from threading import Lock, BoundedSemaphore, Thread, Event, currentThread
-import offlineimap.accounts
import hmac
import socket
import base64
@@ -28,6 +24,12 @@ from sys import exc_info
from socket import gaierror
from ssl import SSLError, cert_time_to_seconds
+from offlineimap import imaplibutil, imaputil, threadutil, OfflineImapError
+from offlineimap.ui import getglobalui
+from threading import Lock, BoundedSemaphore, Thread, Event, currentThread
+import offlineimap.accounts
+from offlineimap.utils.uni import noneString, uniString, dbytesString, fsString, imapString
+
try:
# do we have a recent pykerberos?
have_gss = False
@@ -37,8 +39,16 @@ try:
except ImportError:
pass
+
+#
+# XXX: 'repos' is a variable name currently used as alias to 'repository'.
+# This is bad because it lies about what could be understood as explicit plural
+# (a list of repositories). Either use 'repo' or 'repository' but NOT 'repos'.
+#
+# XXX: if it's actually just about initialization, call it InitIMAPServer.
+#
class IMAPServer:
- """Initializes all variables from an IMAPRepository() instance
+ """Initializes all variables from an IMAPRepository() instance.
Various functions, such as acquireconnection() return an IMAP4
object on which we can operate.
@@ -56,39 +66,40 @@ class IMAPServer:
self.preauth_tunnel = repos.getpreauthtunnel()
self.transport_tunnel = repos.gettransporttunnel()
- if self.preauth_tunnel and self.transport_tunnel:
- raise OfflineImapError('%s: '% repos +
+ if self.preauth_tunnel.uni and self.transport_tunnel:
+ raise OfflineImapError('%s: '% repos.getname().fs +
'you must enable precisely one '
'type of tunnel (preauth or transport), '
'not both', OfflineImapError.ERROR.REPO)
self.tunnel = \
- self.preauth_tunnel if self.preauth_tunnel \
+ self.preauth_tunnel if self.preauth_tunnel.uni \
else self.transport_tunnel
self.username = \
- None if self.preauth_tunnel else repos.getuser()
+ noneString() if self.preauth_tunnel.uni else repos.getuser()
self.user_identity = repos.get_remote_identity()
self.authmechs = repos.get_auth_mechanisms()
- self.password = None
- self.passworderror = None
- self.goodpassword = None
+ self.password = noneString()
+ self.passworderror = noneString()
+ self.goodpassword = noneString()
self.usessl = repos.getssl()
self.hostname = \
- None if self.preauth_tunnel else repos.gethost()
+ noneString() if self.preauth_tunnel.uni else repos.gethost()
self.port = repos.getport()
if self.port == None:
self.port = 993 if self.usessl else 143
self.sslclientcert = repos.getsslclientcert()
self.sslclientkey = repos.getsslclientkey()
self.sslcacertfile = repos.getsslcacertfile()
- if self.sslcacertfile is None:
+ if self.sslcacertfile.uni is None:
self.__verifycert = None # disable cert verification
- self.fingerprint = repos.get_ssl_fingerprint()
+ # List of str values.
+ self.fingerprints = repos.get_ssl_fingerprints()
self.sslversion = repos.getsslversion()
- self.delim = None
- self.root = None
+ self.delim = noneString()
+ self.root = noneString()
self.maxconnections = repos.getmaxconnections()
self.availableconnections = []
self.assignedconnections = []
@@ -103,26 +114,30 @@ class IMAPServer:
def __getpassword(self):
"""Returns the server password or None"""
- if self.goodpassword != None: # use cached good one first
+
+ if self.goodpassword.value != None: # use cached good one first
return self.goodpassword
- if self.password != None and self.passworderror == None:
+ if self.password.uni != None and self.passworderror.uni == None:
return self.password # non-failed preconfigured one
# get 1) configured password first 2) fall back to asking via UI
- self.password = self.repos.getpassword() or \
- self.ui.getpass(self.repos.getname(), self.config,
- self.passworderror)
- self.passworderror = None
+ self.password = self.repos.getpassword()
+ if not self.password.uni:
+ self.ui.getpass(self.repos.getname().uni, self.config,
+ self.passworderror.uni)
+ self.passworderror = noneString()
return self.password
- # XXX: is this function used anywhere?
+
def getroot(self):
"""Returns this server's folder root. Can only be called after one
or more calls to acquireconnection."""
return self.root
+ def getdelim(self):
+ return self.delim
def releaseconnection(self, connection, drop_conn=False):
"""Releases a connection, returning it to the pool.
@@ -130,7 +145,7 @@ class IMAPServer:
:param drop_conn: If True, the connection will be released and
not be reused. This can be used to indicate broken connections."""
- if connection is None: return #noop on bad connection
+ if connection is None: return # noop on bad connection
self.connectionlock.acquire()
self.assignedconnections.remove(connection)
# Don't reuse broken connections
@@ -142,44 +157,51 @@ class IMAPServer:
self.semaphore.release()
def __md5handler(self, response):
+ """This method is a hook. input/ouput are basic str python type."""
+
challenge = response.strip()
- self.ui.debug('imap', '__md5handler: got challenge %s'% challenge)
+ self.ui.debug('imap', u'__md5handler: got challenge %s'% challenge)
- passwd = self.__getpassword()
- retval = self.username + ' ' + hmac.new(passwd, challenge).hexdigest()
- self.ui.debug('imap', '__md5handler: returning %s'% retval)
+ passwd = self.__getpassword().value
+ retval = self.username.dbytes + ' ' + \
+ hmac.new(passwd, challenge).hexdigest()
+ self.ui.debug('imap', u'__md5handler: returning %s'% retval)
return retval
def __loginauth(self, imapobj):
""" Basic authentication via LOGIN command."""
- self.ui.debug('imap', 'Attempting IMAP LOGIN authentication')
- imapobj.login(self.username, self.__getpassword())
-
+ self.ui.debug('imap', u'Attempting IMAP LOGIN authentication')
+ imapobj.login(self.username.dbytes, self.__getpassword().dbytes)
def __plainhandler(self, response):
- """Implements SASL PLAIN authentication, RFC 4616,
- http://tools.ietf.org/html/rfc4616"""
+ """This method is a hook. input/ouput are basic str python type.
+
+ Implements SASL PLAIN authentication, RFC 4616,
+ http://tools.ietf.org/html/rfc4616
+ """
- authc = self.username
- passwd = self.__getpassword()
+ authc = self.username.dbytes
+ passwd = self.__getpassword().value
authz = ''
- if self.user_identity != None:
- authz = self.user_identity
- NULL = u'\x00'
- retval = NULL.join((authz, authc, passwd)).encode('utf-8')
- self.ui.debug('imap', '__plainhandler: returning %s' % retval)
+ if self.user_identity.uni != None:
+ authz = self.user_identity.imap
+ NULL = '\x00'
+ retval = NULL.join((authz, authc, passwd))
+ self.ui.debug('imap', u'__plainhandler: returning %s'%
+ dbytesString(retval).uni)
return retval
-
# XXX: describe function
def __gssauth(self, response):
+ """This method is a hook. input/ouput are basic str python type."""
+
data = base64.b64encode(response)
try:
if self.gss_step == self.GSS_STATE_STEP:
if not self.gss_vc:
rc, self.gss_vc = kerberos.authGSSClientInit(
- 'imap@' + self.hostname)
+ 'imap@' + self.hostname.dbytes)
response = kerberos.authGSSClientResponse(self.gss_vc)
rc = kerberos.authGSSClientStep(self.gss_vc, data)
if rc != kerberos.AUTH_GSS_CONTINUE:
@@ -188,12 +210,12 @@ class IMAPServer:
rc = kerberos.authGSSClientUnwrap(self.gss_vc, data)
response = kerberos.authGSSClientResponse(self.gss_vc)
rc = kerberos.authGSSClientWrap(
- self.gss_vc, response, self.username)
+ self.gss_vc, response, self.username.dbytes)
response = kerberos.authGSSClientResponse(self.gss_vc)
except kerberos.GSSError as err:
# Kerberos errored out on us, respond with None to cancel the
# authentication
- self.ui.debug('imap', '%s: %s'% (err[0][0], err[1][0]))
+ self.ui.debug('imap', u'%s: %s'% err[0][0], err[1][0])
return None
if not response:
@@ -203,7 +225,7 @@ class IMAPServer:
def __start_tls(self, imapobj):
if 'STARTTLS' in imapobj.capabilities and not self.usessl:
- self.ui.debug('imap', 'Using STARTTLS connection')
+ self.ui.debug('imap', u'Using STARTTLS connection')
try:
imapobj.starttls()
except imapobj.error as e:
@@ -305,7 +327,7 @@ class IMAPServer:
for m in mechs:
if m not in auth_methods:
raise Exception("Bad authentication method %s, "
- "please, file OfflineIMAP bug" % m)
+ "please, file OfflineIMAP bug"% m)
func, tryTLS, check_cap = auth_methods[m]
@@ -321,13 +343,12 @@ class IMAPServer:
continue
tried_to_authn = True
- self.ui.debug('imap', u'Attempting '
- '%s authentication'% m)
+ self.ui.debug('imap', u'Attempting %s authentication'% m)
try:
if func(imapobj):
return
except (imapobj.error, OfflineImapError) as e:
- self.ui.warn('%s authentication failed: %s'% (m, e))
+ self.ui.warn(u'%s authentication failed: %s'% (m, e))
exc_stack.append((m, e))
if len(exc_stack):
@@ -343,14 +364,13 @@ class IMAPServer:
lambda x: x[5:], filter(lambda x: x[0:5] == "AUTH=",
imapobj.capabilities)
))
- raise OfflineImapError(u"Repository %s: no supported "
+ raise OfflineImapError("Repository %s: no supported "
"authentication mechanisms found; configured %s, "
- "server advertises %s"% (self.repos,
+ "server advertises %s"% (self.repos.getname().fs,
", ".join(self.authmechs), methods),
OfflineImapError.ERROR.REPO)
- # XXX: move above, closer to releaseconnection()
def acquireconnection(self):
"""Fetches a connection from the pool, making sure to create a new one
if needed, to obey the maximum connection limits, etc.
@@ -359,7 +379,7 @@ class IMAPServer:
self.semaphore.acquire()
self.connectionlock.acquire()
- curThread = currentThread()
+ curThread = currentThread() # .getNames() -> 'Account sync $accountname'
imapobj = None
if len(self.availableconnections): # One is available.
@@ -385,40 +405,48 @@ class IMAPServer:
# Must be careful here that if we fail we should bail out gracefully
# and release locks / threads so that the next attempt can try...
- success = 0
+ success = False
try:
while not success:
# Generate a new connection.
- if self.tunnel:
- self.ui.connecting('tunnel', self.tunnel)
- imapobj = imaplibutil.IMAP4_Tunnel(self.tunnel,
- timeout=socket.getdefaulttimeout())
- success = 1
+ # 0. tunnel
+ if self.tunnel.uni:
+ self.ui.connecting(u'tunnel', self.tunnel.uni)
+ imapobj = imaplibutil.IMAP4_Tunnel(self.tunnel.uni,
+ timeout=socket.getdefaulttimeout())
+ success = True
+ # 1. tunnel
elif self.usessl:
- self.ui.connecting(self.hostname, self.port)
- imapobj = imaplibutil.WrappedIMAP4_SSL(self.hostname,
- self.port,
- self.sslclientkey,
- self.sslclientcert,
- self.sslcacertfile,
- self.__verifycert,
- self.sslversion,
- timeout=socket.getdefaulttimeout(),
- fingerprint=self.fingerprint
- )
+ self.ui.connecting(self.hostname.uni, self.port)
+ imapobj = imaplibutil.WrappedIMAP4_SSL(
+ self.hostname.dbytes,
+ self.port,
+ self.sslclientkey.dbytes,
+ self.sslclientcert.dbytes,
+ self.sslcacertfile.fs,
+ self.__verifycert,
+ self.sslversion.dbytes,
+ timeout=socket.getdefaulttimeout(),
+ fingerprint=self.fingerprints
+ )
+ # 2. ?
else:
- self.ui.connecting(self.hostname, self.port)
- imapobj = imaplibutil.WrappedIMAP4(self.hostname, self.port,
- timeout=socket.getdefaulttimeout())
-
- if not self.preauth_tunnel:
+ self.ui.connecting(self.hostname.uni, self.port)
+ imapobj = imaplibutil.WrappedIMAP4(
+ self.hostname.dbytes,
+ self.port,
+ timeout=socket.getdefaulttimeout())
+
+ # 3. authn helper
+ if not self.preauth_tunnel.uni:
try:
self.__authn_helper(imapobj)
self.goodpassword = self.password
- success = 1
+ success = True
except OfflineImapError as e:
- self.passworderror = str(e)
+ self.passworderror = fsString("%s"% e)
raise
+ # Ok, we could login.
# Enable compression
if self.repos.getconfboolean('usecompression', 0):
@@ -429,29 +457,30 @@ class IMAPServer:
if dat != [None]:
imapobj.capabilities = tuple(dat[-1].upper().split())
- if self.delim == None:
- listres = imapobj.list(self.reference, '""')[1]
+ if self.delim.uni == None:
+ listres = imapobj.list(self.reference.imap, '""')[1]
if listres == [None] or listres == None:
# Some buggy IMAP servers do not respond well to LIST "" ""
# Work around them.
- listres = imapobj.list(self.reference, '"*"')[1]
+ listres = imapobj.list(self.reference.imap, '"*"')[1]
if listres == [None] or listres == None:
# No Folders were returned. This occurs, e.g. if the
# 'reference' prefix does not exist on the mail
# server. Raise exception.
- err = "Server '%s' returned no folders in '%s'"% \
- (self.repos.getname(), self.reference)
- self.ui.warn(err)
- raise Exception(err)
- self.delim, self.root = \
- imaputil.imapsplit(listres[0])[1:]
- self.delim = imaputil.dequote(self.delim)
- self.root = imaputil.dequote(self.root)
+ err = uniString(u"Server '%s' returned no folders in '%s'"%
+ (self.repos.getname().uni, self.reference.uni))
+ self.ui.warn(err.uni)
+ raise Exception(err.fs)
+ imap_delim, imap_root = \
+ imaputil.imapsplit(listres[0])[1:]
+ self.delim = imapString(imaputil.dequote(imap_delim))
+ self.root = imapString(imaputil.dequote(imap_root))
with self.connectionlock:
self.assignedconnections.append(imapobj)
self.lastowner[imapobj] = curThread.ident
return imapobj
+
except Exception as e:
"""If we are here then we did not succeed in getting a
connection - we should clean up and then re-raise the
@@ -466,7 +495,7 @@ class IMAPServer:
reason = "Could not resolve name '%s' for repository "\
"'%s'. Make sure you have configured the ser"\
"ver name correctly and that you are online."%\
- (self.hostname, self.repos)
+ (self.hostname.fs, self.repos.getname().fs)
raise OfflineImapError(reason, severity), None, exc_info()[2]
elif isinstance(e, SSLError) and e.errno == errno.EPERM:
@@ -475,11 +504,11 @@ class IMAPServer:
if self.port != 993:
reason = "Could not connect via SSL to host '%s' and non-s"\
"tandard ssl port %d configured. Make sure you connect"\
- " to the correct port."% (self.hostname, self.port)
+ " to the correct port."% (self.hostname.fs, self.port)
else:
reason = "Unknown SSL protocol connecting to host '%s' for "\
- "repository '%s'. OpenSSL responded:\n%s"\
- % (self.hostname, self.repos, e)
+ "repository '%s'. OpenSSL responded:\n%s"%\
+ (self.hostname.fs, self.repos.getname().fs, e)
raise OfflineImapError(reason, severity), None, exc_info()[2]
elif isinstance(e, socket.error) and e.args[0] == errno.ECONNREFUSED:
@@ -488,14 +517,15 @@ class IMAPServer:
reason = "Connection to host '%s:%d' for repository '%s' was "\
"refused. Make sure you have the right host and port "\
"configured and that you are actually able to access the "\
- "network."% (self.hostname, self.port, self.repos)
+ "network."% (self.hostname.fs, self.port,
+ self.repos.getname().fs)
raise OfflineImapError(reason, severity), None, exc_info()[2]
# Could not acquire connection to the remote;
# socket.error(last_error) raised
if str(e)[:24] == "can't open socket; error":
- raise OfflineImapError("Could not connect to remote server '%s' "\
- "for repository '%s'. Remote does not answer."
- % (self.hostname, self.repos),
+ raise OfflineImapError("Could not connect to remote server '%s' "
+ "for repository '%s'. Remote does not answer."%
+ (self.hostname.fs, self.repos.getname().fs),
OfflineImapError.ERROR.REPO), None, exc_info()[2]
else:
# re-raise all other errors
@@ -541,7 +571,7 @@ class IMAPServer:
is expected to be invoked in a separate thread, which should be join()'d
after the event is set."""
- self.ui.debug('imap', 'keepalive thread started')
+ self.ui.debug('imap', u'keepalive thread started')
while not event.isSet():
self.connectionlock.acquire()
numconnections = len(self.assignedconnections) + \
@@ -550,7 +580,8 @@ class IMAPServer:
threads = []
for i in range(numconnections):
- self.ui.debug('imap', 'keepalive: processing connection %d of %d'% (i, numconnections))
+ self.ui.debug('imap', u'keepalive: processing connection '
+ '%d of %d'% (i, numconnections))
if len(self.idlefolders) > i:
# IDLE thread
idler = IdleThread(self, self.idlefolders[i])
@@ -560,23 +591,26 @@ class IMAPServer:
idler.start()
threads.append(idler)
- self.ui.debug('imap', 'keepalive: waiting for timeout')
+ self.ui.debug('imap', u'keepalive: waiting for timeout')
event.wait(timeout)
- self.ui.debug('imap', 'keepalive: after wait')
+ self.ui.debug('imap', u'keepalive: after wait')
for idler in threads:
# Make sure all the commands have completed.
idler.stop()
idler.join()
- self.ui.debug('imap', 'keepalive: all threads joined')
- self.ui.debug('imap', 'keepalive: event is set; exiting')
+ self.ui.debug('imap', u'keepalive: all threads joined')
+ self.ui.debug('imap', u'keepalive: event is set; exiting')
return
def __verifycert(self, cert, hostname):
"""Verify that cert (in socket.getpeercert() format) matches hostname.
CRLs are not handled.
- Returns error message if any problems are found and None on success."""
+ Returns error message if any problems are found and None on success.
+
+ We don't call this function ourself. It is passed to low-level drivers
+ as a hook. Do NOT work with unicode nor uni String, here."""
errstr = "CA Cert verifying failed: "
if not cert:
@@ -616,15 +650,26 @@ class IdleThread(object):
def __init__(self, parent, folder=None):
"""If invoked without 'folder', perform a NOOP and wait for
self.stop() to be called. If invoked with folder, switch to IDLE
- mode and synchronize once we have a new message"""
+ mode and synchronize once we have a new message.
+
+ XXX: requires better class comments:
+ - why is it here?
+ - what is threaded?
+ IOW, explain WHAT it does and HOW it works.
+ The name is lying around. It is composed with Thread, it doesn't inherit from
+ a Thread class/subclass. Call it IdleMode, IdleSync or whatever but NOT
+ IdleTread."""
self.parent = parent
self.folder = folder
self.stop_sig = Event()
self.ui = getglobalui()
- if folder is None:
+ if folder.uni is None:
self.thread = Thread(target=self.noop)
else:
+ # Oh no... Passing a 'hidden' function to something outside is BAD.
+ # It's like accessing a private method/attribute of an encapsulated
+ # object...
self.thread = Thread(target=self.__idle)
self.thread.setDaemon(1)
@@ -648,7 +693,7 @@ class IdleThread(object):
try:
imapobj.noop()
except imapobj.abort:
- self.ui.warn('Attempting NOOP on dropped connection %s'%
+ self.ui.warn(u'Attempting NOOP on dropped connection %s'%
imapobj.identifier)
self.parent.releaseconnection(imapobj, True)
imapobj = None
@@ -665,17 +710,20 @@ class IdleThread(object):
statusrepos = account.statusrepos
remotefolder = remoterepos.getfolder(self.folder)
- hook = account.getconf('presynchook', '')
+ hook = uniString(account.getconf('presynchook', ''))
account.callhook(hook)
+
offlineimap.accounts.syncfolder(account, remotefolder, quick=False)
- hook = account.getconf('postsynchook', '')
+
+ hook = uniString(account.getconf('postsynchook', ''))
account.callhook(hook)
ui = getglobalui()
- ui.unregisterthread(currentThread()) #syncfolder registered the thread
+ ui.unregisterthread(currentThread()) # syncfolder registered the thread
def __idle(self):
- """Invoke IDLE mode until timeout or self.stop() is invoked"""
+ """Invoke IDLE mode until timeout or self.stop() is invoked."""
+
def callback(args):
"""IDLE callback function invoked by imaplib2
@@ -683,6 +731,7 @@ class IdleThread(object):
while in IDLE mode, b) we get an Exception (e.g. on dropped
connections, or c) the standard imaplib IDLE timeout of 29
minutes kicks in."""
+
result, cb_arg, exc_data = args
if exc_data is None and not self.stop_sig.isSet():
# No Exception, and we are not supposed to stop:
@@ -696,7 +745,7 @@ class IdleThread(object):
while not success:
imapobj = self.parent.acquireconnection()
try:
- imapobj.select(self.folder)
+ imapobj.select(self.folder.imap)
except OfflineImapError as e:
if e.severity == OfflineImapError.ERROR.FOLDER_RETRY:
# Connection closed, release connection and retry
@@ -709,7 +758,7 @@ class IdleThread(object):
if "IDLE" in imapobj.capabilities:
imapobj.idle(callback=callback)
else:
- self.ui.warn("IMAP IDLE not supported on server '%s'."
+ self.ui.warn(u"IMAP IDLE not supported on server '%s'."
"Sleep until next refresh cycle."% imapobj.identifier)
imapobj.noop()
self.stop_sig.wait() # self.stop() or IDLE callback are invoked
@@ -717,7 +766,7 @@ class IdleThread(object):
# End IDLE mode with noop, imapobj can point to a dropped conn.
imapobj.noop()
except imapobj.abort:
- self.ui.warn('Attempting NOOP on dropped connection %s'%
+ self.ui.warn(u'Attempting NOOP on dropped connection %s'%
imapobj.identifier)
self.parent.releaseconnection(imapobj, True)
else:
diff --git a/offlineimap/imaputil.py b/offlineimap/imaputil.py
index f1f287b..e77affb 100644
--- a/offlineimap/imaputil.py
+++ b/offlineimap/imaputil.py
@@ -17,8 +17,13 @@
import re
import string
+
from offlineimap.ui import getglobalui
+#
+# UNICODE: This module makes low-level jobs for IMAP objects. Do NOT import uni
+# module. Everything is expected IMAP encoded.
+#
## Globals
@@ -30,7 +35,7 @@ def __debug(*args):
msg = []
for arg in args:
msg.append(str(arg))
- getglobalui().debug('imap', " ".join(msg))
+ getglobalui().debug('imap', u" ".join(msg))
def dequote(s):
"""Takes string which may or may not be quoted and unquotes it.
@@ -77,7 +82,7 @@ def __options2hash(list):
while (counter < len(list)):
retval[list[counter]] = list[counter + 1]
counter += 2
- __debug("__options2hash returning:", retval)
+ __debug(u"__options2hash returning:", retval)
return retval
def flags2hash(flags):
@@ -99,7 +104,7 @@ def imapsplit(imapstring):
['(\\HasNoChildren)', '"."', '"INBOX.Sent"']"""
if not isinstance(imapstring, basestring):
- __debug("imapsplit() got a non-string input; working around.")
+ __debug(u"imapsplit() got a non-string input; working around.")
# Sometimes, imaplib will throw us a tuple if the input
# contains a literal. See Python bug
# #619732 at https://sourceforge.net/tracker/index.php?func=detail&aid=619732&group_id=5470&atid=105470
@@ -121,7 +126,7 @@ def imapsplit(imapstring):
arg = arg.replace('\\', '\\\\')
arg = arg.replace('"', '\\"')
arg = '"%s"' % arg
- __debug("imapsplit() non-string [%d]: Appending %s"% (i, arg))
+ __debug(u"imapsplit() non-string [%d]: Appending %s"% (i, arg))
retval.append(arg)
else:
# Even -- we have a string that ends with a literal
@@ -130,10 +135,10 @@ def imapsplit(imapstring):
# Recursion to the rescue.
arg = imapstring[i]
arg = re.sub('\{\d+\}$', '', arg)
- __debug("imapsplit() non-string [%d]: Feeding %s to recursion"%\
+ __debug(u"imapsplit() non-string [%d]: Feeding %s to recursion"%
(i, arg))
retval.extend(imapsplit(arg))
- __debug("imapsplit() non-string: returning %s" % str(retval))
+ __debug(u"imapsplit() non-string: returning %s"% str(retval))
return retval
workstr = imapstring.strip()
@@ -254,7 +259,8 @@ def __split_quoted(s):
while True:
next_q = rest.find(q)
if next_q == -1:
- raise ValueError("can't find ending quote '%s' in '%s'"% (q, s))
+ raise ValueError("can't find ending quote '%s' in '%s'"%
+ (q, s))
# If quote is preceeded by even number of backslashes,
# then it is the ending quote, otherwise the quote
# character is escaped by backslash, so we should
diff --git a/offlineimap/init.py b/offlineimap/init.py
index 7f6a679..25b1bcd 100644
--- a/offlineimap/init.py
+++ b/offlineimap/init.py
@@ -22,14 +22,17 @@ import offlineimap.imaplib2 as imaplib
import signal
import socket
import logging
-from optparse import OptionParser
+import codecs
+from optparse import OptionParser, SUPPRESS_HELP
import offlineimap
-from offlineimap import accounts, threadutil, syncmaster
from offlineimap import globals
+from offlineimap import accounts, threadutil, syncmaster
from offlineimap.ui import UI_LIST, setglobalui, getglobalui
from offlineimap.CustomConfig import CustomConfigParser
-from offlineimap.utils import stacktrace
+from offlineimap.utils import stacktrace, uni
+from offlineimap.utils.uni import fsString, uniString, isASCII
+from offlineimap.utils import hack
class OfflineImap:
@@ -52,9 +55,8 @@ class OfflineImap:
def __parse_cmd_options(self):
parser = OptionParser(version=offlineimap.__bigversion__,
- description="%s.\n\n%s" %
- (offlineimap.__copyright__,
- offlineimap.__license__))
+ description="%s.\n\n%s" %
+ (offlineimap.__copyright__, offlineimap.__license__))
parser.add_option("--dry-run",
action="store_true", dest="dryrun",
default=False,
@@ -82,6 +84,15 @@ class OfflineImap:
"maxsyncaccounts and all maxconnections configuration file "
"variables to 1.")
+ parser.add_option("--unicode", dest="unicode_help",
+ action='store_true',
+ help="Enable Unicode support (EXPERIMENTAL).")
+ parser.add_option("--enable-unicode",
+ action='store_true', dest='use_unicode', help=SUPPRESS_HELP)
+ parser.add_option("--no-unicode",
+ action='store_false', dest='use_unicode',
+ help="Disable Unicode support (default).")
+
parser.add_option("-P", dest="profiledir", metavar="DIR",
help="Sets OfflineIMAP into profile mode. The program "
"will create DIR (it must not already exist). "
@@ -92,7 +103,8 @@ class OfflineImap:
"specific reason to do so. It will significantly "
"decrease program performance, may reduce reliability, "
"and can generate huge amounts of data. This option "
- "implies the -1 option.")
+ "implies the -1 option. "
+ "Support only ASCII encoded path.")
parser.add_option("-a", dest="accounts", metavar="ACCOUNTS",
help="Overrides the accounts section in the config file. "
@@ -123,6 +135,7 @@ class OfflineImap:
help="Log to FILE")
parser.add_option("-f", dest="folders", metavar="folder1,[folder2...]",
+ type=str,
help="Only sync the specified folders. The folder names "
"are the *untranslated* foldernames of the remote repository. "
"This command-line option overrides any 'folderfilter' "
@@ -130,6 +143,7 @@ class OfflineImap:
parser.add_option("-k", dest="configoverride",
action="append",
+ type=str,
metavar="[section:]option=value",
help=
"""Override configuration file option. If"section" is
@@ -161,71 +175,96 @@ class OfflineImap:
"not usable. Possible interface choices are: %s " %
", ".join(UI_LIST.keys()))
+ parser.set_defaults(use_unicode=False)
(options, args) = parser.parse_args()
- globals.set_options (options)
+ # UNICODE: REMEMBER that globals.options keeps options filesystem encoded.
+ globals.set_options(options)
+ # Set unicode context as early as possible! This impacts a lot of
+ # things.
+ uni.set_unicode_context(options.use_unicode)
- #read in configuration file
+ if options.unicode_help:
+ uni.help_message()
+ sys.exit(0)
+
+ # Read in configuration file.
if not options.configfile:
+ # Keep options.configfile is str filesystem encoded.
# Try XDG location, then fall back to ~/.offlineimaprc
- xdg_var = 'XDG_CONFIG_HOME'
- if not xdg_var in os.environ or not os.environ[xdg_var]:
- xdg_home = os.path.expanduser('~/.config')
+ xdg_var_fs = 'XDG_CONFIG_HOME'
+ # Expansion on non-ASCII HOME path might be hurting.
+ xdg_home_default = uniString(u'~/.config')
+ configfilename_default = uniString(u'~/.offlineimaprc')
+ # Elements of os.environ are filesystem encoded.
+ if not xdg_var_fs in os.environ or not os.environ[xdg_var_fs]:
+ # os.path.expanduser keeps given encoding.
+ xdg_home = fsString(os.path.expanduser(xdg_home_default.fs))
else:
- xdg_home = os.environ[xdg_var]
- options.configfile = os.path.join(xdg_home, "offlineimap", "config")
+ xdg_home = fsString(os.environ[xdg_var_fs])
+ options.configfile = os.path.join(xdg_home.fs, "offlineimap", "config")
if not os.path.exists(options.configfile):
- options.configfile = os.path.expanduser('~/.offlineimaprc')
- configfilename = options.configfile
+ options.configfile = os.path.expanduser(configfilename_default.fs)
+ configfilename = fsString(options.configfile)
else:
- configfilename = os.path.expanduser(options.configfile)
+ configfilename = fsString(os.path.expanduser(options.configfile))
config = CustomConfigParser()
- 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!"%
- configfilename)
+
+ if not os.path.exists(configfilename.fs):
+ # TODO, initialize and make use of chosen ui for logging.
+ logging.error(u" *** Config file '%s' does not exist; aborting!"%
+ configfilename.uni)
sys.exit(1)
- config.read(configfilename)
+ if uni.use_unicode():
+ config.readfp(codecs.open(configfilename.fs, 'rt', uni.ENCODING))
+ else:
+ config.read(configfilename.fs)
- #profile mode chosen?
+ # Profile mode chosen?
if options.profiledir:
+ profiledir = fsString(options.profiledir)
if not options.singlethreading:
# TODO, make use of chosen ui for logging
- logging.warn("Profile mode: Forcing to singlethreaded.")
+ logging.warn(u"Profile mode: Forcing to singlethreaded.")
options.singlethreading = True
- if os.path.exists(options.profiledir):
+ if os.path.exists(profiledir.fs):
# TODO, make use of chosen ui for logging
- logging.warn("Profile mode: Directory '%s' already exists!"%
- options.profiledir)
+ logging.warn(u"Profile mode: Directory '%s' already exists!"%
+ profiledir.uni)
else:
- os.mkdir(options.profiledir)
- threadutil.ExitNotifyThread.set_profiledir(options.profiledir)
+ os.mkdir(profiledir.fs)
+ # profiledir is still str filesystem encoded, change that.
+ threadutil.ExitNotifyThread.set_profiledir(profiledir)
# TODO, make use of chosen ui for logging
- logging.warn("Profile mode: Potentially large data will be "
- "created in '%s'"% options.profiledir)
+ logging.warn(u"Profile mode: Potentially large data will be "
+ "created in '%s'"% profiledir.fs)
- #override a config value
+ # Override a config value.
if options.configoverride:
- for option in options.configoverride:
+ configoverride = fsString(options.configoverride)
+ for option in configoverride.uni:
(key, value) = option.split('=', 1)
if ':' in key:
(secname, key) = key.split(':', 1)
section = secname.replace("_", " ")
else:
section = "general"
- config.set(section, key, value)
+ # Configuration file is Unicode.
+ config.set(uniString(section).uni, uniString(key).uni,
+ uniString(value).uni)
- #which ui to use? cmd line option overrides config file
- ui_type = config.getdefault('general', 'ui', 'ttyui')
+ # Which ui to use? cmd line option overrides config file.
+ ui_type = uniString(config.getdefault('general', 'ui', 'ttyui'))
if options.interface != None:
- ui_type = options.interface
- if '.' in ui_type:
- #transform Curses.Blinkenlights -> Blinkenlights
- ui_type = ui_type.split('.')[-1]
+ ui_type.fs = options.interface
+ if '.' in ui_type.uni:
+ # Transform Curses.Blinkenlights -> Blinkenlights
+ ui_type.uni = ui_type.split('.')[-1]
# TODO, make use of chosen ui for logging
- logging.warning('Using old interface name, consider using one '
- 'of %s'% ', '.join(UI_LIST.keys()))
- if options.diagnostics: ui_type = 'basic' # enforce basic UI for --info
+ logging.warn(u'Using old interface name, consider using one '
+ 'of %s'% ', '.join(UI_LIST.keys()))
+ if options.diagnostics:
+ ui_type.uni = u'basic' # enforce basic UI for --info
# dry-run? Set [general]dry-run=True
if options.dryrun:
@@ -233,79 +272,96 @@ class OfflineImap:
config.set_if_not_exists('general', 'dry-run', 'False')
try:
- # create the ui class
- self.ui = UI_LIST[ui_type.lower()](config)
+ # Create the ui class.
+ self.ui = UI_LIST[ui_type.dbytes.lower()](config)
except KeyError:
- logging.error("UI '%s' does not exist, choose one of: %s"% \
- (ui_type, ', '.join(UI_LIST.keys())))
+ logging.error(u"UI '%s' does not exist, choose one of: %s"%
+ (ui_type.fs, ', '.join(UI_LIST.keys())))
sys.exit(1)
setglobalui(self.ui)
- #set up additional log files
+ # Set up additional log files.
if options.logfile:
- self.ui.setlogfile(options.logfile)
+ self.ui.setlogfile(fsString(options.logfile))
- #welcome blurb
+ # Welcome blurb.
self.ui.init_banner()
+ if options.use_unicode:
+ self.ui.info(u"*** Unicode support is enabled ***")
if options.debugtype:
self.ui.logger.setLevel(logging.DEBUG)
if options.debugtype.lower() == 'all':
options.debugtype = 'imap,maildir,thread'
- #force single threading?
+ # Force single threading?
if not ('thread' in options.debugtype.split(',') \
and not options.singlethreading):
- self.ui._msg("Debug mode: Forcing to singlethreaded.")
+ self.ui._msg(u"Debug mode: Forcing to singlethreaded.")
options.singlethreading = True
debugtypes = options.debugtype.split(',') + ['']
for dtype in debugtypes:
dtype = dtype.strip()
self.ui.add_debug(dtype)
- if dtype.lower() == u'imap':
+ if dtype.lower() == 'imap':
imaplib.Debug = 5
if options.runonce:
- # FIXME: spaghetti code alert!
+ # It's not possible to get that from config, instead?
+ # XXX: warn about what we do.
for section in accounts.getaccountlist(config):
config.remove_option('Account ' + section, "autorefresh")
if options.quick:
+ # It's not possible to get that from config, instead?
+ # XXX: warn about what we do.
for section in accounts.getaccountlist(config):
config.set('Account ' + section, "quick", '-1')
- #custom folder list specified?
+ # Custom folder list specified?
if options.folders:
- foldernames = options.folders.split(",")
- folderfilter = "lambda f: f in %s"% foldernames
+ # Each folder is filesystem encoded, change that.
+ foldernames = []
+ for fs_foldername in options.folders.split(","):
+ foldernames.append(fsString(fs_foldername).uni)
+
+ folderfilter = uniString(u"lambda f: f in %s"% foldernames)
folderincludes = "[]"
+ # accountname is full ASCII in unicode.
+ # It's not possible to get that from config, instead?
for accountname in accounts.getaccountlist(config):
- account_section = 'Account ' + accountname
- remote_repo_section = 'Repository ' + \
- config.get(account_section, 'remoterepository')
- config.set(remote_repo_section, "folderfilter", folderfilter)
+ uni_account_section = u'Account ' + accountname.uni
+ remoterepository = uniString(
+ config.get(uni_account_section, 'remoterepository'))
+ remote_repo_section = u'Repository ' + remoterepository.uni
+ config.set(remote_repo_section, u"folderfilter", folderfilter)
config.set(remote_repo_section, "folderincludes",
- folderincludes)
+ folderincludes)
if options.logfile:
- sys.stderr = self.ui.logfile
+ # sys.stderr expects filesystem encoding. The erasing should honor
+ # this encoding.
+ sys.stderr = codecs.open(self.ui.logfile, 'wb', uni.FS_ENCODING)
socktimeout = config.getdefaultint("general", "socktimeout", 0)
if socktimeout > 0:
socket.setdefaulttimeout(socktimeout)
- threadutil.initInstanceLimit('ACCOUNTLIMIT',
+ threadutil.initInstanceLimit(uniString(u'ACCOUNTLIMIT'),
config.getdefaultint('general', 'maxsyncaccounts', 1))
for reposname in config.getsectionlist('Repository'):
- for instancename in ["FOLDER_" + reposname,
- "MSGCOPY_" + reposname]:
+ # FIXME: What's that (FOLDER_, MSGCOPY_)?
+ # Boing! concatenation of str on list?
+ # ...Oh, yeah... forgot we have magic python for OfflineImap! :-D
+ for instancename in [u"FOLDER_" + reposname, u"MSGCOPY_" + reposname]:
if options.singlethreading:
- threadutil.initInstanceLimit(instancename, 1)
+ threadutil.initInstanceLimit(uniString(instancename), 1)
else:
- threadutil.initInstanceLimit(instancename,
+ threadutil.initInstanceLimit(uniString(instancename),
config.getdefaultint('Repository ' + reposname,
- 'maxconnections', 2))
+ 'maxconnections', 2))
+
self.config = config
return (options, args)
@@ -314,36 +370,47 @@ class OfflineImap:
self.config is supposed to have been correctly initialized
already."""
+
try:
- pidfd = open(self.config.getmetadatadir() + "/pid", "w")
+ pidfd = open(self.config.getmetadatadir().fs + "/pid", "w")
pidfd.write(str(os.getpid()) + "\n")
pidfd.close()
except:
pass
try:
- # Honor CLI --account option, only.
- # Accounts to sync are put into syncaccounts variable.
- activeaccounts = self.config.get("general", "accounts")
+ # Honor CLI --account option.
+ activeaccounts = uniString(self.config.get("general", "accounts"))
+ # Check all declared accounts are full ASCII.
+ # FIXME: put this into CustomConfigParser.
+ isASCII(activeaccounts.uni, exception_msg=
+ "configuration: non ASCII character in accounts")
if options.accounts:
- activeaccounts = options.accounts
+ activeaccounts = fsString(options.accounts)
+ # Remove whitespaces.
activeaccounts = activeaccounts.replace(" ", "")
+ # Make it a list with unicode values.
activeaccounts = activeaccounts.split(",")
+ # Dict of instanciated accounts with (unicode) account name as key.
+ # FIXME: why would we need to get instanciated accounts here? Hmm...
+ # ...Oh, yeah... forgot we have magic logic too! :-o
allaccounts = accounts.AccountHashGenerator(self.config)
+ # Accounts to sync are put into syncaccounts, a list of (unicode)
+ # values.
syncaccounts = []
- for account in activeaccounts:
- if account not in allaccounts:
+ for accountname in activeaccounts:
+ if accountname.uni not in allaccounts:
if len(allaccounts) == 0:
- errormsg = "The account '%s' does not exist because no" \
- " accounts are defined!"% account
+ uni_errormsg = (u"The account '%s' does not exist because no"
+ " accounts are defined!"% accountname.uni)
else:
- errormsg = "The account '%s' does not exist. Valid ac" \
- "counts are: %s"% \
- (account, ", ".join(allaccounts.keys()))
- self.ui.terminate(1, errormsg=errormsg)
- if account not in syncaccounts:
- syncaccounts.append(account)
+ uni_errormsg = (u"The account '%s' does not exist. Valid ac"
+ "counts are: %s"% (accountname.uni,
+ u", ".join([a.uni for a in allaccounts])))
+ self.ui.terminate(1, errormsg=uni_errormsg)
+ if accountname not in syncaccounts:
+ syncaccounts.append(accountname)
def sig_handler(sig, frame):
if sig == signal.SIGUSR1:
@@ -355,8 +422,8 @@ class OfflineImap:
accounts.Account.set_abort_event(self.config, 2)
elif sig in (signal.SIGTERM, signal.SIGINT, signal.SIGHUP):
# tell each account to ABORT ASAP (ctrl-c)
- getglobalui().warn("Terminating NOW (this may "\
- "take a few seconds)...")
+ getglobalui().warn("Terminating NOW (this may "
+ "take a few seconds)...")
accounts.Account.set_abort_event(self.config, 3)
elif sig == signal.SIGQUIT:
stacktrace.dump(sys.stderr)
@@ -369,44 +436,53 @@ class OfflineImap:
signal.signal(signal.SIGINT, sig_handler)
signal.signal(signal.SIGQUIT, sig_handler)
- #various initializations that need to be performed:
+ # Various initializations that need to be performed:
+ # XXX: is there any reason to no import mbnames in imports?
offlineimap.mbnames.init(self.config, syncaccounts)
if options.singlethreading:
- #singlethreaded
+ # singlethreaded
self.__sync_singlethreaded(syncaccounts)
else:
# multithreaded
+ # ExitNotifyThread is inherited from threading.Thread.
t = threadutil.ExitNotifyThread(target=syncmaster.syncitall,
- name='Sync Runner',
- kwargs = {'accounts': syncaccounts,
- 'config': self.config})
+ name='Sync Runner',
+ kwargs={'accounts': syncaccounts, 'config': self.config})
t.start()
threadutil.exitnotifymonitorloop(threadutil.threadexited)
self.ui.terminate()
except (SystemExit):
raise
except Exception as e:
+ # FIXME: UNICODE: This helps having good traces. There are some
+ # Exception with missing of full traces or other informations when
+ # not set. This should be improved.
+ if uni.use_unicode():
+ hack.inspect_crash()
+ raise
self.ui.error(e)
self.ui.terminate()
def __sync_singlethreaded(self, accs):
"""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."""
+
for accountname in accs:
account = offlineimap.accounts.SyncableAccount(self.config,
- accountname)
- threading.currentThread().name = "Account sync %s"% accountname
+ accountname)
+ threading.currentThread().name = "Account sync %s"% accountname.dbytes
account.syncrunner()
def __serverdiagnostics(self, options):
- activeaccounts = self.config.get("general", "accounts")
+ """Start server diagnostic for active accounts."""
+
+ activeaccounts = uniString(self.config.get("general", "accounts"))
if options.accounts:
- activeaccounts = options.accounts
+ activeaccounts = fsString(options.accounts)
activeaccounts = activeaccounts.split(",")
allaccounts = accounts.AccountListGenerator(self.config)
for account in allaccounts:
- if account.name not in activeaccounts: continue
+ if account.getname() not in activeaccounts: continue
account.serverdiagnostics()
diff --git a/offlineimap/localeval.py b/offlineimap/localeval.py
index a9494fb..2188fdd 100644
--- a/offlineimap/localeval.py
+++ b/offlineimap/localeval.py
@@ -17,6 +17,10 @@
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import imp
+import codecs
+
+from offlineimap.utils import uni
+from offlineimap.utils.uni import noneString
try:
import errno
except:
@@ -25,24 +29,31 @@ except:
class LocalEval:
"""Here is a powerfull but very dangerous option, of course."""
- def __init__(self, path=None):
+ def __init__(self, path=noneString):
self.namespace = {}
- if path is not None:
+ if path.uni is not None:
# FIXME: limit opening files owned by current user with rights set
# to fixed mode 644.
- foo = open(path, 'r')
+ if uni.use_unicode():
+ foo = codecs.open(path.fs, 'r', uni.ENCODING)
+ else:
+ foo = open(path.fs, 'r')
module = imp.load_module(
'<none>',
foo,
- path,
+ path.fs,
('', 'r', imp.PY_SOURCE))
for attr in dir(module):
self.namespace[attr] = getattr(module, attr)
def eval(self, text, namespace=None):
+ """eval() builtin use Latin-1 or UTF-8 arguments, also does this one.
+
+ :param: text is a uni object."""
+
names = {}
names.update(self.namespace)
if namespace is not None:
names.update(namespace)
- return eval(text, names)
+ return eval(text.uni, names)
diff --git a/offlineimap/mbnames.py b/offlineimap/mbnames.py
index 936a110..bf095d2 100644
--- a/offlineimap/mbnames.py
+++ b/offlineimap/mbnames.py
@@ -19,67 +19,113 @@
import os.path
import re # for folderfilter
from threading import Lock
+import codecs
+from offlineimap.utils import uni
+from offlineimap.utils.uni import uniString
+
+
+# Format of boxes is:
+# - keys: account names (uni objects)
+# - values: list of folder names (uni objects)
boxes = {}
+# Format of localroots is:
+# - keys: account names (uni objects)
+# - values: list of local repository roots (uni objects)
localroots = {}
-config = None
+# List of (unicode) (sync)-account names.
accounts = None
+
+config = None
mblock = Lock()
def init(conf, accts):
+ """Initialize mbnames.
+
+ :param accts: list of (unicode) (syncable) accounts.
+ """
+
global config, accounts
config = conf
accounts = accts
def add(accountname, foldername, localfolders):
+ """Add the infos to the boxes and localroots variables."""
+
if not accountname in boxes:
- boxes[accountname] = []
+ boxes[accountname] = [] # Add account name key if missing.
+ # XXX: Erase (?) local repository root.
localroots[accountname] = localfolders
if not foldername in boxes[accountname]:
+ # Add folder name to the list.
boxes[accountname].append(foldername)
def write():
# See if we're ready to write it out.
- for account in accounts:
- if account not in boxes:
+ for uni_accountname in accounts:
+ if uni_accountname not in boxes:
return
__genmbnames()
def __genmbnames():
- """Takes a configparser object and a boxlist, which is a list of hashes
- containing 'accountname' and 'foldername' keys."""
+ """Takes a configparser object and a boxlist, which is a list of dicts
+ containing 'accountname.uni' and 'foldername.uni' keys."""
xforms = [os.path.expanduser, os.path.expandvars]
mblock.acquire()
try:
- localeval = config.getlocaleval()
if not config.getdefaultboolean("mbnames", "enabled", 0):
return
- path = config.apply_xforms(config.get("mbnames", "filename"), xforms)
- file = open(path, "wt")
- file.write(localeval.eval(config.get("mbnames", "header")))
+
+ localeval = config.getlocaleval()
+ path = uniString(config.get("mbnames", "filename"))
+ path = uniString(config.apply_xforms(path, xforms))
+ if uni.use_unicode():
+ fd_mbnames = codecs.open(path.fs, "wt", uni.ENCODING)
+ else:
+ fd_mbnames = open(path.fs, "wt")
+
+ mbnames_header = uniString(config.get("mbnames", "header"))
+ # Write to file with default encoding.
+ fd_mbnames.write(localeval.eval(mbnames_header).dbytes)
+
+ # Default folderfilter.
folderfilter = lambda accountname, foldername: 1
+ # User folderfilter.
if config.has_option("mbnames", "folderfilter"):
- folderfilter = localeval.eval(config.get("mbnames", "folderfilter"),
- {'re': re})
+ mbnames_folderfilter = uniString(
+ config.get("mbnames", "folderfilter"))
+ folderfilter = localeval.eval(mbnames_folderfilter, {'re': re})
+
+ # Default sort_keyfunc.
mb_sort_keyfunc = lambda d: (d['accountname'], d['foldername'])
+ # User sort_keyfunc.
if config.has_option("mbnames", "sort_keyfunc"):
- mb_sort_keyfunc = localeval.eval(config.get("mbnames", "sort_keyfunc"),
- {'re': re})
+ mbnames_sort_keyfunc = uniString(
+ config.get("mbnames", "sort_keyfunc"))
+ mb_sort_keyfunc = localeval.eval(mbnames_sort_keyfunc, {'re': re})
+
itemlist = []
for accountname in boxes.keys():
- localroot = localroots[accountname]
+ localfolders = localroots[accountname]
for foldername in boxes[accountname]:
- if folderfilter(accountname, foldername):
- itemlist.append({'accountname': accountname,
- 'foldername': foldername,
- 'localfolders': localroot})
- itemlist.sort(key = mb_sort_keyfunc)
- format_string = config.get("mbnames", "peritem", raw=1)
- itemlist = [format_string % d for d in itemlist]
- file.write(localeval.eval(config.get("mbnames", "sep")).join(itemlist))
- file.write(localeval.eval(config.get("mbnames", "footer")))
- file.close()
+ if folderfilter(accountname.uni, foldername.uni):
+ itemlist.append({'accountname': accountname.uni,
+ 'foldername': foldername.uni,
+ 'localfolders': localfolders.uni})
+ itemlist.sort(key=mb_sort_keyfunc)
+ uni_format_string = config.get("mbnames", "peritem", raw=1)
+ itemlist = [uni_format_string % d for d in itemlist]
+
+ mbnames_sep = uniString(config.get("mbnames", "sep"))
+ mbnames_footer = uniString(config.get("mbnames", "footer"))
+
+ uni_eval_mbnames_sep = localeval.eval(mbnames_sep).join(itemlist)
+ uni_eval_mbnames_footer = localeval.eval(mbnames_footer)
+
+ fd_mbnames.write(uni_eval_mbnames_sep)
+ fd_mbnames.write(uni_eval_mbnames_footer)
+ fd_mbnames.close()
finally:
mblock.release()
diff --git a/offlineimap/repository/Base.py b/offlineimap/repository/Base.py
index 0cf44f8..76fc0e8 100644
--- a/offlineimap/repository/Base.py
+++ b/offlineimap/repository/Base.py
@@ -21,7 +21,10 @@ from sys import exc_info
from offlineimap import CustomConfig
from offlineimap.ui import getglobalui
+from offlineimap.utils.uni import fsString, uniString, noneString, use_unicode
from offlineimap.error import OfflineImapError
+from offlineimap.utils import hack
+
class BaseRepository(CustomConfig.ConfigHelperMixin, object):
@@ -33,33 +36,54 @@ class BaseRepository(CustomConfig.ConfigHelperMixin, object):
self.localeval = account.getlocaleval()
self._accountname = self.account.getname()
self._readonly = self.getconfboolean('readonly', False)
- self.uiddir = os.path.join(self.config.getmetadatadir(), 'Repository-' + self.name)
- if not os.path.exists(self.uiddir):
- os.mkdir(self.uiddir, 0o700)
- self.mapdir = os.path.join(self.uiddir, 'UIDMapping')
- if not os.path.exists(self.mapdir):
- os.mkdir(self.mapdir, 0o700)
+ metadatapath = uniString(self.config.getmetadatadir())
+ # FIXME: self.uiddir variable name is lying about itself.
+ self.uiddir = fsString(os.path.join(
+ metadatapath.fs, 'Repository-' + self.name.fs))
+ if not os.path.exists(self.uiddir.fs):
+ os.mkdir(self.uiddir.fs, 0o700)
+ self.mapdir = fsString(os.path.join(self.uiddir.fs, 'UIDMapping'))
+ if not os.path.exists(self.mapdir.fs):
+ os.mkdir(self.mapdir.fs, 0o700)
# FIXME: self.uiddir variable name is lying about itself.
- self.uiddir = os.path.join(self.uiddir, 'FolderValidity')
- if not os.path.exists(self.uiddir):
- os.mkdir(self.uiddir, 0o700)
+ self.uiddir = fsString(os.path.join(self.uiddir.fs, 'FolderValidity'))
+ if not os.path.exists(self.uiddir.fs):
+ os.mkdir(self.uiddir.fs, 0o700)
+ # Expects Unicode syntaxt u''.
self.nametrans = lambda foldername: foldername
+ # Expects Unicode syntaxt u''.
self.folderfilter = lambda foldername: 1
+ # Expects Unicode syntaxt u''.
self.folderincludes = []
+ # Expects Unicode syntaxt u''.
self.foldersort = None
if self.config.has_option(self.getsection(), 'nametrans'):
self.nametrans = self.localeval.eval(
- self.getconf('nametrans'), {'re': re})
+ uniString(self.getconf('nametrans')), {'re': re})
if self.config.has_option(self.getsection(), 'folderfilter'):
self.folderfilter = self.localeval.eval(
- self.getconf('folderfilter'), {'re': re})
+ uniString(self.getconf('folderfilter')), {'re': re})
if self.config.has_option(self.getsection(), 'folderincludes'):
- self.folderincludes = self.localeval.eval(
- self.getconf('folderincludes'), {'re': re})
+ folderincludes = self.localeval.eval(
+ uniString(self.getconf('folderincludes')), {'re': re})
+ self.folderincludes = [uniString(f) for f in folderincludes]
if self.config.has_option(self.getsection(), 'foldersort'):
self.foldersort = self.localeval.eval(
- self.getconf('foldersort'), {'re': re})
+ uniString(self.getconf('foldersort')), {'re': re})
+ # Check if user defined stuff don't even run.
+ if not use_unicode():
+ try:
+ for funcname in ['folderfilter', 'foldersort', 'nametrans']:
+ func = self.__dict__[funcname]
+ if func:
+ func('ascii')
+ for funcname in ['folderincludes']:
+ 'ascii' in self.__dict__[funcname]
+ except Exception as e:
+ raise Exception("function %s failed to run. Is there any "
+ "Unicode string in it?"% funcname)
+
def restore_atime(self):
"""Sets folders' atime back to their values after a sync
@@ -91,7 +115,8 @@ class BaseRepository(CustomConfig.ConfigHelperMixin, object):
return self.name
def __str__(self):
- return self.name
+ # Warn to not use this because encoding is made implicit.
+ return self.name.fs
@property
def accountname(self):
@@ -106,7 +131,7 @@ class BaseRepository(CustomConfig.ConfigHelperMixin, object):
# Interface from CustomConfig.ConfigHelperMixin
def getsection(self):
- return 'Repository ' + self.name
+ return uniString(u'Repository ' + self.name.uni).uni
# Interface from CustomConfig.ConfigHelperMixin
def getconfig(self):
@@ -135,7 +160,7 @@ class BaseRepository(CustomConfig.ConfigHelperMixin, object):
def should_sync_folder(self, fname):
"""Should this folder be synced?"""
- return fname in self.folderincludes or self.folderfilter(fname)
+ return fname.uni in self.folderincludes or self.folderfilter(fname)
def get_create_folders(self):
"""Is folder creation enabled on this repository?
@@ -148,12 +173,15 @@ class BaseRepository(CustomConfig.ConfigHelperMixin, object):
def makefolder(self, foldername):
"""Create a new folder."""
+
raise NotImplementedError
def deletefolder(self, foldername):
+
raise NotImplementedError
def getfolder(self, foldername):
+
raise NotImplementedError
def sync_folder_structure(self, dst_repo, status_repo):
@@ -179,43 +207,48 @@ class BaseRepository(CustomConfig.ConfigHelperMixin, object):
src_hash = {}
for folder in src_folders:
src_hash[folder.getvisiblename().replace(
- src_repo.getsep(), dst_repo.getsep())] = folder
+ src_repo.getsep().uni, dst_repo.getsep().uni)] = folder
dst_hash = {}
for folder in dst_folders:
dst_hash[folder.getvisiblename().replace(
- dst_repo.getsep(), src_repo.getsep())] = folder
+ dst_repo.getsep().uni, src_repo.getsep().uni)] = folder
# Find new folders on src_repo.
- for src_name_t, src_folder in src_hash.iteritems():
+ for uni_src_name_t, src_folder in src_hash.iteritems():
+ src_name_t = uniString(uni_src_name_t) # Fix type.
+
# Don't create on dst_repo, if it is readonly
if not dst_repo.get_create_folders():
break
- if src_folder.sync_this and not src_name_t in dst_folders:
+ if src_folder.sync_this and not src_name_t.uni in dst_hash:
try:
dst_repo.makefolder(src_name_t)
dst_haschanged = True # Need to refresh list
except OfflineImapError as e:
- self.ui.error(e, exc_info()[2],
- "Creating folder %s on repository %s"%
- (src_name_t, dst_repo))
+ self.ui.error(e, exc_info()[2],
+ u"Creating folder %s on repository %s"%
+ (src_name_t.uni, dst_repo.getname().uni))
raise
- status_repo.makefolder(src_name_t.replace(dst_repo.getsep(),
- status_repo.getsep()))
+ status_repo.makefolder(uniString(src_name_t.replace(
+ dst_repo.getsep(), status_repo.getsep())))
+
# Find new folders on dst_repo.
for dst_name_t, dst_folder in dst_hash.iteritems():
+
if not src_repo.get_create_folders():
# Don't create missing folder on readonly repo.
break
- if dst_folder.sync_this and not dst_name_t in src_folders:
+ if dst_folder.sync_this and not dst_name_t in src_hash:
# nametrans sanity check!
# Does nametrans back&forth lead to identical names?
# 1) would src repo filter out the new folder name? In this
# case don't create it on it:
if not self.should_sync_folder(dst_name_t):
- self.ui.debug('', "Not creating folder '%s' (repository '%s"
- "') as it would be filtered out on that repository."%
- (dst_name_t, self))
+ self.ui.debug('',
+ u"Not creating folder '%s' (repository '%s') as it "
+ "would be filtered out on that repository."%
+ (dst_name_t.uni, self.name.uni))
continue
# get IMAPFolder and see if the reverse nametrans
# works fine TODO: getfolder() works only because we
@@ -223,9 +256,10 @@ class BaseRepository(CustomConfig.ConfigHelperMixin, object):
# like to change. Take care!
folder = self.getfolder(dst_name_t)
# apply reverse nametrans to see if we end up with the same name
- newdst_name = folder.getvisiblename().replace(
- src_repo.getsep(), dst_repo.getsep())
- if dst_folder.name != newdst_name:
+ newdst_name = uniString(folder.getvisiblename().uni.replace(
+ src_repo.getsep().uni, dst_repo.getsep().uni))
+ #hack.debugger()
+ if dst_folder.getname().imap != newdst_name.imap:
raise OfflineImapError("INFINITE FOLDER CREATION DETECTED! "
"Folder '%s' (repository '%s') would be created as fold"
"er '%s' (repository '%s'). The latter becomes '%s' in "
@@ -234,23 +268,25 @@ class BaseRepository(CustomConfig.ConfigHelperMixin, object):
"itories so they lead to identical names if applied bac"
"k and forth. 2) Use folderfilter settings on a reposit"
"ory to prevent some folders from being created on the "
- "other side." % (dst_folder.name, dst_repo, dst_name_t,
- src_repo, newdst_name),
- OfflineImapError.ERROR.REPO)
+ "other side."% (dst_folder.getname().fs,
+ dst_repo.getname().fs, dst_name_t.fs,
+ src_repo.getname().fs, newdst_name.fs),
+ OfflineImapError.ERROR.REPO)
# end sanity check, actually create the folder
try:
src_repo.makefolder(dst_name_t)
src_haschanged = True # Need to refresh list
except OfflineImapError as e:
- self.ui.error(e, exc_info()[2], "Creating folder %s on "
- "repository %s" % (dst_name_t, src_repo))
+ self.ui.error(e, exc_info()[2],
+ u"Creating folder %s on repository %s"%
+ (dst_name_t.fs, src_repo.getname().fs))
raise
- status_repo.makefolder(dst_name_t.replace(
- src_repo.getsep(), status_repo.getsep()))
+ status_repo.makefolder(uniString(dst_name_t.replace(
+ src_repo.getsep(), status_repo.getsep())))
# Find deleted folders.
# TODO: We don't delete folders right now.
- #Forget old list of cached folders so we get new ones if needed
+ # Forget old list of cached folders so we get new ones if needed.
if src_haschanged:
self.forgetfolders()
if dst_haschanged:
@@ -268,5 +304,6 @@ class BaseRepository(CustomConfig.ConfigHelperMixin, object):
def getlocalroot(self):
""" Local root folder for storing messages.
Will not be set for remote repositories."""
- return None
+
+ return noneString()
diff --git a/offlineimap/repository/Gmail.py b/offlineimap/repository/Gmail.py
index 2e23e62..2ff67fd 100644
--- a/offlineimap/repository/Gmail.py
+++ b/offlineimap/repository/Gmail.py
@@ -17,6 +17,8 @@
from offlineimap.repository.IMAP import IMAPRepository
from offlineimap import folder, OfflineImapError
+from offlineimap.utils.uni import uniString
+
class GmailRepository(IMAPRepository):
"""Gmail IMAP repository.
@@ -24,6 +26,7 @@ class GmailRepository(IMAPRepository):
Falls back to hard-coded gmail host name and port, if none were specified:
http://mail.google.com/support/bin/answer.py?answer=78799&topic=12814
"""
+
# Gmail IMAP server hostname
HOSTNAME = "imap.gmail.com"
# Gmail IMAP server port
@@ -31,44 +34,49 @@ class GmailRepository(IMAPRepository):
def __init__(self, reposname, account):
"""Initialize a GmailRepository object."""
+
# Enforce SSL usage
- account.getconfig().set('Repository ' + reposname,
- 'ssl', 'yes')
+ account.getconfig().set(uniString(
+ 'Repository ' + reposname.dbytes, 'ssl', 'yes')).dbytes
IMAPRepository.__init__(self, reposname, account)
-
def gethost(self):
"""Return the server name to connect to.
Gmail implementation first checks for the usual IMAP settings
and falls back to imap.gmail.com if not specified."""
+
try:
return super(GmailRepository, self).gethost()
except OfflineImapError:
# nothing was configured, cache and return hardcoded one
- self._host = GmailRepository.HOSTNAME
+ self._host = uniString(GmailRepository.HOSTNAME)
return self._host
def getport(self):
+
return GmailRepository.PORT
def getssl(self):
+
return 1
def getpreauthtunnel(self):
+
return None
def getfolder(self, foldername):
- return self.getfoldertype()(self.imapserver, foldername,
- self)
+
+ return self.getfoldertype()(
+ self.imapserver, foldername, self)
def getfoldertype(self):
return folder.Gmail.GmailFolder
def gettrashfolder(self, foldername):
#: Where deleted mail should be moved
- return self.getconf('trashfolder','[Gmail]/Trash')
+ return uniString(self.getconf('trashfolder','[Gmail]/Trash'))
def getspamfolder(self):
#: Gmail also deletes messages upon EXPUNGE in the Spam folder
- return self.getconf('spamfolder','[Gmail]/Spam')
+ return uniString(self.getconf('spamfolder','[Gmail]/Spam'))
diff --git a/offlineimap/repository/IMAP.py b/offlineimap/repository/IMAP.py
index b109546..3ad8bb6 100644
--- a/offlineimap/repository/IMAP.py
+++ b/offlineimap/repository/IMAP.py
@@ -26,27 +26,33 @@ from offlineimap import folder, imaputil, imapserver, OfflineImapError
from offlineimap.folder.UIDMaps import MappedIMAPFolder
from offlineimap.threadutil import ExitNotifyThread
from offlineimap.utils.distro import get_os_sslcertfile, get_os_sslcertfile_searchpath
+from offlineimap.utils.uni import noneString, uniString, dbytesString, \
+ imapString, valueString, isASCII
+from offlineimap.utils import hack
class IMAPRepository(BaseRepository):
def __init__(self, reposname, account):
"""Initialize an IMAPRepository object."""
+
BaseRepository.__init__(self, reposname, account)
# self.ui is being set by the BaseRepository
- self._host = None
+ self._host = noneString()
self.imapserver = imapserver.IMAPServer(self)
self.folders = None
if self.getconf('sep', None):
- self.ui.info("The 'sep' setting is being ignored for IMAP "
- "repository '%s' (it's autodetected)"% self)
+ # FIXME: should warn, instead.
+ self.ui.info(u"The 'sep' setting is being ignored for IMAP "
+ "repository '%s' (it's autodetected)"% self.name.uni)
def startkeepalive(self):
keepalivetime = self.getkeepalive()
if not keepalivetime: return
self.kaevent = Event()
- self.kathread = ExitNotifyThread(target = self.imapserver.keepalive,
- name = "Keep alive " + self.getname(),
- args = (keepalivetime, self.kaevent))
+ self.kathread = ExitNotifyThread(
+ target=self.imapserver.keepalive,
+ name="Keep alive " + self.name.fs,
+ args=(keepalivetime, self.kaevent))
self.kathread.setDaemon(1)
self.kathread.start()
@@ -83,46 +89,50 @@ class IMAPRepository(BaseRepository):
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
+
+ delim = self.imapserver.getdelim()
+ assert delim.uni != None, ("'%s' "
+ "repository called getsep() before the folder separator was "
+ "queried from the server"% self.name.fs)
+ return delim
def gethost(self):
"""Return the configured hostname to connect to
:returns: hostname as string or throws Exception"""
- if self._host: # use cached value if possible
+
+ if self._host.uni: # Use cached value if possible.
return self._host
# 1) check for remotehosteval setting
if self.config.has_option(self.getsection(), 'remotehosteval'):
- host = self.getconf('remotehosteval')
+ host = noneString()
+ hosteval = uniString(self.getconf('remotehosteval'))
try:
- host = self.localeval.eval(host)
+ host = uniString(self.localeval.eval(hosteval))
except Exception as e:
raise OfflineImapError("remotehosteval option for repository "
- "'%s' failed:\n%s"% (self, e), OfflineImapError.ERROR.REPO), \
- None, exc_info()[2]
- if host:
+ "'%s' failed:\n%s"% (self.name.fs, e),
+ OfflineImapError.ERROR.REPO), None, exc_info()[2]
+ if host.uni:
self._host = host
return self._host
# 2) check for plain remotehost setting
- host = self.getconf('remotehost', None)
- if host != None:
+ host = uniString(self.getconf('remotehost', None))
+ if host.uni != None:
self._host = host
return self._host
# no success
- raise OfflineImapError("No remote host for repository "
- "'%s' specified."% self, OfflineImapError.ERROR.REPO)
+ raise OfflineImapError("No remote host for repository '%s' "
+ "specified."% self.name.fs, OfflineImapError.ERROR.REPO)
def get_remote_identity(self):
"""Remote identity is used for certain SASL mechanisms
(currently -- PLAIN) to inform server about the ID
we want to authorize as instead of our login name."""
- return self.getconf('remote_identity', default=None)
+ return uniString(self.getconf('remote_identity', default=None))
def get_auth_mechanisms(self):
supported = ["GSSAPI", "CRAM-MD5", "PLAIN", "LOGIN"]
@@ -132,58 +142,66 @@ class IMAPRepository(BaseRepository):
# TODO: due to the chosen-plaintext resistance.
default = ["GSSAPI", "CRAM-MD5", "PLAIN", "LOGIN"]
- mechs = self.getconflist('auth_mechanisms', r',\s*',
- default)
+ mechs = self.getconflist('auth_mechanisms', r',\s*', default)
for m in mechs:
+ isASCII(m, exception_msg=
+ "auth_mechanisms must be plain ASCII")
if m not in supported:
- raise OfflineImapError("Repository %s: "% self + \
- "unknown authentication mechanism '%s'"% m,
- OfflineImapError.ERROR.REPO)
+ raise OfflineImapError("Repository %s: unknown "
+ "authentication mechanism '%s'"%
+ self.name.fs, m, OfflineImapError.ERROR.REPO)
+
+ self.ui.debug('imap', u"Using authentication mechanisms %s"% mechs)
- self.ui.debug('imap', "Using authentication mechanisms %s" % mechs)
return mechs
def getuser(self):
- user = None
+ def getnetrcuser(path=None):
+ try:
+ if path:
+ netrcentry = netrc.netrc('/etc/netrc').authenticators(self.gethost())
+ else:
+ netrcentry = netrc.netrc().authenticators(self.gethost())
+ except IOError as inst:
+ if inst.errno != errno.ENOENT:
+ raise
+ else:
+ if netrcentry:
+ # Couldn't find the expected encoding of netrc. Assume it to
+ # be uni.ENCODING (UTF-8) because it's the less offending.
+ return dbytesString(netrcentry[0])
+ return noneString()
+
+ user = noneString()
localeval = self.localeval
if self.config.has_option(self.getsection(), 'remoteusereval'):
- user = self.getconf('remoteusereval')
- if user != None:
- return localeval.eval(user)
+ user = uniString(self.getconf('remoteusereval'))
+ if user.uni != None:
+ return uniString(localeval.eval(user))
- user = self.getconf('remoteuser')
- if user != None:
+ user = uniString(self.getconf('remoteuser'))
+ if user.uni != None:
return user
- try:
- netrcentry = netrc.netrc().authenticators(self.gethost())
- except IOError as inst:
- if inst.errno != errno.ENOENT:
- raise
- else:
- if netrcentry:
- return netrcentry[0]
+ user = getnetrcuser('/etc/netrc')
+ if user.dbytes != None:
+ return user
- try:
- netrcentry = netrc.netrc('/etc/netrc').authenticators(self.gethost())
- except IOError as inst:
- if inst.errno not in (errno.ENOENT, errno.EACCES):
- raise
- else:
- if netrcentry:
- return netrcentry[0]
+ user = getnetrcuser()
+ if user.dbytes != None:
+ return user
+ return user
def getport(self):
- port = None
-
if self.config.has_option(self.getsection(), 'remoteporteval'):
- port = self.getconf('remoteporteval')
- if port != None:
- return self.localeval.eval(port)
+ porteval = uniString(self.getconf('remoteporteval'))
+ if porteval.uni != None:
+ # XXX: we should be type checking of what eval() returns...
+ return self.localeval.eval(porteval)
return self.getconfint('remoteport', None)
@@ -192,11 +210,11 @@ class IMAPRepository(BaseRepository):
def getsslclientcert(self):
xforms = [os.path.expanduser, os.path.expandvars, os.path.abspath]
- return self.getconf_xform('sslclientcert', xforms, None)
+ return uniString(self.getconf_xform('sslclientcert', xforms, None))
def getsslclientkey(self):
xforms = [os.path.expanduser, os.path.expandvars, os.path.abspath]
- return self.getconf_xform('sslclientkey', xforms, None)
+ return uniString(self.getconf_xform('sslclientkey', xforms, None))
def getsslcacertfile(self):
"""Determines CA bundle.
@@ -217,52 +235,61 @@ class IMAPRepository(BaseRepository):
"""
xforms = [os.path.expanduser, os.path.expandvars, os.path.abspath]
- cacertfile = self.getconf_xform('sslcacertfile', xforms, None)
- if self.getconf('sslcacertfile', None) == "OS-DEFAULT":
- cacertfile = get_os_sslcertfile()
- if cacertfile == None:
+
+ cacertfile = uniString(self.getconf_xform(
+ 'sslcacertfile', xforms, None))
+ if uniString(self.getconf('sslcacertfile', None)).dbytes == "OS-DEFAULT":
+ cacertfile = dbytesString(get_os_sslcertfile())
+ if cacertfile.uni == None:
searchpath = get_os_sslcertfile_searchpath()
+ # searchpath is iterable of plain ASCII paths.
+ isASCII(', '.join(searchpath), exception_msg=
+ "OS-DEFAULT paths must be plain ASCII")
if searchpath:
reason = "Default CA bundle was requested, "\
"but no existing locations available. "\
- "Tried %s." % (", ".join(searchpath))
+ "Tried %s."% (", ".join(searchpath))
else:
reason = "Default CA bundle was requested, "\
"but OfflineIMAP doesn't know any for your "\
"current operating system."
raise OfflineImapError(reason, OfflineImapError.ERROR.REPO)
- if cacertfile is None:
- return None
- if not os.path.isfile(cacertfile):
+ if cacertfile.uni is None:
+ return noneString()
+ if not os.path.isfile(cacertfile.fs):
reason = "CA certfile for repository '%s' couldn't be found. "\
- "No such file: '%s'" % (self.name, cacertfile)
+ "No such file: '%s'"% (self.name.fs, cacertfile.fs)
raise OfflineImapError(reason, OfflineImapError.ERROR.REPO)
return cacertfile
def getsslversion(self):
- return self.getconf('ssl_version', None)
+ return uniString(self.getconf('ssl_version', None))
- def get_ssl_fingerprint(self):
- """Return array of possible certificate fingerprints.
+ def get_ssl_fingerprints(self):
+ """Return array of possible certificate fingerprints (type str).
Configuration item cert_fingerprint can contain multiple
comma-separated fingerprints in hex form."""
- value = self.getconf('cert_fingerprint', "")
+ # Encode back to what user actually wrote.
+ value = uniString(self.getconf('cert_fingerprint', "")).dbytes
return [f.strip().lower() for f in value.split(',') if f]
def getpreauthtunnel(self):
- return self.getconf('preauthtunnel', None)
+ return uniString(self.getconf('preauthtunnel', None))
def gettransporttunnel(self):
- return self.getconf('transporttunnel', None)
+ return uniString(self.getconf('transporttunnel', None))
def getreference(self):
- return self.getconf('reference', '')
+ return uniString(self.getconf('reference', '""'))
def getidlefolders(self):
+ """Return a list of idle folders."""
+
localeval = self.localeval
- return localeval.eval(self.getconf('idlefolders', '[]'))
+ # XXX: we should be type checking of what eval() returns...
+ return localeval.eval(uniString(self.getconf('idlefolders', '[]')))
def getmaxconnections(self):
num1 = len(self.getidlefolders())
@@ -286,45 +313,50 @@ class IMAPRepository(BaseRepository):
On success we return the password.
If all strategies fail we return None."""
+ def getnetrcpass(path):
+ try:
+ if path:
+ netrcentry = netrc.netrc('/etc/netrc').authenticators(
+ self.gethost())
+ else:
+ netrcentry = netrc.netrc().authenticators(self.gethost())
+ except IOError as inst:
+ if inst.errno != errno.ENOENT:
+ raise
+ else:
+ if netrcentry:
+ # Assume netrc use UTF-8 encoding. This has to be
+ # checked.
+ user = uniString(self.getconf('remoteuser'))
+ if user.uni == None or user.dbytes == netrcentry[0]:
+ return valueString(netrcentry[2])
+ return noneString()
+
# 1. evaluate Repository 'remotepasseval'
- passwd = self.getconf('remotepasseval', None)
- if passwd != None:
- return self.localeval.eval(passwd)
+ passwdeval = uniString(self.getconf('remotepasseval', None))
+ if passwdeval.value != None:
+ return valueString(self.localeval.eval(passwdeval))
# 2. read password from Repository 'remotepass'
- password = self.getconf('remotepass', None)
- if password != None:
- return password
+ password = uniString(self.getconf('remotepass', None))
+ if password.uni != None:
+ return valueString(password.dbytes)
# 3. read password from file specified in Repository 'remotepassfile'
- passfile = self.getconf('remotepassfile', None)
- if passfile != None:
- fd = open(os.path.expanduser(passfile))
- password = fd.readline().strip()
+ passfile = uniString(self.getconf('remotepassfile', None))
+ if passfile.uni != None:
+ fs_file = os.path.expanduser(passfile.fs)
+ fd = open(fs_file, 'rb')
+ password = dbytesString(fd.readline().strip())
fd.close()
- return password
+ return valueString(password.dbytes)
# 4. read password from ~/.netrc
- try:
- netrcentry = netrc.netrc().authenticators(self.gethost())
- except IOError as inst:
- if inst.errno != errno.ENOENT:
- raise
- else:
- if netrcentry:
- user = self.getconf('remoteuser')
- if user == None or user == netrcentry[0]:
- return netrcentry[2]
+ password = getnetrcpass()
+ if password.value is not None:
+ return password
# 5. read password from /etc/netrc
- try:
- netrcentry = netrc.netrc('/etc/netrc').authenticators(self.gethost())
- except IOError as inst:
- if inst.errno not in (errno.ENOENT, errno.EACCES):
- raise
- else:
- if netrcentry:
- user = self.getconf('remoteuser')
- if user == None or user == netrcentry[0]:
- return netrcentry[2]
- # no strategy yielded a password!
- return None
+ password = getnetrcpass('/etc/netrc')
+ if password.value is not None:
+ return password
+ return noneString()
def getfolder(self, foldername):
"""Return instance of OfflineIMAP representative folder."""
@@ -341,6 +373,7 @@ class IMAPRepository(BaseRepository):
def forgetfolders(self):
self.folders = None
+ # XXX: make it clear it returns instances and not names.
def getfolders(self):
"""Return a list of instances of OfflineIMAP representative folder."""
@@ -348,17 +381,17 @@ class IMAPRepository(BaseRepository):
return self.folders
retval = []
imapobj = self.imapserver.acquireconnection()
- # check whether to list all folders, or subscribed only
+ # Check whether to list all folders, or subscribed only.
listfunction = imapobj.list
if self.getconfboolean('subscribedonly', False):
listfunction = imapobj.lsub
try:
- listresult = listfunction(directory = self.imapserver.reference)[1]
+ ref = self.imapserver.reference
+ listresult = listfunction(directory=ref)[1]
finally:
self.imapserver.releaseconnection(imapobj)
for s in listresult:
- if s == None or \
- (isinstance(s, basestring) and s == ''):
+ if s == None or (isinstance(s, basestring) and s == ''):
# Bug in imaplib: empty strings in results from
# literals. TODO: still relevant?
continue
@@ -366,22 +399,22 @@ class IMAPRepository(BaseRepository):
flaglist = [x.lower() for x in imaputil.flagsplit(flags)]
if '\\noselect' in flaglist:
continue
- foldername = imaputil.dequote(name)
- retval.append(self.getfoldertype()(self.imapserver, foldername,
- self))
- # Add all folderincludes
+ foldername = imapString(imaputil.dequote(name))
+ retval.append(
+ self.getfoldertype()(self.imapserver, foldername, self))
+ # Add all folderincludes.
if len(self.folderincludes):
imapobj = self.imapserver.acquireconnection()
try:
for foldername in self.folderincludes:
try:
- imapobj.select(foldername, readonly = True)
+ imapobj.select(foldername.imap, readonly=True)
except OfflineImapError as e:
- # couldn't select this folderinclude, so ignore folder.
+ # Couldn't select this folderinclude, so ignore folder.
if e.severity > OfflineImapError.ERROR.FOLDER:
raise
self.ui.error(e, exc_info()[2],
- 'Invalid folderinclude:')
+ 'Invalid folderinclude: %s'% foldername.fs)
continue
retval.append(self.getfoldertype()(
self.imapserver, foldername, self))
@@ -389,10 +422,10 @@ class IMAPRepository(BaseRepository):
self.imapserver.releaseconnection(imapobj)
if self.foldersort is None:
- # default sorting by case insensitive transposed name
- retval.sort(key=lambda x: str.lower(x.getvisiblename()))
+ # Default sorting by case insensitive transposed name.
+ retval.sort(key=lambda x: x.getvisiblename().lower())
else:
- # do foldersort in a python3-compatible way
+ # Do foldersort in a python3-compatible way.
# http://bytes.com/topic/python/answers/844614-python-3-sorting-comparison-function
def cmp2key(mycmp):
"""Converts a cmp= function into a key= function
@@ -417,20 +450,20 @@ class IMAPRepository(BaseRepository):
:param foldername: Full path of the folder to be created."""
- if self.getreference():
+ if self.getreference().uni:
foldername = self.getreference() + self.getsep() + foldername
- if not foldername: # Create top level folder as folder separator
+ if not foldername.uni: # Create top level folder as folder separator.
foldername = self.getsep()
- self.ui.makefolder(self, foldername)
+ self.ui.makefolder(self, foldername.uni)
if self.account.dryrun:
return
imapobj = self.imapserver.acquireconnection()
try:
- result = imapobj.create(foldername)
+ result = imapobj.create(foldername.imap)
if result[0] != 'OK':
raise OfflineImapError("Folder '%s'[%s] could not be created. "
- "Server responded: %s"% (foldername, self, str(result)),
- OfflineImapError.ERROR.FOLDER)
+ "Server responded: %s"% (foldername.imap, self.name.fs,
+ str(result)), OfflineImapError.ERROR.FOLDER)
finally:
self.imapserver.releaseconnection(imapobj)
diff --git a/offlineimap/repository/LocalStatus.py b/offlineimap/repository/LocalStatus.py
index fc34a55..6a89a6f 100644
--- a/offlineimap/repository/LocalStatus.py
+++ b/offlineimap/repository/LocalStatus.py
@@ -20,6 +20,8 @@ import os
from offlineimap.folder.LocalStatus import LocalStatusFolder
from offlineimap.folder.LocalStatusSQLite import LocalStatusSQLiteFolder
from offlineimap.repository.Base import BaseRepository
+from offlineimap.utils.uni import uniString, fsString
+
class LocalStatusRepository(BaseRepository):
def __init__(self, reposname, account):
@@ -29,32 +31,35 @@ class LocalStatusRepository(BaseRepository):
self.backends = {}
self.backends['sqlite'] = {
'class': LocalStatusSQLiteFolder,
- 'root': os.path.join(account.getaccountmeta(), 'LocalStatus-sqlite')
+ 'root': fsString(os.path.join(
+ account.getaccountmeta().fs, 'LocalStatus-sqlite'))
}
self.backends['plain'] = {
'class': LocalStatusFolder,
- 'root': os.path.join(account.getaccountmeta(), 'LocalStatus')
+ 'root': fsString(os.path.join(
+ account.getaccountmeta().fs, 'LocalStatus'))
}
- # Set class and root for the configured backend
- self.setup_backend(self.account.getconf('status_backend', 'plain'))
+ # Set class and root for the configured backend.
+ self.setup_backend(uniString(
+ self.account.getconf('status_backend', 'plain')))
- if not os.path.exists(self.root):
- os.mkdir(self.root, 0o700)
+ if not os.path.exists(self.root.fs):
+ os.mkdir(self.root.fs, 0o700)
# self._folders is a dict of name:LocalStatusFolders()
self._folders = {}
def setup_backend(self, backend):
- if backend in self.backends.keys():
+ if backend.dbytes in self.backends.keys():
self._backend = backend
- self.root = self.backends[backend]['root']
- self.LocalStatusFolderClass = self.backends[backend]['class']
+ self.root = self.backends[backend.dbytes]['root']
+ self.LocalStatusFolderClass = self.backends[backend.dbytes]['class']
else:
raise SyntaxWarning("Unknown status_backend '%s' for account '%s'"%
- (backend, self.account.name))
+ (backend.fs, self.account.getname().fs))
def import_other_backend(self, folder):
for bk, dic in self.backends.items():
@@ -62,15 +67,16 @@ class LocalStatusRepository(BaseRepository):
if dic['class'] == type(folder):
continue
- repobk = LocalStatusRepository(self.name, self.account)
+ repobk = LocalStatusRepository(self.getname(), self.account)
repobk.setup_backend(bk) # fake the backend
- folderbk = dic['class'](folder.name, repobk)
+ folderbk = dic['class'](folder.getname(), repobk)
# if backend contains data, import it to folder.
if not folderbk.isnewfolder():
- self.ui._msg('Migrating LocalStatus cache from %s to %s " \
- "status folder for %s:%s'%
- (bk, self._backend, self.name, folder.name))
+ self.ui._msg(u"Migrating LocalStatus cache from %s to %s "
+ "status folder for %s:%s"%
+ (bk.getname().uni, self._backend.uni,
+ self.name.uni, folder.getname().uni))
folderbk.cachemessagelist()
folder.messagelist = folderbk.messagelist
@@ -78,7 +84,7 @@ class LocalStatusRepository(BaseRepository):
break
def getsep(self):
- return '.'
+ return uniString(u'.')
def makefolder(self, foldername):
"""Create a LocalStatus Folder."""
@@ -88,7 +94,22 @@ class LocalStatusRepository(BaseRepository):
# Create an empty StatusFolder
folder = self.LocalStatusFolderClass(foldername, self)
- folder.save()
+ # With Unicode, makefolder() might be raised while cache file already
+ # exists. Calling makefolder() let us update the folder name on disk in
+ # a Maildir if the folder name is not full ASCII.
+ #
+ # Avoid clearing the folder cache file if it already exists. We don't
+ # want to re-download emails from that folder if we already have a cache
+ # file.
+ #
+ # Make the check here to not annoy callers with this issue and because
+ # we can't insert it in the save() method simply (missing of context).
+ #
+ # FIXME: why this call to save() in the first place? We have NOTHING to
+ # save at all. This will only create the cache file which case should be
+ # handled from within the class...
+ if folder.isnewfolder():
+ folder.save()
# Invalidate the cache.
self.forgetfolders()
@@ -96,8 +117,8 @@ class LocalStatusRepository(BaseRepository):
def getfolder(self, foldername):
"""Return the Folder() object for a foldername."""
- if foldername in self._folders:
- return self._folders[foldername]
+ if foldername.uni in self._folders:
+ return self._folders[foldername.uni]
folder = self.LocalStatusFolderClass(foldername, self)
@@ -105,7 +126,7 @@ class LocalStatusRepository(BaseRepository):
if folder.isnewfolder():
self.import_other_backend(folder)
- self._folders[foldername] = folder
+ self._folders[foldername.uni] = folder
return folder
def getfolders(self):
diff --git a/offlineimap/repository/Maildir.py b/offlineimap/repository/Maildir.py
index 0262ba2..d07f0be 100644
--- a/offlineimap/repository/Maildir.py
+++ b/offlineimap/repository/Maildir.py
@@ -15,12 +15,17 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+import os
+from stat import *
+
from offlineimap import folder
from offlineimap.ui import getglobalui
from offlineimap.error import OfflineImapError
from offlineimap.repository.Base import BaseRepository
-import os
-from stat import *
+from offlineimap.utils import uni
+from offlineimap.utils.uni import isASCII, fsString, uniString, noneString
+from offlineimap.utils import hack
+
class MaildirRepository(BaseRepository):
def __init__(self, reposname, account):
@@ -32,20 +37,25 @@ class MaildirRepository(BaseRepository):
self.root = self.getlocalroot()
self.folders = None
self.ui = getglobalui()
- self.debug("MaildirRepository initialized, sep is %s"% repr(self.getsep()))
+ self.debug(u"MaildirRepository initialized, sep is %s"% repr(self.getsep()))
self.folder_atimes = []
# Create the top-level folder if it doesn't exist
- if not os.path.isdir(self.root):
- os.mkdir(self.root, 0o700)
+ if not os.path.isdir(self.root.fs):
+ os.mkdir(self.root.fs, 0o700)
def _append_folder_atimes(self, foldername):
- """Store the atimes of a folder's new|cur in self.folder_atimes"""
+ """Store the atimes of a folder's new|cur in self.folder_atimes.
+
+ :params foldername: filsystem encoded."""
- p = os.path.join(self.root, foldername)
- new = os.path.join(p, 'new')
- cur = os.path.join(p, 'cur')
- atimes = (p, os.path.getatime(new), os.path.getatime(cur))
+ # self.root is already encoded... It's a path.
+ # We assume foldername is filesystem encoded.
+ p = fsString(os.path.join(self.root.fs, foldername.fs))
+ fs_new = os.path.join(p.fs, 'new')
+ fs_cur = os.path.join(p.fs, 'cur')
+ # Later use of the path expects filesystem encoding.
+ atimes = (p, os.path.getatime(fs_new), os.path.getatime(fs_cur))
self.folder_atimes.append(atimes)
def restore_atime(self):
@@ -57,20 +67,24 @@ class MaildirRepository(BaseRepository):
return # not configured to restore
for (dirpath, new_atime, cur_atime) in self.folder_atimes:
- new_dir = os.path.join(dirpath, 'new')
- cur_dir = os.path.join(dirpath, 'cur')
- os.utime(new_dir, (new_atime, os.path.getmtime(new_dir)))
- os.utime(cur_dir, (cur_atime, os.path.getmtime(cur_dir)))
+ # dirpath is already filesystem encoded, see
+ # _append_folder_atimes()
+ fs_new_dir = os.path.join(dirpath.fs, 'new')
+ fs_cur_dir = os.path.join(dirpath.fs, 'cur')
+ os.utime(fs_new_dir, (new_atime, os.path.getmtime(fs_new_dir)))
+ os.utime(fs_cur_dir, (cur_atime, os.path.getmtime(fs_cur_dir)))
def getlocalroot(self):
xforms = [os.path.expanduser]
- return self.getconf_xform('localfolders', xforms)
+ localroot = uniString(self.getconf_xform('localfolders', xforms))
+ # encode local root path as soon as possible.
+ return localroot
def debug(self, msg):
self.ui.debug('maildir', msg)
def getsep(self):
- return self.getconf('sep', '.').strip()
+ return uniString(self.getconf('sep', '.').strip())
def makefolder(self, foldername):
"""Create new Maildir folder if necessary
@@ -79,48 +93,58 @@ class MaildirRepository(BaseRepository):
need to invoke :meth:`forgetfolders` to force new caching when
you are done creating folders yourself.
- :param foldername: A relative mailbox name. The maildir will be
- created in self.root+'/'+foldername. All intermediate folder
- levels will be created if they do not exist yet. 'cur',
- 'tmp', and 'new' subfolders will be created in the maildir.
+ :param foldername: A relative mailbox name (unicode).
+ The maildir will be created in self.root+'/'+foldername.
+ All intermediate folder levels will be created if they do
+ not exist yet. 'cur', 'tmp', and 'new' subfolders will be
+ created in the maildir.
"""
- self.ui.makefolder(self, foldername)
+ self.ui.makefolder(self, foldername.uni)
if self.account.dryrun:
return
- full_path = os.path.abspath(os.path.join(self.root, foldername))
# sanity tests
- if self.getsep() == '/':
+ if self.getsep().uni == '/':
for component in foldername.split('/'):
- assert not component in ['new', 'cur', 'tmp'],\
+ assert not component.uni in ['new', 'cur', 'tmp'],\
"When using nested folders (/ as a Maildir separator), "\
"folder names may not contain 'new', 'cur', 'tmp'."
- assert foldername.find('../') == -1, "Folder names may not contain ../"
- assert not foldername.startswith('/'), "Folder names may not begin with /"
+ assert foldername.find(u'../') == -1, "Folder names may not contain ../"
+ assert not foldername.startswith(u'/'), "Folder names may not begin with /"
+
+ diverged, fs_unexpected, fs_expected = uni.diverged_foldernames(
+ foldername)
+ if diverged:
+ uni.rename_diverged(self.root, fs_unexpected, fs_expected)
+ self.debug(u" makefolder : renamed '%s' to '%s'"%
+ (fs_unexpected.uni, fsString(fs_expected).uni))
+
+ full_path = fsString(os.path.abspath(os.path.join(
+ self.root.fs, fs_expected)))
# If we're using hierarchical folders, it's possible that
# sub-folders may be created before higher-up ones.
- self.debug("makefolder: calling makedirs '%s'"% full_path)
+ self.debug(u"makefolder: calling makedirs '%s'"% full_path.uni)
try:
- os.makedirs(full_path, 0o700)
+ os.makedirs(full_path.fs, 0o700)
except OSError as e:
- if e.errno == 17 and os.path.isdir(full_path):
- self.debug("makefolder: '%s' already a directory"% foldername)
+ if e.errno == 17 and os.path.isdir(full_path.fs):
+ self.debug(u"makefolder: '%s' already a directory"% foldername.fs)
else:
raise
- for subdir in ['cur', 'new', 'tmp']:
+ for fs_subdir in ['cur', 'new', 'tmp']:
try:
- os.mkdir(os.path.join(full_path, subdir), 0o700)
+ os.mkdir(os.path.join(full_path.fs, fs_subdir), 0o700)
except OSError as e:
if e.errno == 17 and os.path.isdir(full_path):
- self.debug("makefolder: '%s' already has subdir %s"%
- (foldername, subdir))
+ self.debug(u"makefolder: '%s' already has subdir %s"%
+ (foldername.uni, fs_subdir))
else:
raise
def deletefolder(self, foldername):
- self.ui.warn("NOT YET IMPLEMENTED: DELETE FOLDER %s"% foldername)
+ self.ui.warn(u"NOT YET IMPLEMENTED: DELETE FOLDER %s"% foldername.uni)
def getfolder(self, foldername):
"""Return a Folder instance of this Maildir
@@ -129,69 +153,92 @@ class MaildirRepository(BaseRepository):
we only return existing folders and that 2 calls with the same
name will return the same object."""
- # getfolders() will scan and cache the values *if* necessary
+ # getfolders() will scan and cache the values *if* necessary.
folders = self.getfolders()
for f in folders:
- if foldername == f.name:
+ if foldername.uni == f.name:
return f
raise OfflineImapError("getfolder() asked for a nonexisting "
- "folder '%s'."% foldername,
- OfflineImapError.ERROR.FOLDER)
+ "folder '%s'."% foldername.fs, OfflineImapError.ERROR.FOLDER)
def _getfolders_scandir(self, root, extension=None):
"""Recursively scan folder 'root'; return a list of MailDirFolder
- :param root: (absolute) path to Maildir root
- :param extension: (relative) subfolder to examine within root"""
+ :param root: (absolute) path to Maildir root, uni object.
+ :param extension: (relative) subfolder to examine within root, uni
+ object.
+ """
- self.debug("_GETFOLDERS_SCANDIR STARTING. root = %s, extension = %s"%
- (root, extension))
+ if type(extension) != type(noneString()):
+ extension = noneString()
+ self.debug(u"_GETFOLDERS_SCANDIR STARTING. root = %s, extension = %s"%
+ (root.uni, extension.uni))
retval = []
# Configure the full path to this repository -- "toppath"
- if extension:
- toppath = os.path.join(root, extension)
+ if extension.uni:
+ toppath = fsString(os.path.join(root.fs, extension.fs))
else:
toppath = root
- self.debug(" toppath = %s"% toppath)
+ self.debug(u" toppath = %s"% (toppath.uni))
# Iterate over directories in top & top itself.
- for dirname in os.listdir(toppath) + ['']:
- self.debug(" dirname = %s"% dirname)
- if dirname == '' and extension is not None:
- self.debug(' skip this entry (already scanned)')
+ # os.listdir() returns str when the path is str and unicode if the path
+ # is unicode. Here, we assume filsystem encoding.
+ for fs_dirname in os.listdir(toppath.fs) + ['']:
+ # Sanity check for legacy mode.
+ if not uni.use_unicode() and not isASCII(fs_dirname):
+ self.debug(u" skip dirname (unexpected character) = %s"%
+ dirname.std)
continue
- if dirname in ['cur', 'new', 'tmp']:
- self.debug(" skip this entry (Maildir special)")
+
+ dirname = fsString(fs_dirname)
+ if dirname.fs == '' and extension.uni is not None:
+ self.debug(u' skip this entry (already scanned)')
+ continue
+
+ # If we run without Unicode support, folders are IMAP UTF-7 encoded
+ # (full ASCII).
+ if dirname.fs != '':
+ diverged, fs_unexpected, fs_expected = uni.diverged_foldernames(dirname)
+ if diverged:
+ uni.rename_diverged(toppath, fs_unexpected, fs_expected)
+ self.debug(u" scandir folders: renamed '%s' to '%s'"%
+ (fsString(fs_unexpected).uni, fsString(fs_expected).uni))
+
+ self.debug(u" dirname = %s"% dirname.uni)
+ if dirname.fs in ['cur', 'new', 'tmp']:
+ self.debug(u" skip this entry (Maildir special)")
# Bypass special files.
continue
- fullname = os.path.join(toppath, dirname)
- if not os.path.isdir(fullname):
- self.debug(" skip this entry (not a directory)")
+
+ fullname = fsString(os.path.join(toppath.fs, dirname.fs))
+ if not os.path.isdir(fullname.fs):
+ self.debug(u" skip this entry (not a directory)")
# Not a directory -- not a folder.
continue
# extension can be None.
- if extension:
- foldername = os.path.join(extension, dirname)
+ if extension.uni:
+ foldername = fsString(os.path.join(extension.fs, dirname.fs))
else:
foldername = dirname
- if (os.path.isdir(os.path.join(fullname, 'cur')) and
- os.path.isdir(os.path.join(fullname, 'new')) and
- os.path.isdir(os.path.join(fullname, 'tmp'))):
+ if (os.path.isdir(os.path.join(fullname.fs, 'cur')) and
+ os.path.isdir(os.path.join(fullname.fs, 'new')) and
+ os.path.isdir(os.path.join(fullname.fs, 'tmp'))):
# This directory has maildir stuff -- process
- self.debug(" This is maildir folder '%s'."% foldername)
+ self.debug(u" This is maildir folder '%s'."% foldername.uni)
if self.getconfboolean('restoreatime', False):
self._append_folder_atimes(foldername)
fd = self.getfoldertype()(self.root, foldername,
- self.getsep(), self)
+ self.getsep(), self)
retval.append(fd)
if self.getsep() == '/' and dirname != '':
# Recursively check sub-directories for folders too.
retval.extend(self._getfolders_scandir(root, foldername))
- self.debug("_GETFOLDERS_SCANDIR RETURNING %s"% \
- repr([x.getname() for x in retval]))
+ self.debug(u"_GETFOLDERS_SCANDIR RETURNING %s"%
+ repr([x.getname().uni for x in retval]))
return retval
def getfolders(self):
diff --git a/offlineimap/repository/__init__.py b/offlineimap/repository/__init__.py
index 0fbbc13..e726e2f 100644
--- a/offlineimap/repository/__init__.py
+++ b/offlineimap/repository/__init__.py
@@ -28,6 +28,8 @@ from offlineimap.repository.Maildir import MaildirRepository
from offlineimap.repository.GmailMaildir import GmailMaildirRepository
from offlineimap.repository.LocalStatus import LocalStatusRepository
from offlineimap.error import OfflineImapError
+from offlineimap.utils.uni import uniString, isASCII
+from offlineimap.utils import hack
class Repository(object):
@@ -41,43 +43,46 @@ class Repository(object):
:param regtype: 'remote', 'local', or 'status'"""
if reqtype == 'remote':
- name = account.getconf('remoterepository')
+ name = uniString(account.getconf('remoterepository'))
# We don't support Maildirs on the remote side.
typemap = {'IMAP': IMAPRepository,
'Gmail': GmailRepository}
elif reqtype == 'local':
- name = account.getconf('localrepository')
+ name = uniString(account.getconf('localrepository'))
typemap = {'IMAP': MappedIMAPRepository,
'Maildir': MaildirRepository,
'GmailMaildir': GmailMaildirRepository}
elif reqtype == 'status':
# create and return a LocalStatusRepository.
- name = account.getconf('localrepository')
+ name = uniString(account.getconf('localrepository'))
return LocalStatusRepository(name, account)
else:
- errstr = "Repository type %s not supported" % reqtype
+ errstr = "Repository type %s not supported"% reqtype
raise OfflineImapError(errstr, OfflineImapError.ERROR.REPO)
# Get repository type.
config = account.getconfig()
try:
- repostype = config.get('Repository ' + name, 'type').strip()
+ repostype = uniString(config.get(
+ 'Repository ' + name.uni, 'type').strip())
+ isASCII(repostype.uni, exception_msg=
+ "repository type must be plain ASCII")
except NoSectionError as e:
- errstr = ("Could not find section '%s' in configuration. Required "
- "for account '%s'." % ('Repository %s' % name, account))
- raise OfflineImapError(errstr, OfflineImapError.ERROR.REPO), \
- None, exc_info()[2]
+ errstr = ("Could not find section Repository '%s' in configuration."
+ " Required for account '%s'."% (name.fs, account.getname().fs))
+ raise OfflineImapError(
+ errstr, OfflineImapError.ERROR.REPO), None, exc_info()[2]
try:
- repo = typemap[repostype]
+ repo = typemap[repostype.dbytes]
except KeyError:
- errstr = "'%s' repository not supported for '%s' repositories."% \
- (repostype, reqtype)
- raise OfflineImapError(errstr, OfflineImapError.ERROR.REPO), \
- None, exc_info()[2]
+ errstr = ("'%s' repository not supported for '%s' repositories."%
+ (repostype.fs, reqtype))
+ raise OfflineImapError(
+ errstr, OfflineImapError.ERROR.REPO), None, exc_info()[2]
return repo(name, account)
@@ -87,6 +92,6 @@ class Repository(object):
executed instead of this stub
:param account: :class:`Account`
- :param regtype: 'remote', 'local', or 'status'
+ :param regtype: uni object of 'remote', 'local', or 'status'
"""
pass
diff --git a/offlineimap/syncmaster.py b/offlineimap/syncmaster.py
index 5fd0dee..e6a4648 100644
--- a/offlineimap/syncmaster.py
+++ b/offlineimap/syncmaster.py
@@ -16,24 +16,34 @@
# 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 currentThread
+
from offlineimap.threadutil import threadlist, InstanceLimitedThread
from offlineimap.accounts import SyncableAccount
-from threading import currentThread
+from offlineimap.utils.uni import uniString
+
def syncaccount(threads, config, accountname):
account = SyncableAccount(config, accountname)
- thread = InstanceLimitedThread(instancename = 'ACCOUNTLIMIT',
- target = account.syncrunner,
- name = "Account sync %s" % accountname)
+ thread = InstanceLimitedThread(
+ instancename=uniString(u'ACCOUNTLIMIT'), # Not redirected to Thread.
+ target=account.syncrunner,
+ name="Account sync %s"% accountname.std)
thread.setDaemon(True)
+ # Actually start thread.
thread.start()
+ # Add started thread to the list.
threads.add(thread)
def syncitall(accounts, config):
- # Special exit message for SyncRunner thread, so main thread can exit
+ # Special exit message for SyncRunner thread, so main thread can exit.
currentThread().exit_message = 'SYNCRUNNER_EXITED_NORMALLY'
+ # Retrieve all prepared threads.
threads = threadlist()
- for accountname in accounts:
- syncaccount(threads, config, accountname)
+ # accounts is a list of (unicode) names.
+ for uni_accountname in accounts:
+ syncaccount(threads, config, uniString(uni_accountname))
# Wait for the threads to finish.
+ # FIXME: should actually be called 'join' (much more undersantable), instead
+ # of 'reset'.
threads.reset()
diff --git a/offlineimap/threadutil.py b/offlineimap/threadutil.py
index f69f8a6..294a45b 100644
--- a/offlineimap/threadutil.py
+++ b/offlineimap/threadutil.py
@@ -15,7 +15,7 @@
# 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 Lock, Thread, BoundedSemaphore, currentThread
+from threading import Lock, Thread, BoundedSemaphore
try:
from Queue import Queue, Empty
except ImportError: # python3
@@ -23,8 +23,14 @@ except ImportError: # python3
import traceback
import os.path
import sys
+
from offlineimap.ui import getglobalui
+#
+# UNICODE: Thread library has ASCII expectations.
+#
+
+
######################################################################
# General utilities
######################################################################
@@ -70,6 +76,7 @@ class threadlist:
finally:
self.lock.release()
+ # Fix name.
def reset(self):
while 1:
thread = self.pop()
@@ -109,11 +116,11 @@ def exitnotifymonitorloop(callback):
# 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
# Queue will raise `Empty`.
- thrd = exitthreads.get(True, 60)
+ thrd = exitthreads.get(True, 10)
# request to abort when callback returns true
do_loop = (callback(thrd) != True)
except Empty:
- pass
+ pass # ???
def threadexited(thread):
"""Called when a thread exits.
@@ -133,12 +140,12 @@ def threadexited(thread):
return True
else:
ui.threadExited(thread)
- return False
+ return False
class ExitNotifyThread(Thread):
"""This class is designed to alert a "monitor" to the fact that a
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
+ 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
@@ -171,8 +178,8 @@ class ExitNotifyThread(Thread):
prof = prof.runctx("Thread.run(self)", globals(), locals())
except SystemExit:
pass
- prof.dump_stats(os.path.join(ExitNotifyThread.profiledir,
- "%s_%s.prof"% (self.ident, self.getName())))
+ prof.dump_stats(os.path.join(ExitNotifyThread.profiledir.fs,
+ "%s_%s.prof"% (self.ident.fs, self.getName().fs)))
except Exception as e:
# Thread exited with Exception, store it
tb = traceback.format_exc()
@@ -218,7 +225,9 @@ instancelimitedlock = Lock()
def initInstanceLimit(instancename, instancemax):
"""Initialize the instance-limited thread implementation to permit
- up to intancemax threads with the given instancename."""
+ up to intancemax threads with the given instancename.
+
+ :param instancename: instance name (uni object)."""
instancelimitedlock.acquire()
if not instancename in instancelimitedsems:
diff --git a/offlineimap/ui/Curses.py b/offlineimap/ui/Curses.py
index ddc05ea..e4214ab 100644
--- a/offlineimap/ui/Curses.py
+++ b/offlineimap/ui/Curses.py
@@ -25,6 +25,8 @@ import logging
from offlineimap.ui.UIBase import UIBase
from offlineimap.threadutil import ExitNotifyThread
+from offlineimap.utils import uni
+from offlineimap.utils.uni import fsString
import offlineimap
@@ -180,7 +182,8 @@ class CursesAccountFrame:
# if this belongs to an Account (and not *Control), set the
# skipsleep pref
if isinstance(self.account, offlineimap.accounts.Account):
- self.ui.info("Requested synchronization for acc: %s"% self.account)
+ self.ui.info(u"Requested synchronization for acc: %s"%
+ self.account)
self.account.config.set('Account %s'% self.account.name,
'skipsleep', '1')
@@ -348,7 +351,12 @@ class Blinkenlights(UIBase, CursesUtil):
ch = CursesLogHandler()
#ch.setLevel(logging.DEBUG)
# create formatter and add it to the handlers
- self.formatter = logging.Formatter("%(message)s")
+ if uni.use_unicode():
+ encode_function = uni.uni2fs
+ else:
+ encode_function = None
+ self.formatter = uni.UnicodeFormatter("%(message)s",
+ encode_function=encode_function)
ch.setFormatter(self.formatter)
# add the handlers to the logger
self.logger.addHandler(ch)
@@ -511,7 +519,7 @@ class Blinkenlights(UIBase, CursesUtil):
return
if chr(key) == 'q':
# Request to quit completely.
- self.warn("Requested shutdown via 'q'")
+ self.warn(u"Requested shutdown via 'q'")
offlineimap.accounts.Account.set_abort_event(self.config, 3)
try:
index = int(chr(key))
@@ -525,7 +533,7 @@ class Blinkenlights(UIBase, CursesUtil):
def sleep(self, sleepsecs, account):
self.gettf().setcolor('red')
- self.info("Next sync in %d:%02d"% (sleepsecs / 60, sleepsecs % 60))
+ self.info(u"Next sync in %d:%02d"% (sleepsecs / 60, sleepsecs % 60))
return super(Blinkenlights, self).sleep(sleepsecs, account)
def sleeping(self, sleepsecs, remainingsecs):
@@ -551,15 +559,15 @@ class Blinkenlights(UIBase, CursesUtil):
self.lock()
try:
#s.gettf().setcolor('white')
- self.warn(" *** Input Required")
- self.warn(" *** Please enter password for account %s: " % \
- accountname)
+ self.warn(u" *** Input Required")
+ self.warn(u" *** Please enter password for account %s: "%
+ accountname)
self.logwin.refresh()
password = self.logwin.getstr()
finally:
self.unlock()
self.inputhandler.input_release()
- return password
+ return fsString(password)
def setupwindows(self, resize=False):
"""Setup and draw bannerwin and logwin.
diff --git a/offlineimap/ui/Machine.py b/offlineimap/ui/Machine.py
index dc650c3..cda1f1b 100644
--- a/offlineimap/ui/Machine.py
+++ b/offlineimap/ui/Machine.py
@@ -20,14 +20,31 @@ except ImportError: # python3
import sys
import time
import logging
+
from threading import currentThread
from offlineimap.ui.UIBase import UIBase
+from offlineimap.utils import uni
+from offlineimap.utils.uni import dbytesString
import offlineimap
protocol = '7.0.0'
-class MachineLogFormatter(logging.Formatter):
+#
+# UNICODE: This module is a low-level UI driver. Work with Unicode strings.
+#
+
+
+def getThreadname(thread):
+ return dbytesString(thread.getName()).uni
+
+
+class MachineLogFormatter(uni.UnicodeFormatter):
"""urlencodes any outputted line, to avoid multi-line output"""
+
+ def __init__(self, fmt, datefmt=None):
+ uni.UnicodeFormatter.__init__(self, fmt, datefmt,
+ encode_function=uni.uni2bytes)
+
def format(s, record):
# Mapping of log levels to historic tag names
severity_map = {
@@ -43,10 +60,10 @@ class MachineLogFormatter(logging.Formatter):
whoami = record.machineui["id"]
else:
command = ""
- whoami = currentThread().getName()
+ whoami = getThreadname(currentThread())
- prefix = "%s:%s"% (command, urlencode([('', whoami)])[1:])
- return "%s:%s:%s"% (severity, prefix, urlencode([('', line)])[1:])
+ prefix = u"%s:%s"% (command, urlencode([(u'', whoami)])[1:])
+ return u"%s:%s:%s"% (severity, prefix, urlencode([(u'', line)])[1:])
class MachineUI(UIBase):
@@ -67,7 +84,7 @@ class MachineUI(UIBase):
extra = {
'machineui': {
'command': command,
- 'id': currentThread().getName(),
+ 'id': getThreadname(currentThread())
}
})
@@ -84,7 +101,7 @@ class MachineUI(UIBase):
def unregisterthread(s, thread):
UIBase.unregisterthread(s, thread)
- s._printData(s.logger.info, 'unregisterthread', thread.getName())
+ s._printData(s.logger.info, 'unregisterthread', getThreadname(thread))
def debugging(s, debugtype):
s._printData(s.logger.debug, 'debugging', debugtype)
@@ -96,79 +113,80 @@ class MachineUI(UIBase):
s._printData(s.logger.info, 'acctdone', accountname)
def validityproblem(s, folder):
- s._printData(s.logger.warning, 'validityproblem', "%s\n%s\n%s\n%s"%
- (folder.getname(), folder.getrepository().getname(),
+ s._printData(s.logger.warning, 'validityproblem', u"%s\n%s\n%s\n%s"%
+ (folder.getname().uni, folder.getrepository().getname().uni,
folder.get_saveduidvalidity(), folder.get_uidvalidity()))
- def connecting(s, hostname, port):
- s._printData(s.logger.info, 'connecting', "%s\n%s"% (hostname, str(port)))
+ def connecting(s, uni_hostname, port):
+ s._printData(s.logger.info, 'connecting', u"%s\n%s"%
+ (uni_hostname, str(port)))
def syncfolders(s, srcrepos, destrepos):
- s._printData(s.logger.info, 'syncfolders', "%s\n%s"% (s.getnicename(srcrepos),
- s.getnicename(destrepos)))
+ s._printData(s.logger.info, 'syncfolders', u"%s\n%s"%
+ (s.getnicename(srcrepos), s.getnicename(destrepos)))
def syncingfolder(s, srcrepos, srcfolder, destrepos, destfolder):
- s._printData(s.logger.info, 'syncingfolder', "%s\n%s\n%s\n%s\n"%
+ s._printData(s.logger.info, 'syncingfolder', u"%s\n%s\n%s\n%s\n"%
(s.getnicename(srcrepos), srcfolder.getname(),
s.getnicename(destrepos), destfolder.getname()))
def loadmessagelist(s, repos, folder):
- s._printData(s.logger.info, 'loadmessagelist', "%s\n%s"% (s.getnicename(repos),
- folder.getvisiblename()))
+ s._printData(s.logger.info, 'loadmessagelist', u"%s\n%s"%
+ (s.getnicename(repos), folder.getvisiblename().uni))
def messagelistloaded(s, repos, folder, count):
- s._printData(s.logger.info, 'messagelistloaded', "%s\n%s\n%d"%
- (s.getnicename(repos), folder.getname(), count))
+ s._printData(s.logger.info, 'messagelistloaded', u"%s\n%s\n%d"%
+ (s.getnicename(repos), folder.getname().uni, count))
def syncingmessages(s, sr, sf, dr, df):
- s._printData(s.logger.info, 'syncingmessages', "%s\n%s\n%s\n%s\n"%
- (s.getnicename(sr), sf.getname(), s.getnicename(dr),
- df.getname()))
+ s._printData(s.logger.info, 'syncingmessages', u"%s\n%s\n%s\n%s\n"%
+ (s.getnicename(sr), sf.getname().uni, s.getnicename(dr),
+ df.getname().uni))
def copyingmessage(s, uid, num, num_to_copy, srcfolder, destfolder):
- s._printData(s.logger.info, 'copyingmessage', "%d\n%s\n%s\n%s[%s]"%
- (uid, s.getnicename(srcfolder), srcfolder.getname(),
+ s._printData(s.logger.info, 'copyingmessage', u"%d\n%s\n%s\n%s[%s]"%
+ (uid, s.getnicename(srcfolder), srcfolder.getname().uni,
s.getnicename(destfolder), destfolder))
def folderlist(s, list):
- return ("\f".join(["%s\t%s"% (s.getnicename(x), x.getname()) for x in list]))
+ return (u"\f".join([u"%s\t%s"% (s.getnicename(x), x.getname().uni) for x in list]))
def uidlist(s, list):
- return ("\f".join([str(u) for u in list]))
+ return (u"\f".join([str(u) for u in list]))
def deletingmessages(s, uidlist, destlist):
ds = s.folderlist(destlist)
- s._printData(s.logger.info, 'deletingmessages', "%s\n%s"% (s.uidlist(uidlist), ds))
+ s._printData(s.logger.info, 'deletingmessages', u"%s\n%s"%
+ (s.uidlist(uidlist), ds))
def addingflags(s, uidlist, flags, dest):
- s._printData(s.logger.info, "addingflags", "%s\n%s\n%s"% (s.uidlist(uidlist),
- "\f".join(flags),
- dest))
+ s._printData(s.logger.info, "addingflags", u"%s\n%s\n%s"%
+ (s.uidlist(uidlist), u"\f".join(flags), dest))
def deletingflags(s, uidlist, flags, dest):
- s._printData(s.logger.info, 'deletingflags', "%s\n%s\n%s"% (s.uidlist(uidlist),
- "\f".join(flags),
- dest))
+ s._printData(s.logger.info, 'deletingflags', u"%s\n%s\n%s"%
+ (s.uidlist(uidlist), u"\f".join(flags), dest))
def threadException(s, thread):
- s._printData(s.logger.warning, 'threadException', "%s\n%s"%
- (thread.getName(), s.getThreadExceptionString(thread)))
+ s._printData(s.logger.warning, 'threadException', u"%s\n%s"%
+ (getThreadname(thread), s.getThreadExceptionString(thread)))
s.delThreadDebugLog(thread)
s.terminate(100)
def terminate(s, exitstatus=0, errortitle='', errormsg=''):
- s._printData(s.logger.info, 'terminate', "%d\n%s\n%s"% (exitstatus, errortitle, errormsg))
+ s._printData(s.logger.info, 'terminate', u"%d\n%s\n%s"%
+ (exitstatus, errortitle, errormsg))
sys.exit(exitstatus)
def mainException(s):
s._printData(s.logger.warning, 'mainException', s.getMainExceptionString())
def threadExited(s, thread):
- s._printData(s.logger.info, 'threadExited', thread.getName())
+ s._printData(s.logger.info, 'threadExited', getThreadname(thread))
UIBase.threadExited(s, thread)
def sleeping(s, sleepsecs, remainingsecs):
- s._printData(s.logger.info, 'sleeping', "%d\n%d"% (sleepsecs, remainingsecs))
+ s._printData(s.logger.info, 'sleeping', u"%d\n%d"% (sleepsecs, remainingsecs))
if sleepsecs > 0:
time.sleep(sleepsecs)
return 0
@@ -176,9 +194,8 @@ class MachineUI(UIBase):
def getpass(s, accountname, config, errmsg=None):
if errmsg:
- s._printData(s.logger.warning,
- 'getpasserror', "%s\n%s"% (accountname, errmsg),
- False)
+ s._printData(s.logger.warning, 'getpasserror', u"%s\n%s"%
+ (accountname, errmsg), False)
s._log_con_handler.acquire() # lock the console output
try:
diff --git a/offlineimap/ui/TTY.py b/offlineimap/ui/TTY.py
index 0b5aa6a..ae2459a 100644
--- a/offlineimap/ui/TTY.py
+++ b/offlineimap/ui/TTY.py
@@ -19,31 +19,45 @@ import logging
import sys
import time
from getpass import getpass
+
from offlineimap import banner
from offlineimap.ui.UIBase import UIBase
+from offlineimap.utils import uni
+from offlineimap.utils.uni import fsString
+
-class TTYFormatter(logging.Formatter):
+class TTYFormatter(uni.UnicodeFormatter):
"""Specific Formatter that adds thread information to the log output."""
- def __init__(self, *args, **kwargs):
+ def __init__(self, fmt, datefmt=None):
#super() doesn't work in py2.6 as 'logging' uses old-style class
- logging.Formatter.__init__(self, *args, **kwargs)
+ if uni.use_unicode():
+ encode_function = uni.uni2fs
+ else:
+ encode_function = None
+ uni.UnicodeFormatter.__init__(self, fmt, datefmt,
+ encode_function=encode_function)
self._last_log_thread = None
def format(self, record):
- """Override format to add thread information."""
+ """Override format to add thread information.
- #super() doesn't work in py2.6 as 'logging' uses old-style class
- log_str = logging.Formatter.format(self, record)
+ We are near to output. It's convenient to encode everything here."""
+
+ # super() doesn't work in py2.6 as 'logging' uses old-style class.
+ log_str = uni.UnicodeFormatter.format(self, 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
+ #
+ # Because the Thread module does not support Unicode, threadName is
+ # encoded with uni.ENCODING. Re-encode to filesystem.
+ t_name = uni.uni2fs(uni.bytes2uni(record.threadName))
if t_name == 'MainThread':
- return log_str # main thread doesn't get things prepended
+ 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)
+ log_str = "%s:\n %s"% (t_name, log_str)
else:
log_str = " %s"% log_str
return log_str
@@ -60,7 +74,7 @@ class TTYUI(UIBase):
ch = logging.StreamHandler()
#ch.setLevel(logging.DEBUG)
# create formatter and add it to the handlers
- self.formatter = TTYFormatter("%(message)s")
+ self.formatter = TTYFormatter(u"%(message)s")
ch.setFormatter(self.formatter)
# add the handlers to the logger
self.logger.addHandler(ch)
@@ -78,17 +92,18 @@ class TTYUI(UIBase):
"""TTYUI backend is capable of querying the password."""
if errmsg:
- self.warn("%s: %s"% (accountname, errmsg))
+ self.warn(u"%s: %s"% (accountname, errmsg))
self._log_con_handler.acquire() # lock the console output
try:
- return getpass("Enter password for account '%s': " % accountname)
+ return fsString(getpass(
+ u"Enter password for account '%s': "% accountname))
finally:
self._log_con_handler.release()
def mainException(self):
if isinstance(sys.exc_info()[1], KeyboardInterrupt):
- self.logger.warn("Timer interrupted at user request; program "
- "terminating.\n")
+ self.logger.warn(u"Timer interrupted at user request; program "
+ u"terminating.\n")
self.terminate()
else:
UIBase.mainException(self)
@@ -105,7 +120,7 @@ class TTYUI(UIBase):
if sleepsecs > 0:
if remainingsecs//60 != (remainingsecs-sleepsecs)//60:
- self.logger.info("Next refresh in %.1f minutes" % (
- remainingsecs/60.0))
+ self.logger.info(u"Next refresh in %.1f minutes"%
+ (remainingsecs/60.0))
time.sleep(sleepsecs)
return 0
diff --git a/offlineimap/ui/UIBase.py b/offlineimap/ui/UIBase.py
index 9285b51..75ee3fd 100644
--- a/offlineimap/ui/UIBase.py
+++ b/offlineimap/ui/UIBase.py
@@ -26,8 +26,14 @@ try:
except ImportError: #python3
from queue import Queue
from collections import deque
+
+from offlineimap import globals
from offlineimap.error import OfflineImapError
import offlineimap
+from offlineimap.utils import uni
+from offlineimap.utils.uni import fsString
+from offlineimap.utils import hack
+
debugtypes = {'':'Other offlineimap related sync messages',
'imap': 'IMAP protocol debugging',
@@ -82,7 +88,12 @@ class UIBase(object):
ch = logging.StreamHandler(sys.stdout)
#ch.setLevel(logging.DEBUG)
# create formatter and add it to the handlers
- self.formatter = logging.Formatter("%(message)s")
+ if globals.options.use_unicode:
+ encode_function = uni.uni2fs
+ else:
+ encode_function = None
+ self.formatter = uni.UnicodeFormatter("%(message)s",
+ encode_function=encode_function)
ch.setFormatter(self.formatter)
# add the handlers to the logger
self.logger.addHandler(ch)
@@ -92,16 +103,22 @@ class UIBase(object):
def setlogfile(self, logfile):
"""Create file handler which logs to file."""
- fh = logging.FileHandler(logfile, 'at')
- file_formatter = logging.Formatter("%(asctime)s %(levelname)s: "
- "%(message)s", '%Y-%m-%d %H:%M:%S')
+ if globals.options.use_unicode:
+ encode_function = uni.uni2bytes
+ # logfile was kept encoded.
+ else:
+ encoding = uni.uni2std
+ fh = logging.FileHandler(logfile, 'at', encoding=encoding)
+ file_formatter = uni.UnicodeFormatter(u"%(asctime)s %(levelname)s: "
+ u"%(message)s", '%Y-%m-%d %H:%M:%S',
+ encode_function=None)
fh.setFormatter(file_formatter)
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.__bigversion__, p_ver, sys.platform,
- " ".join(sys.argv))
+ msg = u"OfflineImap %s starting...\n Python: %s Platform: %s\n "\
+ u"Args: %s"% (offlineimap.__bigversion__, p_ver, sys.platform,
+ u" ".join(sys.argv))
self.logger.info(msg)
def _msg(self, msg):
@@ -142,10 +159,12 @@ class UIBase(object):
ui.error(exc, sys.exc_info()[2], msg="While syncing Folder %s in "
"repo %s")
"""
+
if msg:
- self.logger.error("ERROR: %s\n %s" % (msg, exc))
+ self.logger.error(u"ERROR: %s\n %s"%
+ (msg, fsString(str(exc)).uni))
else:
- self.logger.error("ERROR: %s" % (exc))
+ self.logger.error(u"ERROR: %s"% (fsString(str(exc)).uni))
instant_traceback = exc_traceback
if not self.debuglist:
@@ -160,14 +179,15 @@ class UIBase(object):
"""Register current thread as being associated with an account name."""
cur_thread = threading.currentThread()
+ thr_name = cur_thread.getName()
if cur_thread in self.threadaccounts:
# was already associated with an old account, update info
- self.debug('thread', "Register thread '%s' (previously '%s', now "
- "'%s')" % (cur_thread.getName(),
- self.getthreadaccount(cur_thread), account))
+ self.debug('thread', u"Register thread '%s' "
+ u"(previously '%s', now '%s')"% (thr_name,
+ self.getthreadaccount(cur_thread), account))
else:
- self.debug('thread', "Register new thread '%s' (account '%s')"%
- (cur_thread.getName(), account))
+ self.debug('thread', u"Register new thread '%s' (account '%s')"%
+ (thr_name, account))
self.threadaccounts[cur_thread] = account
def unregisterthread(self, thr):
@@ -175,7 +195,8 @@ class UIBase(object):
if thr in self.threadaccounts:
del self.threadaccounts[thr]
- self.debug('thread', "Unregister thread '%s'" % thr.getName())
+ thr_name = thr.getName()
+ self.debug('thread', u"Unregister thread '%s'"% thr_name)
def getthreadaccount(self, thr=None):
"""Get Account() for a thread (current if None)
@@ -195,14 +216,14 @@ class UIBase(object):
# introduced in p2.6 only, so we'll need to work around and
# shorten our debugmsg list manually :-(
self.debugmessages[cur_thread] = deque()
- self.debugmessages[cur_thread].append("%s: %s" % (debugtype, msg))
+ self.debugmessages[cur_thread].append(u"%s: %s"% (debugtype, msg))
# Shorten queue if needed
if len(self.debugmessages[cur_thread]) > self.debugmsglen:
self.debugmessages[cur_thread].popleft()
if debugtype in self.debuglist: # log if we are supposed to do so
- self.logger.debug("[%s]: %s" % (debugtype, msg))
+ self.logger.debug("u[%s]: %s"% (debugtype, msg))
def add_debug(self, debugtype):
global debugtypes
@@ -215,18 +236,18 @@ class UIBase(object):
def debugging(self, debugtype):
global debugtypes
- self.logger.debug("Now debugging for %s: %s" % (debugtype,
- debugtypes[debugtype]))
+ msg = u"Now debugging for %s: %s"% (debugtype, debugtypes[debugtype])
+ self.logger.debug(msg)
def invaliddebug(self, debugtype):
- self.warn("Invalid debug type: %s" % debugtype)
+ self.warn(u"Invalid debug type: %s"% debugtype)
- def getnicename(self, object):
+ def getnicename(self, obj):
"""Return the type of a repository or Folder as string.
(IMAP, Gmail, Maildir, etc...)"""
- prelimname = object.__class__.__name__.split('.')[-1]
+ prelimname = obj.__class__.__name__.split('.')[-1]
# Strip off extra stuff.
return re.sub('(Folder|Repository)', '', prelimname)
@@ -239,20 +260,20 @@ class UIBase(object):
################################################## INPUT
- def getpass(self, accountname, config, errmsg = None):
+ def getpass(self, accountname, config, errmsg=None):
raise NotImplementedError("Prompting for a password is not supported"
" in this UI backend.")
def folderlist(self, folder_list):
- return ', '.join(["%s[%s]"% \
- (self.getnicename(x), x.getname()) for x in folder_list])
+ return u', '.join([u"%s[%s]"% \
+ (self.getnicename(x), x.getname().uni) for x in folder_list])
################################################## WARNINGS
def msgtoreadonly(self, destfolder, uid, content, flags):
if self.config.has_option('general', 'ignore-readonly') and \
self.config.getboolean('general', 'ignore-readonly'):
return
- self.warn("Attempted to synchronize message %d to folder %s[%s], "
+ self.warn(u"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))
@@ -261,7 +282,7 @@ class UIBase(object):
if self.config.has_option('general', 'ignore-readonly') and \
self.config.getboolean('general', 'ignore-readonly'):
return
- self.warn("Attempted to modify flags for messages %s in folder %s[%s], "
+ self.warn(u"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))
@@ -270,7 +291,7 @@ class UIBase(object):
if self.config.has_option('general', 'ignore-readonly') and \
self.config.getboolean('general', 'ignore-readonly'):
return
- self.warn("Attempted to modify labels for messages %s in folder %s[%s], "
+ self.warn(u"Attempted to modify labels for messages %s in folder %s[%s], "
"but that folder is read-only. No labels have been modified "
"for that message."% (
str(uidlist), self.getnicename(destfolder), destfolder))
@@ -279,7 +300,7 @@ class UIBase(object):
if self.config.has_option('general', 'ignore-readonly') and \
self.config.getboolean('general', 'ignore-readonly'):
return
- self.warn("Attempted to delete messages %s in folder %s[%s], but that "
+ self.warn(u"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))
@@ -298,162 +319,173 @@ class UIBase(object):
if not self.logger.isEnabledFor(logging.INFO): return
displaystr = ''
- hostname = hostname if hostname else ''
- port = "%s"% port if port else ''
+ hostname = hostname if hostname else u''
+ port = u"%s"% port if port else u''
if hostname:
- displaystr = ' to %s:%s' % (hostname, port)
- self.logger.info("Establishing connection%s" % displaystr)
+ displaystr = u' to %s:%s'% (hostname, port)
+ self.logger.info(u"Establishing connection%s"% displaystr)
def acct(self, account):
"""Output that we start syncing an account (and start counting)."""
self.acct_startimes[account] = time.time()
- self.logger.info("*** Processing account %s" % account)
+ self.logger.info(u"*** Processing account %s"% account)
def acctdone(self, account):
"""Output that we finished syncing an account (in which time)."""
sec = time.time() - self.acct_startimes[account]
del self.acct_startimes[account]
- self.logger.info("*** Finished account '%s' in %d:%02d"%
+ self.logger.info(u"*** Finished account '%s' in %d:%02d"%
(account, sec // 60, sec % 60))
def syncfolders(self, src_repo, dst_repo):
"""Log 'Copying folder structure...'."""
if self.logger.isEnabledFor(logging.DEBUG):
- self.debug('', "Copying folder structure from %s to %s" %\
- (src_repo, dst_repo))
+ self.debug('', u"Copying folder structure from %s to %s"%
+ (src_repo, dst_repo))
############################## Folder syncing
def makefolder(self, repo, foldername):
"""Called when a folder is created."""
- prefix = "[DRYRUN] " if self.dryrun else ""
- self.info(("{0}Creating folder {1}[{2}]".format(
+ prefix = u"[DRYRUN] " if self.dryrun else u""
+ self.info((u"{0}Creating folder {1}[{2}]".format(
prefix, foldername, repo)))
def syncingfolder(self, srcrepos, srcfolder, destrepos, destfolder):
"""Called when a folder sync operation is started."""
- self.logger.info("Syncing %s: %s -> %s"% (srcfolder,
+ self.logger.info(u"Syncing %s: %s -> %s"% (srcfolder,
self.getnicename(srcrepos),
self.getnicename(destrepos)))
def skippingfolder(self, folder):
"""Called when a folder sync operation is started."""
- self.logger.info("Skipping %s (not changed)" % folder)
+
+ self.logger.info(u"Skipping %s (not changed)"% folder)
def validityproblem(self, folder):
- self.logger.warning("UID validity problem for folder %s (repo %s) "
- "(saved %d; got %d); skipping it. Please see FAQ "
- "and manual on how to handle this."% \
- (folder, folder.getrepository(),
- folder.get_saveduidvalidity(), folder.get_uidvalidity()))
+ msg = u"UID validity problem for folder %s (repo %s) " \
+ "(saved %d; got %d); skipping it. Please see FAQ " \
+ "and manual on how to handle this."% \
+ (folder, folder.getrepository(),
+ folder.get_saveduidvalidity(),
+ folder.get_uidvalidity())
+ self.logger.warning(msg)
def loadmessagelist(self, repos, folder):
- self.logger.debug("Loading message list for %s[%s]"% (
- self.getnicename(repos),
- folder))
+ msg = u"Loading message list for %s[%s]"% (
+ self.getnicename(repos), folder)
+ self.logger.debug(msg)
def messagelistloaded(self, repos, folder, count):
- self.logger.debug("Message list for %s[%s] loaded: %d messages" % (
- self.getnicename(repos), folder, count))
+ self.logger.debug(u"Message list for %s[%s] loaded: %d messages"%
+ (self.getnicename(repos), folder, count))
############################## Message syncing
def syncingmessages(self, sr, srcfolder, dr, dstfolder):
- self.logger.debug("Syncing messages %s[%s] -> %s[%s]" % (
- self.getnicename(sr), srcfolder,
- self.getnicename(dr), dstfolder))
+ msg = u"Syncing messages %s[%s] -> %s[%s]"% (
+ self.getnicename(sr), srcfolder,
+ self.getnicename(dr), dstfolder)
+ self.logger.debug(msg)
def copyingmessage(self, uid, num, num_to_copy, src, destfolder):
- """Output a log line stating which message we copy"""
- self.logger.info("Copy message %s (%d of %d) %s:%s -> %s" % (
+ """Output a log line stating which message we copy. """
+
+ self.logger.info(u"Copy message %s (%d of %d) %s:%s -> %s"% (
uid, num, num_to_copy, src.repository, src,
destfolder.repository))
def deletingmessages(self, uidlist, destlist):
ds = self.folderlist(destlist)
- prefix = "[DRYRUN] " if self.dryrun else ""
- self.info("{0}Deleting {1} messages ({2}) in {3}".format(
+ prefix = u"[DRYRUN] " if self.dryrun else u""
+ self.info(u"{0}Deleting {1} messages ({2}) in {3}".format(
prefix, len(uidlist),
offlineimap.imaputil.uid_sequence(uidlist), ds))
def addingflags(self, uidlist, flags, dest):
- self.logger.info("Adding flag %s to %d messages on %s" % (
- ", ".join(flags), len(uidlist), dest))
+ msg = u"Adding flag %s to %d messages on %s"% (
+ u", ".join(flags), len(uidlist), dest)
+ self.logger.info(msg)
def deletingflags(self, uidlist, flags, dest):
- self.logger.info("Deleting flag %s from %d messages on %s" % (
- ", ".join(flags), len(uidlist), dest))
+ msg = u"Deleting flag %s from %d messages on %s"% (
+ u", ".join(flags), len(uidlist), dest)
+ self.logger.info(msg)
def addinglabels(self, uidlist, label, dest):
- self.logger.info("Adding label %s to %d messages on %s" % (
- label, len(uidlist), dest))
+ msg = u"Adding label %s to %d messages on %s"% (
+ label, len(uidlist), dest)
+ self.logger.info(msg)
def deletinglabels(self, uidlist, label, dest):
- self.logger.info("Deleting label %s from %d messages on %s" % (
- label, len(uidlist), dest))
+ msg = u"Deleting label %s from %d messages on %s"% (
+ label, len(uidlist), dest)
+ self.logger.info(msg)
def settinglabels(self, uid, num, num_to_set, labels, dest):
- self.logger.info("Setting labels to message %d on %s (%d of %d): %s" % (
- uid, dest, num, num_to_set, ", ".join(labels)))
+ msg = u"Setting labels to message %d on %s (%d of %d): %s"% (
+ uid, dest, num, num_to_set, ", ".join(labels))
+ self.logger.info()
def collectingdata(self, uidlist, source):
if uidlist:
- self.logger.info("Collecting data from %d messages on %s"% (
- len(uidlist), source))
+ msg = u"Collecting data from %d messages on %s"% (
+ len(uidlist), source)
else:
- self.logger.info("Collecting data from messages on %s"% source)
+ msg = u"Collecting data from messages on %s"% source
+ self.logger.info(msg)
def serverdiagnostics(self, repository, type):
"""Connect to repository and output useful information for debugging."""
conn = None
- self._msg("%s repository '%s': type '%s'" % (type, repository.name,
- self.getnicename(repository)))
+ self._msg(u"%s repository '%s': type '%s'"% (
+ type, repository.getname().uni, self.getnicename(repository)))
try:
if hasattr(repository, 'gethost'): # IMAP
- self._msg("Host: %s Port: %s SSL: %s"% (repository.gethost(),
+ self._msg(u"Host: %s Port: %s SSL: %s"% (repository.gethost().uni,
repository.getport(), repository.getssl()))
try:
conn = repository.imapserver.acquireconnection()
except OfflineImapError as e:
- self._msg("Failed to connect. Reason %s" % e)
+ self._msg(u"Failed to connect. Reason %s"% e)
else:
if 'ID' in conn.capabilities:
- self._msg("Server supports ID extension.")
+ self._msg(u"Server supports ID extension.")
#TODO: Debug and make below working, it hangs Gmail
#res_type, response = conn.id((
# 'name', offlineimap.__productname__,
# 'version', offlineimap.__bigversion__))
- #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))
+ #self._msg(u"Server ID: %s %s" % (res_type, response[0]))
+ self._msg(u"Server welcome string: %s"% str(conn.welcome))
+ self._msg(u"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)
+ self._msg(u"folderfilter= %s\n"% folderfilter)
folderincludes = repository.getconf('folderincludes', None)
if folderincludes:
- self._msg("folderincludes= %s\n" % folderincludes)
+ self._msg(u"folderincludes= %s\n"% folderincludes)
nametrans = repository.getconf('nametrans', None)
if nametrans:
- self._msg("nametrans= %s\n" % nametrans)
+ self._msg(u"nametrans= %s\n"% nametrans)
folders = repository.getfolders()
- foldernames = [(f.name, f.getvisiblename(), f.sync_this)
+ foldernames = [(f.getname().uni, f.getvisiblename().uni, f.sync_this)
for f in folders]
folders = []
for name, visiblename, sync_this in foldernames:
- syncstr = "" if sync_this else " (disabled)"
- if name == visiblename: folders.append("%s%s" % (name,
- syncstr))
- else: folders.append("%s -> %s%s" % (name,
- visiblename, syncstr))
- self._msg("Folderlist:\n %s\n" % "\n ".join(folders))
+ syncstr = u"" if sync_this else u" (disabled)"
+ if name == visiblename: folders.append(u"%s%s"%
+ (name, syncstr))
+ else: folders.append(u"%s -> %s%s"%
+ (name, visiblename, syncstr))
+ self._msg(u"Folderlist:\n %s\n"% u"\n ".join(folders))
finally:
if conn: #release any existing IMAP connection
repository.imapserver.close()
@@ -462,18 +494,18 @@ class UIBase(object):
"""Output a log line stating that we save a msg."""
self.debug(debugtype, "Write mail '%s:%d' with flags %s"%
- (folder, uid, repr(flags)))
+ (folder.getname().uni, uid, repr(flags)))
################################################## Threads
def getThreadDebugLog(self, thread):
+ thr_name = thread.getName()
if thread in self.debugmessages:
- message = "\nLast %d debug messages logged for %s prior to exception:\n"\
- % (len(self.debugmessages[thread]), thread.getName())
- message += "\n".join(self.debugmessages[thread])
+ message = u"\nLast %d debug messages logged for %s prior to" \
+ " exception:\n"% (len(self.debugmessages[thread]), thr_name)
+ message += u"\n".join(self.debugmessages[thread])
else:
- message = "\nNo debug messages were logged for %s."% \
- thread.getName()
+ message = u"\nNo debug messages were logged for %s."% thr_name
return message
def delThreadDebugLog(self, thread):
@@ -481,9 +513,10 @@ class UIBase(object):
del self.debugmessages[thread]
def getThreadExceptionString(self, thread):
- message = "Thread '%s' terminated with exception:\n%s"% \
- (thread.getName(), thread.exit_stacktrace)
- message += "\n" + self.getThreadDebugLog(thread)
+ thr_name = thread.getName()
+ message = u"Thread '%s' terminated with exception:\n%s"% (
+ thr_name, thread.exit_stacktrace)
+ message += u"\n" + self.getThreadDebugLog(thread)
return message
def threadException(self, thread):
@@ -497,23 +530,23 @@ class UIBase(object):
def terminate(self, exitstatus = 0, errortitle = None, errormsg = None):
"""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():
- self.warn("ERROR: Exceptions occurred during the run!")
+ self.warn(u"ERROR: Exceptions occurred during the run!")
while not self.exc_queue.empty():
msg, exc, exc_traceback = self.exc_queue.get()
if msg:
- self.warn("ERROR: %s\n %s"% (msg, exc))
+ self.warn(u"ERROR: %s\n %s"% (msg, fsString(str(exc)).uni))
else:
- self.warn("ERROR: %s"% (exc))
+ self.warn(u"ERROR: %s"% (fsString(str(exc)).uni))
if exc_traceback:
- self.warn("\nTraceback:\n%s"% "".join(
- traceback.format_tb(exc_traceback)))
+ self.warn(u"\nTraceback:\n%s"% u"".join(
+ traceback.format_tb(exc_traceback)))
if errormsg and errortitle:
- self.warn('ERROR: %s\n\n%s\n'% (errortitle, errormsg))
+ self.warn(u'ERROR: %s\n\n%s\n'% (errortitle, errormsg))
elif errormsg:
- self.warn('%s\n'% errormsg)
+ self.warn(u'%s\n'% errormsg)
sys.exit(exitstatus)
def threadExited(self, thread):
@@ -528,7 +561,7 @@ class UIBase(object):
def callhook(self, msg):
if self.dryrun:
- self.info("[DRYRUN] {0}".format(msg))
+ self.info(u"[DRYRUN] {0}".format(msg))
else:
self.info(msg)
@@ -566,7 +599,7 @@ class UIBase(object):
if sleepsecs > 0:
if remainingsecs//60 != (remainingsecs-sleepsecs)//60:
- self.logger.debug("Next refresh in %.1f minutes" % (
- remainingsecs/60.0))
+ self.logger.debug(u"Next refresh in %.1f minutes"% (
+ remainingsecs/60.0))
time.sleep(sleepsecs)
return 0
diff --git a/offlineimap/ui/debuglock.py b/offlineimap/ui/debuglock.py
index 673efb0..96609af 100644
--- a/offlineimap/ui/debuglock.py
+++ b/offlineimap/ui/debuglock.py
@@ -17,7 +17,14 @@
from threading import Lock, currentThread
import traceback
-logfile = open("/tmp/logfile", "wt")
+import codecs
+
+from offlineimap.utils import uni
+
+if uni.use_unicode():
+ logfile = codecs.open("/tmp/logfile", "wt", uni.ENCODING)
+else:
+ logfile = open("/tmp/logfile", "wt")
loglock = Lock()
class DebuggingLock:
@@ -26,24 +33,27 @@ class DebuggingLock:
self.name = name
def acquire(self, blocking = 1):
- self.print_tb("Acquire lock")
+ self.print_tb(u"Acquire lock")
self.lock.acquire(blocking)
- self.logmsg("===== %s: Thread %s acquired lock\n"%
+ self.logmsg(u"===== %s: Thread %s acquired lock\n"%
(self.name, currentThread().getName()))
def release(self):
- self.print_tb("Release lock")
+ self.print_tb(u"Release lock")
self.lock.release()
def logmsg(self, msg):
loglock.acquire()
- logfile.write(msg + "\n")
+ msg = msg + u"\n"
+ if uni.use_unicode():
+ msg = uni.uni2bytes(msg)
+ logfile.write(msg)
logfile.flush()
loglock.release()
def print_tb(self, msg):
- self.logmsg(".... %s: Thread %s attempting to %s\n"% \
- (self.name, currentThread().getName(), msg) + \
- "\n".join(traceback.format_list(traceback.extract_stack())))
+ self.logmsg(u".... %s: Thread %s attempting to %s\n"% \
+ (self.name, currentThread().getName(), msg) + \
+ u"\n".join(traceback.format_list(traceback.extract_stack())))
diff --git a/offlineimap/utils/const.py b/offlineimap/utils/const.py
index f4584bc..80fdd47 100644
--- a/offlineimap/utils/const.py
+++ b/offlineimap/utils/const.py
@@ -5,6 +5,11 @@
import copy
+#
+# UNICODE: assume variables to be full ASCII. Should be fine while
+# metaprogramming.
+#
+
class ConstProxy(object):
"""Implements read-only access to a given object
that can be attached to each instance only once."""
@@ -21,12 +26,12 @@ class ConstProxy(object):
def __setattr__(self, name, value):
- raise AttributeError("tried to set '%s' to '%s' for constant object"% \
+ raise AttributeError("tried to set '%s' to '%s' for constant object"%
(name, value))
def __delattr__(self, name):
- raise RuntimeError("tried to delete field '%s' from constant object"% \
+ raise RuntimeError("tried to delete field '%s' from constant object"%
(name))
diff --git a/offlineimap/utils/distro.py b/offlineimap/utils/distro.py
index 8cd2b79..3d35223 100644
--- a/offlineimap/utils/distro.py
+++ b/offlineimap/utils/distro.py
@@ -11,6 +11,8 @@ import os
# For the former we will just return the value, for an iterable
# we will walk through the values and will return the first
# one that corresponds to the existing file.
+#
+# UNICODE: all paths must be full ASCII.
__DEF_OS_LOCATIONS = {
'freebsd': '/usr/local/share/certs/ca-root-nss.crt',
'openbsd': '/etc/ssl/cert.pem',
@@ -35,9 +37,8 @@ def get_os_name():
the first component of platform.linux_distribution.
Return value will be all-lowercase to avoid confusion about
- proper name capitalisation.
+ proper name capitalisation."""
- """
OS = platform.system().lower()
if OS.startswith('linux'):
@@ -55,8 +56,7 @@ def get_os_sslcertfile_searchpath():
that they can iterate over result.
Returned value of None means that there is no search path
- at all.
- """
+ at all."""
OS = get_os_name()
@@ -83,9 +83,9 @@ def get_os_sslcertfile():
return None
for f in l:
- assert (type(f) == type(""))
- if os.path.exists(f) and \
- (os.path.isfile(f) or os.path.islink(f)):
- return f
+ assert (type(f) == type(""))
+ if os.path.exists(f) and (
+ os.path.isfile(f) or os.path.islink(f)):
+ return f
return None
--
2.2.2
More information about the OfflineIMAP-project
mailing list