Merge branch 'next'

This commit is contained in:
Nicolas Sebrecht 2012-09-12 19:48:19 +02:00
commit a73b4b3465
19 changed files with 177 additions and 104 deletions

View File

@ -4,17 +4,35 @@ ChangeLog
:website: http://offlineimap.org
WIP (add new stuff for the next release)
========================================
OfflineIMAP v6.5.5-rc1 (2012-09-05)
===================================
* Bump version number
OfflineIMAP v6.5.5-rc1 (2012-09-05)
===================================
* Don't create folders if readonly is enabled.
* Learn to deal with readonly folders to properly detect this condition and act
accordingly. One example is Gmail's "Chats" folder that is read-only,
but contains logs of the quick chats. (E. Ryabinkin)
* Fix str.format() calls for Python 2.6 (D. Logie)
* Remove APPENDUID hack, previously introduced to fix Gmail, no longer
necessary, it might have been breaking things. (J. Wiegley)
* Improve regex that could lead to 'NoneType' object has no attribute 'group'
(D. Franke)
* Improved error throwing on repository misconfiguration
OfflineIMAP v6.5.4 (2012-06-02)
=================================
===============================
* bump bundled imaplib2 library 2.29 --> 2.33
* Actually perform the SSL fingerprint check (reported by J. Cook)
* Curses UI, don't use colors after we shut down curses already (C.Höger)
* Document that '%' needs encoding as '%%' in *.conf
* Document that '%' needs encoding as '%%' in configuration files.
* Fix crash when IMAP.quickchanged() led to an Error (reported by sharat87)
* Implement the createfolders setting to disable folder propagation (see docs)
@ -60,7 +78,7 @@ OfflineIMAP v6.5.3 (2012-04-02)
* Improve compatability of the curses UI with python 2.6
OfflineIMAP v6.5.2.1 (2012-04-04)
=====================================
=================================
* Fix python2.6 compatibility with the TTYUI backend (crash)
@ -116,7 +134,7 @@ Smallish bug fixes that deserve to be put out.
* Add filter information to the filter list in --info output
OfflineIMAP v6.5.1.1 (2012-01-07) - "Das machine control is nicht fur gerfinger-poken und mittengrabben"
==================================================================================================================
========================================================================================================
Blinkenlights UI 6.5.0 regression fixes only.

19
MAINTAINERS Normal file
View File

@ -0,0 +1,19 @@
Official maintainers
====================
Dmitrijs Ledkovs
email: xnox at debian.org
github: xnox
Eygene Ryabinkin
email: rea at freebsd.org
github: konvpalto
Nicolas Sebrecht
email: nicolas.s-dev at laposte.net
github: nicolas33
Sebastian Spaeth
email: sebastian at sspaeth.de
github: spaetz

12
README
View File

@ -10,7 +10,9 @@ messages on each computer, and changes you make one place will be visible on all
other systems. For instance, you can delete a message on your home computer, and
it will appear deleted on your work computer as well. OfflineIMAP is also useful
if you want to use a mail reader that does not have IMAP support, has poor IMAP
support, or does not provide disconnected operation. It's homepage at http://offlineimap.org contains more information, source code, and online documentation.
support, or does not provide disconnected operation. It's homepage at
http://offlineimap.org contains more information, source code, and online
documentation.
OfflineIMAP does not require additional python dependencies beyond python >=2.6
(although python-sqlite is strongly recommended).
@ -87,9 +89,13 @@ Mailing list & bug reporting
----------------------------
The user discussion, development and all exciting stuff take place in the
OfflineImap mailing list at http://lists.alioth.debian.org/mailman/listinfo/offlineimap-project. You do not need to subscribe to send emails.
OfflineImap mailing list at
http://lists.alioth.debian.org/mailman/listinfo/offlineimap-project. You do not
need to subscribe to send emails.
Bugs, issues and contributions should be reported to the mailing list. Bugs can also be reported in the issue tracker at https://github.com/spaetz/offlineimap/issues.
Bugs, issues and contributions should be reported to the mailing list. Bugs can
also be reported in the issue tracker at
https://github.com/OfflineIMAP/offlineimap/issues.
Configuration Examples
======================

