Merge branch 'next' of github.com:OfflineIMAP/offlineimap into next

This commit is contained in:
Eygene Ryabinkin 2015-01-18 11:08:23 +03:00
commit 9b911faa58
19 changed files with 513 additions and 343 deletions

10
COPYING
View File

@ -65,7 +65,7 @@ patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and The precise terms and conditions for copying, distribution and
modification follow. modification follow.
GNU GENERAL PUBLIC LICENSE GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
@ -120,7 +120,7 @@ above, provided that you also meet all of these conditions:
License. (Exception: if the Program itself is interactive but License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on does not normally print such an announcement, your work based on
the Program is not required to print an announcement.) the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program, identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in and can be reasonably considered independent and separate works in
@ -178,7 +178,7 @@ access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not distribution of the source code, even though third parties are not
compelled to copy the source along with the object code. compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program 4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is otherwise to copy, modify, sublicense or distribute the Program is
@ -235,7 +235,7 @@ impose that choice.
This section is intended to make thoroughly clear what is believed to This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License. be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in 8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License original copyright holder who places the Program under this License
@ -288,7 +288,7 @@ PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES. POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest If you develop a new program, and you want it to be of the greatest

View File

@ -10,10 +10,22 @@ Eygene Ryabinkin
email: rea at freebsd.org email: rea at freebsd.org
github: konvpalto github: konvpalto
Sebastian Spaeth
email: sebastian at sspaeth.de
github: spaetz
Nicolas Sebrecht Nicolas Sebrecht
email: nicolas.s-dev at laposte.net email: nicolas.s-dev at laposte.net
github: nicolas33 github: nicolas33
MAILING LIST MAINTAINERS
========================
Eygene Ryabinkin
email: rea at freebsd.org
Sebastian Spaeth Sebastian Spaeth
email: sebastian at sspaeth.de email: sebastian at sspaeth.de
github: spaetz
Nicolas Sebrecht
email: nicolas.s-dev at laposte.net

View File

@ -13,57 +13,6 @@ This document contains assorted guidelines for programmers that want
to hack OfflineIMAP. to hack OfflineIMAP.
------------------
Exception handling
------------------
OfflineIMAP on many occasions re-raises various exceptions and often
changes exception type to `OfflineImapError`. This is not a problem
per se, but you must always remember that we need to preserve original
tracebacks. This is not hard if you follow these simple rules.
For re-raising original exceptions, just use::
raise
from inside your exception handling code.
If you need to change exception type, or its argument, or whatever,
use this three-argument form::
raise YourExceptionClass(argum, ents), None, sys.exc_info()[2]
In this form, you're creating an instance of new exception, so ``raise``
will deduce its ``type`` and ``value`` parameters from the first argument,
thus the second expression passed to ``raise`` is always ``None``.
And the third one is the traceback object obtained from the thread-safe
``exc_info()`` function.
In fact, if you hadn't already imported the whole ``sys`` module, it will
be better to import just ``exc_info()``::
from sys import exc_info
and raise like this::
raise YourExceptionClass(argum, ents), None, exc_info()[2]
since this is the historically-preferred style in the OfflineIMAP code.
.. -*- coding: utf-8 -*-
.. _OfflineIMAP: https://github.com/OfflineIMAP/offlineimap
.. _OLI_git_repo: git://github.com/OfflineIMAP/offlineimap.git
=================================
Coding guidelines for OfflineIMAP
=================================
.. contents::
.. .. sectnum::
This document contains assorted guidelines for programmers that want
to hack OfflineIMAP.
------------------ ------------------
Exception handling Exception handling
------------------ ------------------

View File

@ -23,7 +23,8 @@ from offlineimap import __version__,__author__
# Add any Sphinx extension module names here, as strings. They can be extensions # Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. # coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx', 'sphinx.ext.todo'] extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest',
'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'sphinx.ext.viewcode']
autoclass_content = "both" autoclass_content = "both"
# Add any paths that contain templates here, relative to this directory. # Add any paths that contain templates here, relative to this directory.

File diff suppressed because it is too large Load Diff

View File

