Merge branch 'next'
This commit is contained in:
commit
a73b4b3465
@ -4,17 +4,35 @@ ChangeLog
|
|||||||
|
|
||||||
:website: http://offlineimap.org
|
:website: http://offlineimap.org
|
||||||
|
|
||||||
|
|
||||||
WIP (add new stuff for the next release)
|
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)
|
OfflineIMAP v6.5.4 (2012-06-02)
|
||||||
=================================
|
===============================
|
||||||
|
|
||||||
* bump bundled imaplib2 library 2.29 --> 2.33
|
* bump bundled imaplib2 library 2.29 --> 2.33
|
||||||
* Actually perform the SSL fingerprint check (reported by J. Cook)
|
* 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)
|
* 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)
|
* Fix crash when IMAP.quickchanged() led to an Error (reported by sharat87)
|
||||||
* Implement the createfolders setting to disable folder propagation (see docs)
|
* 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
|
* Improve compatability of the curses UI with python 2.6
|
||||||
|
|
||||||
OfflineIMAP v6.5.2.1 (2012-04-04)
|
OfflineIMAP v6.5.2.1 (2012-04-04)
|
||||||
=====================================
|
=================================
|
||||||
|
|
||||||
* Fix python2.6 compatibility with the TTYUI backend (crash)
|
* 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
|
* 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"
|
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.
|
Blinkenlights UI 6.5.0 regression fixes only.
|
||||||
|
|
||||||
|
19
MAINTAINERS
Normal file
19
MAINTAINERS
Normal 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
12
README
@ -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
|
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
|
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
|
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
|
OfflineIMAP does not require additional python dependencies beyond python >=2.6
|
||||||
(although python-sqlite is strongly recommended).
|
(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
|
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
|
Configuration Examples
|
||||||
======================
|
======================
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
.. -*- coding: utf-8 -*-
|
.. -*- coding: utf-8 -*-
|
||||||
.. _OfflineIMAP: https://github.com/spaetz/offlineimap
|
.. _OfflineIMAP: https://github.com/OfflineIMAP/offlineimap
|
||||||
.. _OLI_git_repo: git://github.com/spaetz/offlineimap.git
|
.. _OLI_git_repo: git://github.com/OfflineIMAP/offlineimap.git
|
||||||
|
|
||||||
============
|
============
|
||||||
Installation
|
Installation
|
||||||
@ -37,29 +37,40 @@ In order to use `OfflineIMAP`_, you need to have these conditions satisfied:
|
|||||||
Installation
|
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 :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 the source package <inst_src_tar>`
|
||||||
#. system-wide or single user :ref:`installation from a git checkout <inst_git>`
|
#. 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:
|
.. _inst_pkg_man:
|
||||||
|
|
||||||
System-Wide Installation via distribution
|
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:
|
.. _inst_src_tar:
|
||||||
|
|
||||||
Installation from source package
|
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:
|
.. _inst_git:
|
||||||
@ -78,7 +89,9 @@ checkout a particular release like this::
|
|||||||
cd offlineimap
|
cd offlineimap
|
||||||
git checkout v6.5.2.1
|
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:
|
.. _system_wide_inst:
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
__all__ = ['OfflineImap']
|
__all__ = ['OfflineImap']
|
||||||
|
|
||||||
__productname__ = 'OfflineIMAP'
|
__productname__ = 'OfflineIMAP'
|
||||||
__version__ = "6.5.4"
|
__version__ = "6.5.5-rc2"
|
||||||
__copyright__ = "Copyright 2002-2012 John Goerzen & contributors"
|
__copyright__ = "Copyright 2002-2012 John Goerzen & contributors"
|
||||||
__author__ = "John Goerzen"
|
__author__ = "John Goerzen"
|
||||||
__author_email__= "john@complete.org"
|
__author_email__= "john@complete.org"
|
||||||
|
@ -217,13 +217,19 @@ class SyncableAccount(Account):
|
|||||||
|
|
||||||
def syncrunner(self):
|
def syncrunner(self):
|
||||||
self.ui.registerthread(self)
|
self.ui.registerthread(self)
|
||||||
accountmetadata = self.getaccountmeta()
|
try:
|
||||||
if not os.path.exists(accountmetadata):
|
accountmetadata = self.getaccountmeta()
|
||||||
os.mkdir(accountmetadata, 0o700)
|
if not os.path.exists(accountmetadata):
|
||||||
|
os.mkdir(accountmetadata, 0o700)
|
||||||
|
|
||||||
self.remoterepos = Repository(self, 'remote')
|
self.remoterepos = Repository(self, 'remote')
|
||||||
self.localrepos = Repository(self, 'local')
|
self.localrepos = Repository(self, 'local')
|
||||||
self.statusrepos = Repository(self, 'status')
|
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)
|
# Loop account sync if needed (bail out after 3 failures)
|
||||||
looping = 3
|
looping = 3
|
||||||
@ -255,6 +261,12 @@ class SyncableAccount(Account):
|
|||||||
if looping and self.sleeper() >= 2:
|
if looping and self.sleeper() >= 2:
|
||||||
looping = 0
|
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):
|
def sync(self):
|
||||||
"""Synchronize the account once, then return
|
"""Synchronize the account once, then return
|
||||||
|
|
||||||
@ -289,7 +301,6 @@ class SyncableAccount(Account):
|
|||||||
# folder delimiter etc)
|
# folder delimiter etc)
|
||||||
remoterepos.getfolders()
|
remoterepos.getfolders()
|
||||||
localrepos.getfolders()
|
localrepos.getfolders()
|
||||||
statusrepos.getfolders()
|
|
||||||
|
|
||||||
remoterepos.sync_folder_structure(localrepos, statusrepos)
|
remoterepos.sync_folder_structure(localrepos, statusrepos)
|
||||||
# replicate the folderstructure between REMOTE to LOCAL
|
# replicate the folderstructure between REMOTE to LOCAL
|
||||||
@ -300,10 +311,16 @@ class SyncableAccount(Account):
|
|||||||
for remotefolder in remoterepos.getfolders():
|
for remotefolder in remoterepos.getfolders():
|
||||||
# check for CTRL-C or SIGTERM
|
# check for CTRL-C or SIGTERM
|
||||||
if Account.abort_NOW_signal.is_set(): break
|
if Account.abort_NOW_signal.is_set(): break
|
||||||
|
|
||||||
if not remotefolder.sync_this:
|
if not remotefolder.sync_this:
|
||||||
self.ui.debug('', "Not syncing filtered remote folder '%s'"
|
self.ui.debug('', "Not syncing filtered folder '%s'"
|
||||||
"[%s]" % (remotefolder, remoterepos))
|
"[%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(\
|
thread = InstanceLimitedThread(\
|
||||||
instancename = 'FOLDER_' + self.remoterepos.getname(),
|
instancename = 'FOLDER_' + self.remoterepos.getname(),
|
||||||
target = syncfolder,
|
target = syncfolder,
|
||||||
@ -367,17 +384,8 @@ def syncfolder(account, remotefolder, quick):
|
|||||||
ui.registerthread(account)
|
ui.registerthread(account)
|
||||||
try:
|
try:
|
||||||
# Load local folder.
|
# Load local folder.
|
||||||
localfolder = localrepos.\
|
localfolder = account.get_local_folder(remotefolder)
|
||||||
getfolder(remotefolder.getvisiblename().\
|
|
||||||
replace(remoterepos.getsep(), localrepos.getsep()))
|
|
||||||
|
|
||||||
#Filtered folders on the remote side will not invoke this
|
|
||||||
#function, but we need to NOOP if the local folder is filtered
|
|
||||||
#out too:
|
|
||||||
if not localfolder.sync_this:
|
|
||||||
ui.debug('', "Not syncing filtered local folder '%s'" \
|
|
||||||
% localfolder)
|
|
||||||
return
|
|
||||||
# Write the mailboxes
|
# Write the mailboxes
|
||||||
mbnames.add(account.name, localfolder.getname())
|
mbnames.add(account.name, localfolder.getname())
|
||||||
|
|
||||||
@ -457,15 +465,8 @@ def syncfolder(account, remotefolder, quick):
|
|||||||
if e.severity > OfflineImapError.ERROR.FOLDER:
|
if e.severity > OfflineImapError.ERROR.FOLDER:
|
||||||
raise
|
raise
|
||||||
else:
|
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' "
|
ui.error(e, exc_info()[2], msg = "Aborting sync, folder '%s' "
|
||||||
"[acc: '%s']" % (
|
"[acc: '%s']" % (localfolder, account))
|
||||||
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.
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
ui.error(e, msg = "ERROR in syncfolder for %s folder %s: %s" % \
|
ui.error(e, msg = "ERROR in syncfolder for %s folder %s: %s" % \
|
||||||
(account, remotefolder.getvisiblename(),
|
(account, remotefolder.getvisiblename(),
|
||||||
|
@ -31,9 +31,12 @@ class BaseFolder(object):
|
|||||||
:para name: Path & name of folder minus root or reference
|
:para name: Path & name of folder minus root or reference
|
||||||
:para repository: Repository() in which the folder is.
|
:para repository: Repository() in which the folder is.
|
||||||
"""
|
"""
|
||||||
self.sync_this = True
|
|
||||||
"""Should this folder be included in syncing?"""
|
|
||||||
self.ui = getglobalui()
|
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 ''
|
# Top level dir name is always ''
|
||||||
self.name = name if not name == self.getsep() else ''
|
self.name = name if not name == self.getsep() else ''
|
||||||
self.repository = repository
|
self.repository = repository
|
||||||
@ -57,6 +60,11 @@ class BaseFolder(object):
|
|||||||
"""Account name as string"""
|
"""Account name as string"""
|
||||||
return self.repository.accountname
|
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):
|
def suggeststhreads(self):
|
||||||
"""Returns true if this folder suggests using threads for actions;
|
"""Returns true if this folder suggests using threads for actions;
|
||||||
false otherwise. Probably only IMAP will return true."""
|
false otherwise. Probably only IMAP will return true."""
|
||||||
@ -386,7 +394,7 @@ class BaseFolder(object):
|
|||||||
self.getmessageuidlist())
|
self.getmessageuidlist())
|
||||||
num_to_copy = len(copylist)
|
num_to_copy = len(copylist)
|
||||||
if num_to_copy and self.repository.account.dryrun:
|
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))
|
num_to_copy, self, self.repository, dstfolder.repository))
|
||||||
return
|
return
|
||||||
for num, uid in enumerate(copylist):
|
for num, uid in enumerate(copylist):
|
||||||
|
@ -575,9 +575,8 @@ class IMAPFolder(BaseFolder):
|
|||||||
(typ,dat) = imapobj.check()
|
(typ,dat) = imapobj.check()
|
||||||
assert(typ == 'OK')
|
assert(typ == 'OK')
|
||||||
|
|
||||||
# get the new UID. Test for APPENDUID response even if the
|
# get the new UID, do we use UIDPLUS?
|
||||||
# server claims to not support it, as e.g. Gmail does :-(
|
if use_uidplus:
|
||||||
if use_uidplus or imapobj._get_untagged_response('APPENDUID', True):
|
|
||||||
# get new UID from the APPENDUID response, it could look
|
# get new UID from the APPENDUID response, it could look
|
||||||
# like OK [APPENDUID 38505 3955] APPEND completed with
|
# like OK [APPENDUID 38505 3955] APPEND completed with
|
||||||
# 38505 bein folder UIDvalidity and 3955 the new UID.
|
# 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
|
# often seems to return [None], even though we have
|
||||||
# data. TODO
|
# data. TODO
|
||||||
resp = imapobj._get_untagged_response('APPENDUID')
|
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 "
|
self.ui.warn("Server supports UIDPLUS but got no APPENDUID "
|
||||||
"appending a message.")
|
"appending a message.")
|
||||||
return 0
|
return 0
|
||||||
|
@ -85,8 +85,7 @@ class LocalStatusFolder(BaseFolder):
|
|||||||
file.close()
|
file.close()
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
self.savelock.acquire()
|
with self.savelock:
|
||||||
try:
|
|
||||||
file = open(self.filename + ".tmp", "wt")
|
file = open(self.filename + ".tmp", "wt")
|
||||||
file.write(magicline + "\n")
|
file.write(magicline + "\n")
|
||||||
for msg in self.messagelist.values():
|
for msg in self.messagelist.values():
|
||||||
@ -104,9 +103,6 @@ class LocalStatusFolder(BaseFolder):
|
|||||||
os.fsync(fd)
|
os.fsync(fd)
|
||||||
os.close(fd)
|
os.close(fd)
|
||||||
|
|
||||||
finally:
|
|
||||||
self.savelock.release()
|
|
||||||
|
|
||||||
def getmessagelist(self):
|
def getmessagelist(self):
|
||||||
return self.messagelist
|
return self.messagelist
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
# along with this program; if not, write to the Free Software
|
# along with this program; if not, write to the Free Software
|
||||||
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
from threading import Lock
|
from threading import Lock
|
||||||
|
from offlineimap import OfflineImapError
|
||||||
from .IMAP import IMAPFolder
|
from .IMAP import IMAPFolder
|
||||||
import os.path
|
import os.path
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ from offlineimap.ui import getglobalui
|
|||||||
|
|
||||||
# find the first quote in a string
|
# find the first quote in a string
|
||||||
quotere = re.compile(
|
quotere = re.compile(
|
||||||
r"""(?P<quote>"(?:\\"|[^"])*") # Quote, possibly containing encoded
|
r"""(?P<quote>"[^\"\\]*(?:\\"|[^"])*") # Quote, possibly containing encoded
|
||||||
# quotation mark
|
# quotation mark
|
||||||
\s*(?P<rest>.*)$ # Whitespace & remainder of string""",
|
\s*(?P<rest>.*)$ # Whitespace & remainder of string""",
|
||||||
re.VERBOSE)
|
re.VERBOSE)
|
||||||
|
@ -129,12 +129,17 @@ class BaseRepository(CustomConfig.ConfigHelperMixin, object):
|
|||||||
def getsep(self):
|
def getsep(self):
|
||||||
raise NotImplementedError
|
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):
|
def get_create_folders(self):
|
||||||
"""Is folder creation enabled on this repository?
|
"""Is folder creation enabled on this repository?
|
||||||
|
|
||||||
It is disabled by either setting the whole repository
|
It is disabled by either setting the whole repository
|
||||||
'readonly' or by using the 'createfolders' setting."""
|
'readonly' or by using the 'createfolders' setting."""
|
||||||
return self._readonly or self.getconfboolean('createfolders', True)
|
return (not self._readonly) and \
|
||||||
|
self.getconfboolean('createfolders', True)
|
||||||
|
|
||||||
def makefolder(self, foldername):
|
def makefolder(self, foldername):
|
||||||
"""Create a new folder"""
|
"""Create a new folder"""
|
||||||
@ -201,7 +206,7 @@ class BaseRepository(CustomConfig.ConfigHelperMixin, object):
|
|||||||
# Does nametrans back&forth lead to identical names?
|
# Does nametrans back&forth lead to identical names?
|
||||||
# 1) would src repo filter out the new folder name? In this
|
# 1) would src repo filter out the new folder name? In this
|
||||||
# case don't create it on it:
|
# 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"
|
self.ui.debug('', "Not creating folder '%s' (repository '%s"
|
||||||
"') as it would be filtered out on that repository." %
|
"') as it would be filtered out on that repository." %
|
||||||
(dst_name_t, self))
|
(dst_name_t, self))
|
||||||
|
@ -287,11 +287,6 @@ class IMAPRepository(BaseRepository):
|
|||||||
foldername = imaputil.dequote(name)
|
foldername = imaputil.dequote(name)
|
||||||
retval.append(self.getfoldertype()(self.imapserver, foldername,
|
retval.append(self.getfoldertype()(self.imapserver, foldername,
|
||||||
self))
|
self))
|
||||||
# filter out the folder?
|
|
||||||
if not self.folderfilter(foldername):
|
|
||||||
self.ui.debug('imap', "Filtering out '%s'[%s] due to folderfilt"
|
|
||||||
"er" % (foldername, self))
|
|
||||||
retval[-1].sync_this = False
|
|
||||||
# Add all folderincludes
|
# Add all folderincludes
|
||||||
if len(self.folderincludes):
|
if len(self.folderincludes):
|
||||||
imapobj = self.imapserver.acquireconnection()
|
imapobj = self.imapserver.acquireconnection()
|
||||||
|
@ -43,8 +43,8 @@ class LocalStatusRepository(BaseRepository):
|
|||||||
if not os.path.exists(self.root):
|
if not os.path.exists(self.root):
|
||||||
os.mkdir(self.root, 0o700)
|
os.mkdir(self.root, 0o700)
|
||||||
|
|
||||||
# self._folders is a list of LocalStatusFolders()
|
# self._folders is a dict of name:LocalStatusFolders()
|
||||||
self._folders = None
|
self._folders = {}
|
||||||
|
|
||||||
def getsep(self):
|
def getsep(self):
|
||||||
return '.'
|
return '.'
|
||||||
@ -79,23 +79,27 @@ class LocalStatusRepository(BaseRepository):
|
|||||||
file.close()
|
file.close()
|
||||||
os.rename(filename + ".tmp", filename)
|
os.rename(filename + ".tmp", filename)
|
||||||
# Invalidate the cache.
|
# Invalidate the cache.
|
||||||
self._folders = None
|
self._folders = {}
|
||||||
|
|
||||||
def getfolder(self, foldername):
|
def getfolder(self, foldername):
|
||||||
"""Return the Folder() object for a 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):
|
def getfolders(self):
|
||||||
"""Returns a list of all cached folders."""
|
"""Returns a list of all cached folders.
|
||||||
if self._folders != None:
|
|
||||||
return self._folders
|
|
||||||
|
|
||||||
self._folders = []
|
Does nothing for this backend. We mangle the folder file names
|
||||||
for folder in os.listdir(self.root):
|
(see getfolderfilename) so we can not derive folder names from
|
||||||
self._folders.append(self.getfolder(folder))
|
the file names that we have available. TODO: need to store a
|
||||||
return self._folders
|
list of folder names somehow?"""
|
||||||
|
pass
|
||||||
|
|
||||||
def forgetfolders(self):
|
def forgetfolders(self):
|
||||||
"""Forgets the cached list of folders, if any. Useful to run
|
"""Forgets the cached list of folders, if any. Useful to run
|
||||||
after a sync run."""
|
after a sync run."""
|
||||||
self._folders = None
|
self._folders = {}
|
||||||
|
@ -181,11 +181,6 @@ class MaildirRepository(BaseRepository):
|
|||||||
foldername,
|
foldername,
|
||||||
self.getsep(),
|
self.getsep(),
|
||||||
self))
|
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 != '':
|
if self.getsep() == '/' and dirname != '':
|
||||||
# Recursively check sub-directories for folders too.
|
# Recursively check sub-directories for folders too.
|
||||||
|
@ -15,10 +15,17 @@
|
|||||||
# along with this program; if not, write to the Free Software
|
# along with this program; if not, write to the Free Software
|
||||||
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
|
||||||
|
try:
|
||||||
|
from configparser import NoSectionError
|
||||||
|
except ImportError: #python2
|
||||||
|
from ConfigParser import NoSectionError
|
||||||
|
|
||||||
from offlineimap.repository.IMAP import IMAPRepository, MappedIMAPRepository
|
from offlineimap.repository.IMAP import IMAPRepository, MappedIMAPRepository
|
||||||
from offlineimap.repository.Gmail import GmailRepository
|
from offlineimap.repository.Gmail import GmailRepository
|
||||||
from offlineimap.repository.Maildir import MaildirRepository
|
from offlineimap.repository.Maildir import MaildirRepository
|
||||||
from offlineimap.repository.LocalStatus import LocalStatusRepository
|
from offlineimap.repository.LocalStatus import LocalStatusRepository
|
||||||
|
from offlineimap.error import OfflineImapError
|
||||||
|
|
||||||
|
|
||||||
class Repository(object):
|
class Repository(object):
|
||||||
"""Abstract class that returns the correct Repository type
|
"""Abstract class that returns the correct Repository type
|
||||||
@ -47,17 +54,26 @@ class Repository(object):
|
|||||||
return LocalStatusRepository(name, account)
|
return LocalStatusRepository(name, account)
|
||||||
|
|
||||||
else:
|
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()
|
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:
|
try:
|
||||||
repo = typemap[repostype]
|
repo = typemap[repostype]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise ValueError("'%s' repository not supported for %s repositories"
|
errstr = "'%s' repository not supported for '%s' repositories." \
|
||||||
"." % (repostype, reqtype))
|
% (repostype, reqtype)
|
||||||
return repo(name, account)
|
raise OfflineImapError(errstr, OfflineImapError.ERROR.REPO)
|
||||||
|
|
||||||
|
return repo(name, account)
|
||||||
|
|
||||||
def __init__(self, account, reqtype):
|
def __init__(self, account, reqtype):
|
||||||
"""Load the correct Repository type and return that. The
|
"""Load the correct Repository type and return that. The
|
||||||
|
@ -88,7 +88,6 @@ class UIBase(object):
|
|||||||
def setlogfile(self, logfile):
|
def setlogfile(self, logfile):
|
||||||
"""Create file handler which logs to file"""
|
"""Create file handler which logs to file"""
|
||||||
fh = logging.FileHandler(logfile, 'at')
|
fh = logging.FileHandler(logfile, 'at')
|
||||||
#fh.setLevel(logging.DEBUG)
|
|
||||||
file_formatter = logging.Formatter("%(asctime)s %(levelname)s: "
|
file_formatter = logging.Formatter("%(asctime)s %(levelname)s: "
|
||||||
"%(message)s", '%Y-%m-%d %H:%M:%S')
|
"%(message)s", '%Y-%m-%d %H:%M:%S')
|
||||||
fh.setFormatter(file_formatter)
|
fh.setFormatter(file_formatter)
|
||||||
@ -98,9 +97,7 @@ class UIBase(object):
|
|||||||
msg = "OfflineImap %s starting...\n Python: %s Platform: %s\n "\
|
msg = "OfflineImap %s starting...\n Python: %s Platform: %s\n "\
|
||||||
"Args: %s" % (offlineimap.__version__, p_ver, sys.platform,
|
"Args: %s" % (offlineimap.__version__, p_ver, sys.platform,
|
||||||
" ".join(sys.argv))
|
" ".join(sys.argv))
|
||||||
record = logging.LogRecord('OfflineImap', logging.INFO, __file__,
|
self.logger.info(msg)
|
||||||
None, msg, None, None)
|
|
||||||
fh.emit(record)
|
|
||||||
|
|
||||||
def _msg(self, msg):
|
def _msg(self, msg):
|
||||||
"""Display a message."""
|
"""Display a message."""
|
||||||
@ -301,7 +298,7 @@ class UIBase(object):
|
|||||||
def makefolder(self, repo, foldername):
|
def makefolder(self, repo, foldername):
|
||||||
"""Called when a folder is created"""
|
"""Called when a folder is created"""
|
||||||
prefix = "[DRYRUN] " if self.dryrun else ""
|
prefix = "[DRYRUN] " if self.dryrun else ""
|
||||||
self.info("{}Creating folder {}[{}]".format(
|
self.info("{0}Creating folder {1}[{2}]".format(
|
||||||
prefix, foldername, repo))
|
prefix, foldername, repo))
|
||||||
|
|
||||||
def syncingfolder(self, srcrepos, srcfolder, destrepos, destfolder):
|
def syncingfolder(self, srcrepos, srcfolder, destrepos, destfolder):
|
||||||
@ -346,7 +343,7 @@ class UIBase(object):
|
|||||||
def deletingmessages(self, uidlist, destlist):
|
def deletingmessages(self, uidlist, destlist):
|
||||||
ds = self.folderlist(destlist)
|
ds = self.folderlist(destlist)
|
||||||
prefix = "[DRYRUN] " if self.dryrun else ""
|
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),
|
prefix, len(uidlist),
|
||||||
offlineimap.imaputil.uid_sequence(uidlist), ds))
|
offlineimap.imaputil.uid_sequence(uidlist), ds))
|
||||||
|
|
||||||
@ -474,7 +471,7 @@ class UIBase(object):
|
|||||||
|
|
||||||
def callhook(self, msg):
|
def callhook(self, msg):
|
||||||
if self.dryrun:
|
if self.dryrun:
|
||||||
self.info("[DRYRUN] {}".format(msg))
|
self.info("[DRYRUN] {0}".format(msg))
|
||||||
else:
|
else:
|
||||||
self.info(msg)
|
self.info(msg)
|
||||||
|
|
||||||
|
@ -127,7 +127,7 @@ class OLITestLib():
|
|||||||
reponame: All on `reponame` or all IMAP-type repositories if None"""
|
reponame: All on `reponame` or all IMAP-type repositories if None"""
|
||||||
config = cls.get_default_config()
|
config = cls.get_default_config()
|
||||||
if reponame:
|
if reponame:
|
||||||
sections = ['Repository {}'.format(reponame)]
|
sections = ['Repository {0}'.format(reponame)]
|
||||||
else:
|
else:
|
||||||
sections = [r for r in config.sections() \
|
sections = [r for r in config.sections() \
|
||||||
if r.startswith('Repository')]
|
if r.startswith('Repository')]
|
||||||
@ -162,8 +162,8 @@ class OLITestLib():
|
|||||||
dirs = [d for d in dirs if d.startswith(b'INBOX.OLItest')]
|
dirs = [d for d in dirs if d.startswith(b'INBOX.OLItest')]
|
||||||
for folder in dirs:
|
for folder in dirs:
|
||||||
res_t, data = imapobj.delete(b'\"'+folder+b'\"')
|
res_t, data = imapobj.delete(b'\"'+folder+b'\"')
|
||||||
assert res_t == 'OK', "Folder deletion of {} failed with error"\
|
assert res_t == 'OK', "Folder deletion of {0} failed with error"\
|
||||||
":\n{} {}".format(folder.decode('utf-8'), res_t, data)
|
":\n{1} {2}".format(folder.decode('utf-8'), res_t, data)
|
||||||
imapobj.logout()
|
imapobj.logout()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -197,7 +197,7 @@ class OLITestLib():
|
|||||||
Use some default content if not given"""
|
Use some default content if not given"""
|
||||||
assert cls.testdir != None
|
assert cls.testdir != None
|
||||||
while True: # Loop till we found a unique filename
|
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',
|
mailfilepath = os.path.join(cls.testdir, 'mail',
|
||||||
folder, 'new', mailfile)
|
folder, 'new', mailfile)
|
||||||
if not os.path.isfile(mailfilepath):
|
if not os.path.isfile(mailfilepath):
|
||||||
|
@ -67,7 +67,7 @@ class TestBasicFunctions(unittest.TestCase):
|
|||||||
self.assertEqual(res, "")
|
self.assertEqual(res, "")
|
||||||
boxes, mails = OLITestLib.count_maildir_mails('')
|
boxes, mails = OLITestLib.count_maildir_mails('')
|
||||||
self.assertTrue((boxes, mails)==(0,0), msg="Expected 0 folders and 0 "
|
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))
|
boxes, mails))
|
||||||
|
|
||||||
def test_02_createdir(self):
|
def test_02_createdir(self):
|
||||||
@ -82,7 +82,7 @@ class TestBasicFunctions(unittest.TestCase):
|
|||||||
self.assertEqual(res, "")
|
self.assertEqual(res, "")
|
||||||
boxes, mails = OLITestLib.count_maildir_mails('')
|
boxes, mails = OLITestLib.count_maildir_mails('')
|
||||||
self.assertTrue((boxes, mails)==(2,0), msg="Expected 2 folders and 0 "
|
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))
|
boxes, mails))
|
||||||
|
|
||||||
def test_03_nametransmismatch(self):
|
def test_03_nametransmismatch(self):
|
||||||
@ -101,7 +101,7 @@ class TestBasicFunctions(unittest.TestCase):
|
|||||||
mismatch = "ERROR: INFINITE FOLDER CREATION DETECTED!" in res
|
mismatch = "ERROR: INFINITE FOLDER CREATION DETECTED!" in res
|
||||||
self.assertEqual(mismatch, True, msg="Mismatching nametrans rules did "
|
self.assertEqual(mismatch, True, msg="Mismatching nametrans rules did "
|
||||||
"NOT trigger an 'infinite folder generation' error. Output was:\n"
|
"NOT trigger an 'infinite folder generation' error. Output was:\n"
|
||||||
"{}".format(res))
|
"{0}".format(res))
|
||||||
# Write out default config file again
|
# Write out default config file again
|
||||||
OLITestLib.write_config_file()
|
OLITestLib.write_config_file()
|
||||||
|
|
||||||
@ -121,12 +121,12 @@ class TestBasicFunctions(unittest.TestCase):
|
|||||||
self.assertEqual(res, "")
|
self.assertEqual(res, "")
|
||||||
boxes, mails = OLITestLib.count_maildir_mails('')
|
boxes, mails = OLITestLib.count_maildir_mails('')
|
||||||
self.assertTrue((boxes, mails)==(1,1), msg="Expected 1 folders and 1 "
|
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))
|
boxes, mails))
|
||||||
# The local Mail should have been assigned a proper UID now, check!
|
# The local Mail should have been assigned a proper UID now, check!
|
||||||
uids = OLITestLib.get_maildir_uids('INBOX.OLItest')
|
uids = OLITestLib.get_maildir_uids('INBOX.OLItest')
|
||||||
self.assertFalse (None in uids, msg = "All mails should have been "+ \
|
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])))
|
.format(len([None for x in uids if x==None])))
|
||||||
|
|
||||||
def test_05_createfolders(self):
|
def test_05_createfolders(self):
|
||||||
|
Loading…
Reference in New Issue
Block a user