View File

@ -1,6 +1,6 @@
.. -*- coding: utf-8 -*-
.. _OfflineIMAP: https://github.com/spaetz/offlineimap
.. _OLI_git_repo: git://github.com/spaetz/offlineimap.git
.. _OfflineIMAP: https://github.com/OfflineIMAP/offlineimap
.. _OLI_git_repo: git://github.com/OfflineIMAP/offlineimap.git
============
Installation
@ -37,29 +37,40 @@ In order to use `OfflineIMAP`_, you need to have these conditions satisfied:
Installation
------------
Installing OfflineImap should usually be quite easy, as you can simply unpack and run OfflineImap in place if you wish to do so. There are a number of options though:
Installing OfflineImap should usually be quite easy, as you can simply unpack
and run OfflineImap in place if you wish to do so. There are a number of options
though:
#. system-wide :ref:`installation via your distribution package manager <inst_pkg_man>`
#. system-wide or single user :ref:`installation from the source package <inst_src_tar>`
#. system-wide or single user :ref:`installation from a git checkout <inst_git>`
Having installed OfflineImap, you will need to configure it, to be actually useful. Please check the :ref:`Configuration` section in the :doc:`MANUAL` for more information on the configuration step.
Having installed OfflineImap, you will need to configure it, to be actually
useful. Please check the :ref:`Configuration` section in the :doc:`MANUAL` for
more information on the configuration step.
.. _inst_pkg_man:
System-Wide Installation via distribution
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The easiest way to install OfflineIMAP is via your distribution's package manager. OfflineImap is available under the name `offlineimap` in most Linux and BSD distributions.
The easiest way to install OfflineIMAP is via your distribution's package
manager. OfflineImap is available under the name `offlineimap` in most Linux and
BSD distributions.
.. _inst_src_tar:
Installation from source package
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Download the latest source archive from our `download page <https://github.com/spaetz/offlineimap/downloads>`_. Simply click the "Download as .zip" or "Download as .tar.gz" buttons to get the latest "stable" code from the master branch. If you prefer command line, you will want to use:
wget https://github.com/spaetz/offlineimap/tarball/master
Unpack and continue with the :ref:`system-wide installation <system_wide_inst>` or the :ref:`single-user installation <single_user_inst>` section.
Download the latest source archive from our `download page
<https://github.com/spaetz/offlineimap/downloads>`_. Simply click the "Download
as .zip" or "Download as .tar.gz" buttons to get the latest "stable" code from
the master branch. If you prefer command line, you will want to use: wget
https://github.com/spaetz/offlineimap/tarball/master
Unpack and continue with the :ref:`system-wide installation <system_wide_inst>`
or the :ref:`single-user installation <single_user_inst>` section.
.. _inst_git:
@ -78,7 +89,9 @@ checkout a particular release like this::
cd offlineimap
git checkout v6.5.2.1
You have now a source tree available and proceed with either the :ref:`system-wide installation <system_wide_inst>` or the :ref:`single-user installation <single_user_inst>`.
You have now a source tree available and proceed with either the
:ref:`system-wide installation <system_wide_inst>` or the :ref:`single-user
installation <single_user_inst>`.
.. _system_wide_inst:

View File

@ -1,7 +1,7 @@
__all__ = ['OfflineImap']
__productname__ = 'OfflineIMAP'
__version__ = "6.5.4"
__version__ = "6.5.5-rc2"
__copyright__ = "Copyright 2002-2012 John Goerzen & contributors"
__author__ = "John Goerzen"
__author_email__= "john@complete.org"

View File