@ -33,15 +33,21 @@ except:
# FIXME: spaghetti code alert! # FIXME: spaghetti code alert!
def getaccountlist(customconfig): def getaccountlist(customconfig):
# Account names in a list.
return customconfig.getsectionlist('Account') return customconfig.getsectionlist('Account')
# FIXME: spaghetti code alert! # FIXME: spaghetti code alert!
def AccountListGenerator(customconfig): def AccountListGenerator(customconfig):
"""Returns a list of instanciated Account class, one per account name."""
return [Account(customconfig, accountname) return [Account(customconfig, accountname)
for accountname in getaccountlist(customconfig)] for accountname in getaccountlist(customconfig)]
# FIXME: spaghetti code alert! # FIXME: spaghetti code alert!
def AccountHashGenerator(customconfig): def AccountHashGenerator(customconfig):
"""Returns a dict of instanciated Account class with the account name as
key."""
retval = {} retval = {}
for item in AccountListGenerator(customconfig): for item in AccountListGenerator(customconfig):
retval[item.getname()] = item retval[item.getname()] = item
@ -54,9 +60,10 @@ class Account(CustomConfig.ConfigHelperMixin):
Most of the time you will actually want to use the derived Most of the time you will actually want to use the derived
:class:`accounts.SyncableAccount` which contains all functions used :class:`accounts.SyncableAccount` which contains all functions used
for syncing an account.""" for syncing an account."""
#signal gets set when we should stop looping
# Signal gets set when we should stop looping.
abort_soon_signal = Event() abort_soon_signal = Event()
#signal gets set on CTRL-C/SIGTERM # Signal gets set on CTRL-C/SIGTERM.
abort_NOW_signal = Event() abort_NOW_signal = Event()
def __init__(self, config, name): def __init__(self, config, name):
@ -66,6 +73,7 @@ class Account(CustomConfig.ConfigHelperMixin):
:param name: A string denoting the name of the Account :param name: A string denoting the name of the Account
as configured""" as configured"""
self.config = config self.config = config
self.name = name self.name = name
self.metadatadir = config.getmetadatadir() self.metadatadir = config.getmetadatadir()
@ -257,8 +265,8 @@ class SyncableAccount(Account):
raise raise
self.ui.error(e, exc_info()[2]) self.ui.error(e, exc_info()[2])
except Exception as e: except Exception as e:
self.ui.error(e, exc_info()[2], msg = "While attempting to sync" self.ui.error(e, exc_info()[2], msg="While attempting to sync"
" account '%s'" % self) " account '%s'"% self)
else: else:
# after success sync, reset the looping counter to 3 # after success sync, reset the looping counter to 3
if self.refreshperiod: if self.refreshperiod:
@ -483,3 +491,7 @@ def syncfolder(account, remotefolder, quick):
ui.error(e, msg = "ERROR in syncfolder for %s folder %s: %s" % \ ui.error(e, msg = "ERROR in syncfolder for %s folder %s: %s" % \
(account, remotefolder.getvisiblename(), (account, remotefolder.getvisiblename(),
traceback.format_exc())) traceback.format_exc()))
finally:
for folder in ["statusfolder", "localfolder", "remotefolder"]:
if folder in locals():
locals()[folder].dropmessagelistcache()

View File

@ -247,6 +247,9 @@ class BaseFolder(object):
raise NotImplementedError raise NotImplementedError
def dropmessagelistcache(self):
raise NotImplementedException
def getmessagelist(self): def getmessagelist(self):
"""Gets the current message list. """Gets the current message list.
You must call cachemessagelist() before calling this function!""" You must call cachemessagelist() before calling this function!"""
@ -438,6 +441,11 @@ class BaseFolder(object):
- headername: name of the header to add - headername: name of the header to add
- headervalue: value of the header to add - headervalue: value of the header to add
.. note::
The following documentation will not get displayed correctly after being
processed by Sphinx. View the source of this method to read it.
This has to deal with strange corner cases where the header is This has to deal with strange corner cases where the header is
missing or empty. Here are illustrations for all the cases, missing or empty. Here are illustrations for all the cases,
showing where the header gets inserted and what the end result showing where the header gets inserted and what the end result

View File

@ -40,6 +40,8 @@ CRLF = '\r\n'
class IMAPFolder(BaseFolder): class IMAPFolder(BaseFolder):
def __init__(self, imapserver, name, repository): 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) name = imaputil.dequote(name)
self.sep = imapserver.delim self.sep = imapserver.delim
super(IMAPFolder, self).__init__(name, repository) super(IMAPFolder, self).__init__(name, repository)
@ -248,6 +250,8 @@ class IMAPFolder(BaseFolder):
rtime = imaplibutil.Internaldate2epoch(messagestr) rtime = imaplibutil.Internaldate2epoch(messagestr)
self.messagelist[uid] = {'uid': uid, 'flags': flags, 'time': rtime} self.messagelist[uid] = {'uid': uid, 'flags': flags, 'time': rtime}
def dropmessagelistcache(self):
self.messagelist = None
# Interface from BaseFolder # Interface from BaseFolder
def getmessagelist(self): def getmessagelist(self):

View File

@ -161,6 +161,8 @@ class LocalStatusFolder(BaseFolder):
self.readstatus(file) self.readstatus(file)
file.close() file.close()
def dropmessagelistcache(self):
self.messagelist = None
def save(self): def save(self):
"""Save changed data to disk. For this backend it is the same as saveall.""" """Save changed data to disk. For this backend it is the same as saveall."""

View File

@ -203,6 +203,9 @@ class LocalStatusSQLiteFolder(BaseFolder):
self.messagelist[uid]['labels'] = labels self.messagelist[uid]['labels'] = labels
self.messagelist[uid]['mtime'] = row[2] self.messagelist[uid]['mtime'] = row[2]
def dropmessagelistcache(self):
self.messagelist = None
# Interface from LocalStatusFolder # Interface from LocalStatusFolder
def save(self): def save(self):
pass pass

View File

@ -220,6 +220,9 @@ class MaildirFolder(BaseFolder):
if self.messagelist is None: if self.messagelist is None:
self.messagelist = self._scanfolder() self.messagelist = self._scanfolder()
def dropmessagelistcache(self):
self.messagelist = None
# Interface from BaseFolder # Interface from BaseFolder
def getmessagelist(self): def getmessagelist(self):
return self.messagelist return self.messagelist

View File

@ -125,6 +125,9 @@ class MappedIMAPFolder(IMAPFolder):
finally: finally:
self.maplock.release() self.maplock.release()
def dropmessagelistcache(self):
self._mb.dropmessagelistcache()
# Interface from BaseFolder # Interface from BaseFolder
def uidexists(self, ruid): def uidexists(self, ruid):
"""Checks if the (remote) UID exists in this Folder""" """Checks if the (remote) UID exists in this Folder"""

View File

@ -40,6 +40,7 @@ class OfflineImap:
oi = OfflineImap() oi = OfflineImap()
oi.run() oi.run()
""" """
def run(self): def run(self):
"""Parse the commandline and invoke everything""" """Parse the commandline and invoke everything"""
# next line also sets self.config and self.ui # next line also sets self.config and self.ui
@ -321,6 +322,8 @@ class OfflineImap:
pass pass
try: try:
# Honor CLI --account option, only.
# Accounts to sync are put into syncaccounts variable.
activeaccounts = self.config.get("general", "accounts") activeaccounts = self.config.get("general", "accounts")
if options.accounts: if options.accounts:
activeaccounts = options.accounts activeaccounts = options.accounts

View File

@ -31,10 +31,10 @@ class LocalEval:
if path is not None: if path is not None:
# FIXME: limit opening files owned by current user with rights set # FIXME: limit opening files owned by current user with rights set
# to fixed mode 644. # to fixed mode 644.
file = open(path, 'r') foo = open(path, 'r')
module = imp.load_module( module = imp.load_module(
'<none>', '<none>',
file, foo,
path, path,
('', 'r', imp.PY_SOURCE)) ('', 'r', imp.PY_SOURCE))
for attr in dir(module): for attr in dir(module):

View File

@ -39,6 +39,7 @@ class BaseRepository(CustomConfig.ConfigHelperMixin, object):
self.mapdir = os.path.join(self.uiddir, 'UIDMapping') self.mapdir = os.path.join(self.uiddir, 'UIDMapping')
if not os.path.exists(self.mapdir): if not os.path.exists(self.mapdir):
os.mkdir(self.mapdir, 0o700) os.mkdir(self.mapdir, 0o700)
# FIXME: self.uiddir variable name is lying about itself.
self.uiddir = os.path.join(self.uiddir, 'FolderValidity') self.uiddir = os.path.join(self.uiddir, 'FolderValidity')
if not os.path.exists(self.uiddir): if not os.path.exists(self.uiddir):
os.mkdir(self.uiddir, 0o700) os.mkdir(self.uiddir, 0o700)