@ -217,13 +217,19 @@ class SyncableAccount(Account):
def syncrunner(self):
self.ui.registerthread(self)
accountmetadata = self.getaccountmeta()
if not os.path.exists(accountmetadata):
os.mkdir(accountmetadata, 0o700)
try:
accountmetadata = self.getaccountmeta()
if not os.path.exists(accountmetadata):
os.mkdir(accountmetadata, 0o700)
self.remoterepos = Repository(self, 'remote')
self.localrepos = Repository(self, 'local')
self.statusrepos = Repository(self, 'status')
self.remoterepos = Repository(self, 'remote')
self.localrepos = Repository(self, 'local')
self.statusrepos = Repository(self, 'status')
except OfflineImapError as e:
self.ui.error(e, exc_info()[2])
if e.severity >= OfflineImapError.ERROR.CRITICAL:
raise
return
# Loop account sync if needed (bail out after 3 failures)
looping = 3
@ -255,6 +261,12 @@ class SyncableAccount(Account):
if looping and self.sleeper() >= 2:
looping = 0
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()))
def sync(self):
"""Synchronize the account once, then return
@ -289,7 +301,6 @@ class SyncableAccount(Account):
# folder delimiter etc)
remoterepos.getfolders()
localrepos.getfolders()
statusrepos.getfolders()
remoterepos.sync_folder_structure(localrepos, statusrepos)
# replicate the folderstructure between REMOTE to LOCAL
@ -300,10 +311,16 @@ class SyncableAccount(Account):
for remotefolder in remoterepos.getfolders():
# check for CTRL-C or SIGTERM
if Account.abort_NOW_signal.is_set(): break
if not remotefolder.sync_this:
self.ui.debug('', "Not syncing filtered remote folder '%s'"
self.ui.debug('', "Not syncing filtered folder '%s'"
"[%s]" % (remotefolder, remoterepos))
continue # Filtered out remote folder
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))
continue # Ignore filtered folder
thread = InstanceLimitedThread(\
instancename = 'FOLDER_' + self.remoterepos.getname(),
target = syncfolder,
@ -367,17 +384,8 @@ def syncfolder(account, remotefolder, quick):
ui.registerthread(account)
try:
# Load local folder.
localfolder = localrepos.\
getfolder(remotefolder.getvisiblename().\
replace(remoterepos.getsep(), localrepos.getsep()))
localfolder = account.get_local_folder(remotefolder)
#Filtered folders on the remote side will not invoke this
#function, but we need to NOOP if the local folder is filtered
#out too:
if not localfolder.sync_this:
ui.debug('', "Not syncing filtered local folder '%s'" \
% localfolder)
return
# Write the mailboxes
mbnames.add(account.name, localfolder.getname())
@ -457,15 +465,8 @@ def syncfolder(account, remotefolder, quick):
if e.severity > OfflineImapError.ERROR.FOLDER:
raise
else:
#if the initial localfolder assignement bailed out, the localfolder var will not be available, so we need
ui.error(e, exc_info()[2], msg = "Aborting sync, folder '%s' "
"[acc: '%s']" % (
remotefolder.getvisiblename().\
replace(remoterepos.getsep(), localrepos.getsep()),
account))
# we reconstruct foldername above rather than using
# localfolder, as the localfolder var is not
# available if assignment fails.
"[acc: '%s']" % (localfolder, account))
except Exception as e:
ui.error(e, msg = "ERROR in syncfolder for %s folder %s: %s" % \
(account, remotefolder.getvisiblename(),

View File

@ -31,9 +31,12 @@ class BaseFolder(object):
:para name: Path & name of folder minus root or reference
:para repository: Repository() in which the folder is.
"""
self.sync_this = True
"""Should this folder be included in syncing?"""
self.ui = getglobalui()
"""Should this folder be included in syncing?"""
self._sync_this = repository.should_sync_folder(name)
if not self._sync_this:
self.ui.debug('', "Filtering out '%s'[%s] due to folderfilter" \
% (name, repository))
# Top level dir name is always ''
self.name = name if not name == self.getsep() else ''
self.repository = repository
@ -57,6 +60,11 @@ class BaseFolder(object):
"""Account name as string"""
return self.repository.accountname
@property
def sync_this(self):
"""Should this folder be synced or is it e.g. filtered out?"""
return self._sync_this
def suggeststhreads(self):
"""Returns true if this folder suggests using threads for actions;
false otherwise. Probably only IMAP will return true."""
@ -386,7 +394,7 @@ class BaseFolder(object):
self.getmessageuidlist())
num_to_copy = len(copylist)
if num_to_copy and self.repository.account.dryrun:
self.ui.info("[DRYRUN] Copy {} messages from {}[{}] to {}".format(
self.ui.info("[DRYRUN] Copy {0} messages from {1}[{2}] to {3}".format(
num_to_copy, self, self.repository, dstfolder.repository))
return
for num, uid in enumerate(copylist):

View File

@ -575,9 +575,8 @@ class IMAPFolder(BaseFolder):
(typ,dat) = imapobj.check()
assert(typ == 'OK')
# get the new UID. Test for APPENDUID response even if the
# server claims to not support it, as e.g. Gmail does :-(
if use_uidplus or imapobj._get_untagged_response('APPENDUID', True):
# get the new UID, do we use UIDPLUS?
if use_uidplus:
# get new UID from the APPENDUID response, it could look
# like OK [APPENDUID 38505 3955] APPEND completed with
# 38505 bein folder UIDvalidity and 3955 the new UID.
@ -585,7 +584,7 @@ class IMAPFolder(BaseFolder):
# often seems to return [None], even though we have
# data. TODO
resp = imapobj._get_untagged_response('APPENDUID')
if resp == [None]:
if resp == [None] or resp is None:
self.ui.warn("Server supports UIDPLUS but got no APPENDUID "
"appending a message.")
return 0

View File

@ -85,8 +85,7 @@ class LocalStatusFolder(BaseFolder):
file.close()
def save(self):
self.savelock.acquire()
try:
with self.savelock:
file = open(self.filename + ".tmp", "wt")
file.write(magicline + "\n")
for msg in self.messagelist.values():
@ -104,9 +103,6 @@ class LocalStatusFolder(BaseFolder):
os.fsync(fd)
os.close(fd)
finally:
self.savelock.release()
def getmessagelist(self):
return self.messagelist

View File

@ -15,6 +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
from offlineimap import OfflineImapError
from .IMAP import IMAPFolder
import os.path

View File

@ -23,7 +23,7 @@ from offlineimap.ui import getglobalui
# find the first quote in a string
quotere = re.compile(
r"""(?P<quote>"(?:\\"|[^"])*") # Quote, possibly containing encoded
r"""(?P<quote>"[^\"\\]*(?:\\"|[^"])*") # Quote, possibly containing encoded
# quotation mark
\s*(?P<rest>.*)$ # Whitespace & remainder of string""",
re.VERBOSE)

View File

@ -129,12 +129,17 @@ class BaseRepository(CustomConfig.ConfigHelperMixin, object):
def getsep(self):
raise NotImplementedError
def should_sync_folder(self, fname):
"""Should this folder be synced?"""
return fname in self.folderincludes or self.folderfilter(fname)
def get_create_folders(self):
"""Is folder creation enabled on this repository?
It is disabled by either setting the whole repository
'readonly' or by using the 'createfolders' setting."""
return self._readonly or self.getconfboolean('createfolders', True)
'readonly' or by using the 'createfolders' setting."""
return (not self._readonly) and \
self.getconfboolean('createfolders', True)
def makefolder(self, foldername):
"""Create a new folder"""
@ -201,7 +206,7 @@ class BaseRepository(CustomConfig.ConfigHelperMixin, object):
# 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.folderfilter(dst_name_t):
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))

View File

@ -287,11 +287,6 @@ class IMAPRepository(BaseRepository):
foldername = imaputil.dequote(name)
retval.append(self.getfoldertype()(self.imapserver, foldername,
self))
# filter out the folder?
if not self.folderfilter(foldername):
self.ui.debug('imap', "Filtering out '%s'[%s] due to folderfilt"
"er" % (foldername, self))
retval[-1].sync_this = False
# Add all folderincludes
if len(self.folderincludes):
imapobj = self.imapserver.acquireconnection()

View File

@ -43,8 +43,8 @@ class LocalStatusRepository(BaseRepository):
if not os.path.exists(self.root):
os.mkdir(self.root, 0o700)
# self._folders is a list of LocalStatusFolders()
self._folders = None
# self._folders is a dict of name:LocalStatusFolders()
self._folders = {}
def getsep(self):
return '.'
@ -79,23 +79,27 @@ class LocalStatusRepository(BaseRepository):
file.close()
os.rename(filename + ".tmp", filename)
# Invalidate the cache.
self._folders = None
self._folders = {}
def getfolder(self, foldername):
"""Return the Folder() object for a foldername"""
return self.LocalStatusFolderClass(foldername, self)
if foldername in self._folders:
return self._folders[foldername]
folder = self.LocalStatusFolderClass(foldername, self)
self._folders[foldername] = folder
return folder
def getfolders(self):
"""Returns a list of all cached folders."""
if self._folders != None:
return self._folders
"""Returns a list of all cached folders.
self._folders = []
for folder in os.listdir(self.root):
self._folders.append(self.getfolder(folder))
return self._folders
Does nothing for this backend. We mangle the folder file names
(see getfolderfilename) so we can not derive folder names from
the file names that we have available. TODO: need to store a
list of folder names somehow?"""
pass
def forgetfolders(self):
"""Forgets the cached list of folders, if any. Useful to run
after a sync run."""
self._folders = None
self._folders = {}

View File

@ -181,11 +181,6 @@ class MaildirRepository(BaseRepository):
foldername,
self.getsep(),
self))
# filter out the folder?
if not self.folderfilter(foldername):
self.debug("Filtering out '%s'[%s] due to folderfilt"
"er" % (foldername, self))
retval[-1].sync_this = False
if self.getsep() == '/' and dirname != '':
# Recursively check sub-directories for folders too.

View File

@ -15,10 +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
try:
from configparser import NoSectionError
except ImportError: #python2
from ConfigParser import NoSectionError
from offlineimap.repository.IMAP import IMAPRepository, MappedIMAPRepository
from offlineimap.repository.Gmail import GmailRepository
from offlineimap.repository.Maildir import MaildirRepository
from offlineimap.repository.LocalStatus import LocalStatusRepository
from offlineimap.error import OfflineImapError
class Repository(object):
"""Abstract class that returns the correct Repository type
@ -47,17 +54,26 @@ class Repository(object):
return LocalStatusRepository(name, account)
else:
raise ValueError("Request type %s not supported" % reqtype)
errstr = "Repository type %s not supported" % reqtype
raise OfflineImapError(errstr, OfflineImapError.ERROR.REPO)
# Get repository type
config = account.getconfig()
repostype = config.get('Repository ' + name, 'type').strip()
try:
repostype = config.get('Repository ' + name, 'type').strip()
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)
try:
repo = typemap[repostype]
except KeyError:
raise ValueError("'%s' repository not supported for %s repositories"
"." % (repostype, reqtype))
return repo(name, account)
errstr = "'%s' repository not supported for '%s' repositories." \
% (repostype, reqtype)
raise OfflineImapError(errstr, OfflineImapError.ERROR.REPO)
return repo(name, account)
def __init__(self, account, reqtype):
"""Load the correct Repository type and return that. The

View File

@ -88,7 +88,6 @@ class UIBase(object):
def setlogfile(self, logfile):
"""Create file handler which logs to file"""
fh = logging.FileHandler(logfile, 'at')
#fh.setLevel(logging.DEBUG)
file_formatter = logging.Formatter("%(asctime)s %(levelname)s: "
"%(message)s", '%Y-%m-%d %H:%M:%S')
fh.setFormatter(file_formatter)
@ -98,9 +97,7 @@ class UIBase(object):
msg = "OfflineImap %s starting...\n Python: %s Platform: %s\n "\
"Args: %s" % (offlineimap.__version__, p_ver, sys.platform,
" ".join(sys.argv))
record = logging.LogRecord('OfflineImap', logging.INFO, __file__,
None, msg, None, None)
fh.emit(record)
self.logger.info(msg)
def _msg(self, msg):
"""Display a message."""
@ -301,7 +298,7 @@ class UIBase(object):
def makefolder(self, repo, foldername):
"""Called when a folder is created"""
prefix = "[DRYRUN] " if self.dryrun else ""
self.info("{}Creating folder {}[{}]".format(
self.info("{0}Creating folder {1}[{2}]".format(
prefix, foldername, repo))
def syncingfolder(self, srcrepos, srcfolder, destrepos, destfolder):
@ -346,7 +343,7 @@ class UIBase(object):
def deletingmessages(self, uidlist, destlist):
ds = self.folderlist(destlist)
prefix = "[DRYRUN] " if self.dryrun else ""
self.info("{}Deleting {} messages ({}) in {}".format(
self.info("{0}Deleting {1} messages ({2}) in {3}".format(
prefix, len(uidlist),
offlineimap.imaputil.uid_sequence(uidlist), ds))
@ -474,7 +471,7 @@ class UIBase(object):
def callhook(self, msg):
if self.dryrun:
self.info("[DRYRUN] {}".format(msg))
self.info("[DRYRUN] {0}".format(msg))
else:
self.info(msg)

View File

@ -127,7 +127,7 @@ class OLITestLib():
reponame: All on `reponame` or all IMAP-type repositories if None"""
config = cls.get_default_config()
if reponame:
sections = ['Repository {}'.format(reponame)]
sections = ['Repository {0}'.format(reponame)]
else:
sections = [r for r in config.sections() \
if r.startswith('Repository')]
@ -162,8 +162,8 @@ class OLITestLib():
dirs = [d for d in dirs if d.startswith(b'INBOX.OLItest')]
for folder in dirs:
res_t, data = imapobj.delete(b'\"'+folder+b'\"')
assert res_t == 'OK', "Folder deletion of {} failed with error"\
":\n{} {}".format(folder.decode('utf-8'), res_t, data)
assert res_t == 'OK', "Folder deletion of {0} failed with error"\
":\n{1} {2}".format(folder.decode('utf-8'), res_t, data)
imapobj.logout()
@classmethod
@ -197,7 +197,7 @@ class OLITestLib():
Use some default content if not given"""
assert cls.testdir != None
while True: # Loop till we found a unique filename
mailfile = '{}:2,'.format(random.randint(0,999999999))
mailfile = '{0}:2,'.format(random.randint(0,999999999))
mailfilepath = os.path.join(cls.testdir, 'mail',
folder, 'new', mailfile)
if not os.path.isfile(mailfilepath):

View File

@ -67,7 +67,7 @@ class TestBasicFunctions(unittest.TestCase):
self.assertEqual(res, "")
boxes, mails = OLITestLib.count_maildir_mails('')
self.assertTrue((boxes, mails)==(0,0), msg="Expected 0 folders and 0 "
"mails, but sync led to {} folders and {} mails".format(
"mails, but sync led to {0} folders and {1} mails".format(
boxes, mails))
def test_02_createdir(self):
@ -82,7 +82,7 @@ class TestBasicFunctions(unittest.TestCase):
self.assertEqual(res, "")
boxes, mails = OLITestLib.count_maildir_mails('')
self.assertTrue((boxes, mails)==(2,0), msg="Expected 2 folders and 0 "
"mails, but sync led to {} folders and {} mails".format(
"mails, but sync led to {0} folders and {1} mails".format(
boxes, mails))
def test_03_nametransmismatch(self):
@ -101,7 +101,7 @@ class TestBasicFunctions(unittest.TestCase):
mismatch = "ERROR: INFINITE FOLDER CREATION DETECTED!" in res
self.assertEqual(mismatch, True, msg="Mismatching nametrans rules did "
"NOT trigger an 'infinite folder generation' error. Output was:\n"
"{}".format(res))
"{0}".format(res))
# Write out default config file again
OLITestLib.write_config_file()
@ -121,12 +121,12 @@ class TestBasicFunctions(unittest.TestCase):
self.assertEqual(res, "")
boxes, mails = OLITestLib.count_maildir_mails('')
self.assertTrue((boxes, mails)==(1,1), msg="Expected 1 folders and 1 "
"mails, but sync led to {} folders and {} mails".format(
"mails, but sync led to {0} folders and {1} mails".format(
boxes, mails))
# The local Mail should have been assigned a proper UID now, check!
uids = OLITestLib.get_maildir_uids('INBOX.OLItest')
self.assertFalse (None in uids, msg = "All mails should have been "+ \
"assigned the IMAP's UID number, but {} messages had no valid ID "\
"assigned the IMAP's UID number, but {0} messages had no valid ID "\
.format(len([None for x in uids if x==None])))
def test_05_createfolders(self):