View File

@ -1,5 +1,5 @@
# Maildir repository support # Maildir repository support
# Copyright (C) 2002 John Goerzen # Copyright (C) 2002-2015 John Goerzen & contributors
# <jgoerzen@complete.org> # <jgoerzen@complete.org>
# #
# This program is free software; you can redistribute it and/or modify # This program is free software; you can redistribute it and/or modify
@ -23,8 +23,8 @@ class GmailMaildirRepository(MaildirRepository):
def __init__(self, reposname, account): def __init__(self, reposname, account):
"""Initialize a MaildirRepository object. Takes a path name """Initialize a MaildirRepository object. Takes a path name
to the directory holding all the Maildir directories.""" to the directory holding all the Maildir directories."""
super(GmailMaildirRepository, self).__init__(reposname, account)
super(GmailMaildirRepository, self).__init__(reposname, account)
def getfoldertype(self): def getfoldertype(self):
return GmailMaildirFolder return GmailMaildirFolder

View File

@ -301,6 +301,8 @@ class IMAPRepository(BaseRepository):
return None return None
def getfolder(self, foldername): def getfolder(self, foldername):
"""Return instance of OfflineIMAP representative folder."""
return self.getfoldertype()(self.imapserver, foldername, self) return self.getfoldertype()(self.imapserver, foldername, self)
def getfoldertype(self): def getfoldertype(self):
@ -314,6 +316,8 @@ class IMAPRepository(BaseRepository):
self.folders = None self.folders = None
def getfolders(self): def getfolders(self):
"""Return a list of instances of OfflineIMAP representative folder."""
if self.folders != None: if self.folders != None:
return self.folders return self.folders
retval = [] retval = []
@ -326,13 +330,13 @@ class IMAPRepository(BaseRepository):
listresult = listfunction(directory = self.imapserver.reference)[1] listresult = listfunction(directory = self.imapserver.reference)[1]
finally: finally:
self.imapserver.releaseconnection(imapobj) self.imapserver.releaseconnection(imapobj)
for string in listresult: for s in listresult:
if string == None or \ if s == None or \
(isinstance(string, basestring) and string == ''): (isinstance(s, basestring) and s == ''):
# Bug in imaplib: empty strings in results from # Bug in imaplib: empty strings in results from
# literals. TODO: still relevant? # literals. TODO: still relevant?
continue continue
flags, delim, name = imaputil.imapsplit(string) flags, delim, name = imaputil.imapsplit(s)
flaglist = [x.lower() for x in imaputil.flagsplit(flags)] flaglist = [x.lower() for x in imaputil.flagsplit(flags)]
if '\\noselect' in flaglist: if '\\noselect' in flaglist:
continue continue
@ -353,9 +357,8 @@ class IMAPRepository(BaseRepository):
self.ui.error(e, exc_info()[2], self.ui.error(e, exc_info()[2],
'Invalid folderinclude:') 'Invalid folderinclude:')
continue continue
retval.append(self.getfoldertype()(self.imapserver, retval.append(self.getfoldertype()(
foldername, self.imapserver, foldername, self))
self))
finally: finally:
self.imapserver.releaseconnection(imapobj) self.imapserver.releaseconnection(imapobj)

View File

@ -94,7 +94,8 @@ class LocalStatusRepository(BaseRepository):
self.forgetfolders() self.forgetfolders()
def getfolder(self, foldername): def getfolder(self, foldername):
"""Return the Folder() object for a foldername""" """Return the Folder() object for a foldername."""
if foldername in self._folders: if foldername in self._folders:
return self._folders[foldername] return self._folders[foldername]

View File

@ -170,8 +170,8 @@ class MaildirRepository(BaseRepository):
self.debug(" skip this entry (not a directory)") self.debug(" skip this entry (not a directory)")
# Not a directory -- not a folder. # Not a directory -- not a folder.
continue continue
# extension can be None.
if extension: if extension:
# extension can be None which fails.
foldername = os.path.join(extension, dirname) foldername = os.path.join(extension, dirname)
else: else:
foldername = dirname foldername = dirname