Merge branch 'next'

This commit is contained in:
Nicolas Sebrecht 2015-01-07 22:21:09 +01:00
commit d28ea704c4
32 changed files with 528 additions and 360 deletions

View File

@ -5,8 +5,26 @@ ChangeLog
:website: http://offlineimap.org :website: http://offlineimap.org
OfflineIMAP v6.5.6.1 (YYYY-MM-DD) OfflineIMAP v6.5.7-rc1 (2015-01-07)
================================= ===================================
Notes
-----
I think it's time for a new release candidate. Our release cycle are long
enough and users are asked to use the current TIP of the next branch to test
our recent patches.
The current version makes better support for environment variable expansion and
improves OS portability. Gmail should be better supported: we are still
expecting feedbacks. Embedded library imaplib2 is updated to v2.37.
Debugging messages are added and polished.
There's some code cleanups and refactoring, also.
Features
--------
* Expand environment variables in the following * Expand environment variables in the following
configuration items: configuration items:
@ -19,39 +37,73 @@ OfflineIMAP v6.5.6.1 (YYYY-MM-DD)
configuration items: configuration items:
- Repository.sslclientcert; - Repository.sslclientcert;
- Repository.sslclientkey. - Repository.sslclientkey.
* Support default CA bundle locations for a couple of
* Updated bundled imaplib2 to 2.37: known Unix systems (Michael Vogt, GutHub pull #19)
- add missing idle_lock in _handler()
* Added default CA bundle location for OpenBSD * Added default CA bundle location for OpenBSD
(GitHub pull #120) and DragonFlyBSD. (GitHub pull #120) and DragonFlyBSD.
Fixes
-----
* Fix unbounded recursion during flag update (Josh Berry).
* Do not ignore gmail labels if header appears multiple times
* Delete gmail labels header before adding a new one
* Fix improper header separator for X-OfflineIMAP header
* Match header names case-insensitively
* Create SQLite database directory if it doesn't exist
yet; warn if path is not a directory (Nick Farrell,
GutHub pull #102)
* Properly manipulate contents of messagelist for folder
* Fix label processing in GmailMaildir
* Properly capitalize OpenSSL
* Fix warning-level message processing by MachineUI
(GitHub pull #64, GitHub pull #118).
* Properly generate tarball from "sdist" command (GitHub #137)
* Fix Markdown formatting
* Fix typo in apply_xforms invocation
* Merge pull request #136 from aroig/gh/label-fix
* Fix mangled message headers for servers without UIDPLUS:
X-OfflineIMAP was added with preceeding '\n' instead of
'\r\n' just before message was uploaded to the IMAP server.
* Add missing version bump for 6.5.6 (it was released with
6.5.5 in setup.py and other places).
Changes
-------
* Warn about a tricky piece of code in addmessageheader
* Rename addmessageheader()'s crlf parameter to linebreak
* addmessageheader: fix case #2 and flesh out docstring
* addmessageheader(): add debug for header insertion
* Add version qualifier to differentiate releases and development ones
* More clearly show results of folder name translation
* IMAP: provide message-id in error messages
* Trade recursion by plain old cycle
* Avoid copying array every time, just slice it
* Added OpenSSL exception clause to our main GPL to allow * Added OpenSSL exception clause to our main GPL to allow
people to link with OpenSSL in run-time. It is needed people to link with OpenSSL in run-time. It is needed
at least for Debian, see at least for Debian, see
https://lists.debian.org/debian-legal/2002/10/msg00113.html https://lists.debian.org/debian-legal/2002/10/msg00113.html
for details. for details.
* Brought CustomConfig.py into more proper shape
* Updated bundled imaplib2 to 2.37:
- add missing idle_lock in _handler()
* Imaplib2: trade backticks to repr()
* Introduce CustomConfig method that applies set of transforms
* imaplibutil.py: remove unused imports
* CustomConfig.py: remove unused imports
* init.py: remove unused import
* repository/Base.py: remove unused import
* repository/GmailMaildir.py: remove unused import
* repository/LocalStatus.py: remove unused import
* ui/Curses.py: remove unused import
* ui/UIBase.py: remove unused import
* localeval: comment on security issues
* docs: remove obsolete comment about SubmittingPatches.rst
* utils/const.py: fix ident
* ui/UIBase: folderlist(): avoid built-in list() redefinition
* more consistent style
* Fix warning-level message processing by MachineUI
(GitHub pull #64, GitHub pull #118).
* Support default CA bundle locations for a couple of
known Unix systems (Michael Vogt, GutHub pull #19)
* Create SQLite database directory if it doesn't exist
yet; warn if path is not a directory (Nick Farrell,
GutHub pull #102)
* Fix mangled message headers for servers without UIDPLUS:
X-OfflineIMAP was added with preceeding '\n' instead of
'\r\n' just before message was uploaded to the IMAP server.
* Add missing version bump for 6.5.6 (it was released with
6.5.5 in setup.py and other places).
* Various fixes in documentation.
* Fix unbounded recursion during flag update (Josh Berry).
OfflineIMAP v6.5.6 (2014-05-14) OfflineIMAP v6.5.6 (2014-05-14)
@ -270,7 +322,9 @@ OfflineIMAP v6.5.1 (2012-01-07) - "Quest for stability"
OfflineIMAP v6.5.0 (2012-01-06) OfflineIMAP v6.5.0 (2012-01-06)
=============================== ===============================
This is a CRITICAL bug fix release for everyone who is on the 6.4.x series. Please upgrade to avoid potential data loss! The version has been bumped to 6.5.0, please let everyone know that the 6.4.x series is problematic. This is a CRITICAL bug fix release for everyone who is on the 6.4.x series.
Please upgrade to avoid potential data loss! The version has been bumped to
6.5.0, please let everyone know that the 6.4.x series is problematic.
* Uploading multiple emails to an IMAP server would lead to wrong UIDs * Uploading multiple emails to an IMAP server would lead to wrong UIDs
being returned (ie the same for all), which confused offlineimap and being returned (ie the same for all), which confused offlineimap and
@ -366,7 +420,8 @@ Bug Fixes
OfflineIMAP v6.4.0 (2011-09-29) OfflineIMAP v6.4.0 (2011-09-29)
=============================== ===============================
This is the first stable release to support the forward-compatible per-account locks and remote folder creation that has been introduced in the 6.3.5 series. This is the first stable release to support the forward-compatible per-account
locks and remote folder creation that has been introduced in the 6.3.5 series.
* Various regression and bug fixes from the last couple of RCs * Various regression and bug fixes from the last couple of RCs

14
MANIFEST.in Normal file
View File

@ -0,0 +1,14 @@
global-exclude .gitignore .git *.bak *.orig *.rej
include setup.py
include COPYING
include Changelog*
include MAINTAINERS
include MANIFEST.in
include Makefile
include README.md
include offlineimap.conf*
include offlineimap.py
recursive-include offlineimap *.py
recursive-include bin *
recursive-include docs *
recursive-include test *

View File

@ -15,10 +15,9 @@ OfflineImap can be imported as::
from offlineimap import OfflineImap from offlineimap import OfflineImap
The file ``SubmittingPatches.rst`` in the source distribution documents a The file ``HACKING.rst`` in the source distribution documents a
number of resources and conventions you may find useful. It will eventually number of resources and conventions you may find useful.
be merged into the main documentation.
.. TODO: merge SubmittingPatches.rst to the main documentation
:mod:`offlineimap` -- The OfflineImap module :mod:`offlineimap` -- The OfflineImap module
============================================= =============================================

View File

@ -1,9 +1,11 @@
Message filtering Message filtering
================= =================
There are two ways to selectively filter messages out of a folder, using `maxsize` and `maxage`. Setting each option will basically ignore all messages that are on the server by pretending they don't exist. There are two ways to selectively filter messages out of a folder, using
`maxsize` and `maxage`. Setting each option will basically ignore all messages
that are on the server by pretending they don't exist.
:todo: explain them and give tipps on how to use and not use them. Use cases! :todo: explain them and give tips on how to use and not use them. Use cases!
maxage maxage
------ ------

View File

@ -13,7 +13,10 @@ safely skip this section.
folderfilter folderfilter
------------ ------------
If you do not want to synchronize all your folders, you can specify a `folderfilter`_ function that determines which folders to include in a sync and which to exclude. Typically, you would set a folderfilter option on the remote repository only, and it would be a lambda or any other python function. If you do not want to synchronize all your folders, you can specify a
`folderfilter`_ function that determines which folders to include in a sync and
which to exclude. Typically, you would set a folderfilter option on the remote
repository only, and it would be a lambda or any other python function.
The only parameter to that function is the folder name. If the filter The only parameter to that function is the folder name. If the filter
function returns True, the folder will be synced, if it returns False, function returns True, the folder will be synced, if it returns False,
@ -45,12 +48,18 @@ at the end when required by Python syntax) For instance::
['INBOX', 'Sent Mail', 'Deleted Items', ['INBOX', 'Sent Mail', 'Deleted Items',
'Received'] 'Received']
Usually it suffices to put a `folderfilter`_ setting in the remote repository section. You might want to put a folderfilter option on the local repository if you want to prevent some folders on the local repository to be created on the remote one. (Even in this case, folder filters on the remote repository will prevent that) Usually it suffices to put a `folderfilter`_ setting in the remote repository
section. You might want to put a folderfilter option on the local repository if
you want to prevent some folders on the local repository to be created on the
remote one. (Even in this case, folder filters on the remote repository will
prevent that)
folderincludes folderincludes
-------------- --------------
You can specify `folderincludes`_ to manually include additional folders to be synced, even if they had been filtered out by a folderfilter setting. `folderincludes`_ should return a Python list. You can specify `folderincludes`_ to manually include additional folders to be
synced, even if they had been filtered out by a folderfilter setting.
`folderincludes`_ should return a Python list.
This can be used to 1) add a folder that was excluded by your This can be used to 1) add a folder that was excluded by your
folderfilter rule, 2) to include a folder that your server does not specify folderfilter rule, 2) to include a folder that your server does not specify
@ -84,11 +93,14 @@ nametrans`_ rules on the LOCAL repository.
nametrans nametrans
---------- ----------
Sometimes, folders need to have different names on the remote and the Sometimes, folders need to have different names on the remote and the local
local repositories. To achieve this you can specify a folder name repositories. To achieve this you can specify a folder name translator. This
translator. This must be a eval-able Python expression that takes a must be a eval-able Python expression that takes a foldername arg and returns
foldername arg and returns the new value. We suggest a lambda function, the new value. We suggest a lambda function, but it could be any python
but it could be any python function really. If you use nametrans rules, you will need to set them both on the remote and the local repository, see `Reverse nametrans`_ just below for details. The following examples are thought to be put in the remote repository section. function really. If you use nametrans rules, you will need to set them both on
the remote and the local repository, see `Reverse nametrans`_ just below for
details. The following examples are thought to be put in the remote repository
section.
The below will remove "INBOX." from the leading edge of folders (great The below will remove "INBOX." from the leading edge of folders (great
for Courier IMAP users):: for Courier IMAP users)::
@ -112,38 +124,59 @@ locally? Try this::
Reverse nametrans Reverse nametrans
+++++++++++++++++ +++++++++++++++++
Since 6.4.0, OfflineImap supports the creation of folders on the remote repository and that complicates things. Previously, only one nametrans setting on the remote repository was needed and that transformed a remote to a local name. However, nametrans transformations are one-way, and OfflineImap has no way using those rules on the remote repository to back local names to remote names. Since 6.4.0, OfflineImap supports the creation of folders on the remote
repository and that complicates things. Previously, only one nametrans setting
on the remote repository was needed and that transformed a remote to a local
name. However, nametrans transformations are one-way, and OfflineImap has no way
using those rules on the remote repository to back local names to remote names.
Take a remote nametrans rule `lambda f: re.sub('^INBOX/','',f)` which cuts off any existing INBOX prefix. Now, if we parse a list of local folders, finding e.g. a folder "Sent", is it supposed to map to "INBOX/Sent" or to "Sent"? We have no way of knowing. This is why **every nametrans setting on a remote repository requires an equivalent nametrans rule on the local repository that reverses the transformation**. Take a remote nametrans rule `lambda f: re.sub('^INBOX/','',f)` which cuts off
any existing INBOX prefix. Now, if we parse a list of local folders, finding
e.g. a folder "Sent", is it supposed to map to "INBOX/Sent" or to "Sent"? We
have no way of knowing. This is why **every nametrans setting on a remote
repository requires an equivalent nametrans rule on the local repository that
reverses the transformation**.
Take the above examples. If your remote nametrans setting was:: Take the above examples. If your remote nametrans setting was::
nametrans = lambda folder: re.sub('^INBOX\.', '', folder) nametrans = lambda folder: re.sub('^INBOX\.', '', folder)
then you will want to have this in your local repository, prepending "INBOX" to any local folder name:: then you will want to have this in your local repository, prepending "INBOX" to
any local folder name::
nametrans = lambda folder: 'INBOX.' + folder nametrans = lambda folder: 'INBOX.' + folder
Failure to set the local nametrans rule will lead to weird-looking error messages of -for instance- this type:: Failure to set the local nametrans rule will lead to weird-looking error
messages of -for instance- this type::
ERROR: Creating folder moo.foo on repository remote ERROR: Creating folder moo.foo on repository remote
Folder 'moo.foo'[remote] could not be created. Server responded: ('NO', ['Unknown namespace.']) Folder 'moo.foo'[remote] could not be created. Server responded: ('NO', ['Unknown namespace.'])
(This indicates that you attempted to create a folder "Sent" when all remote folders needed to be under the prefix of "INBOX."). (This indicates that you attempted to create a folder "Sent" when all remote
folders needed to be under the prefix of "INBOX.").
OfflineImap will make some sanity checks if it needs to create a new OfflineImap will make some sanity checks if it needs to create a new
folder on the remote side and a back-and-forth nametrans-lation does not folder on the remote side and a back-and-forth nametrans-lation does not
yield the original foldername (as that could potentially lead to yield the original foldername (as that could potentially lead to
infinite folder creation cycles). infinite folder creation cycles).
You can probably already see now that creating nametrans rules can be a pretty daunting and complex endeavour. Check out the Use cases in the manual. If you have some interesting use cases that we can present as examples here, please let us know. You can probably already see now that creating nametrans rules can be a pretty
daunting and complex endeavour. Check out the Use cases in the manual. If you
have some interesting use cases that we can present as examples here, please let
us know.
Debugging folderfilter and nametrans Debugging folderfilter and nametrans
------------------------------------ ------------------------------------
Given the complexity of the functions and regexes involved, it is easy to misconfigure things. One way to test your configuration without danger to corrupt anything or to create unwanted folders is to invoke offlineimap with the `--info` option. Given the complexity of the functions and regexes involved, it is easy to
misconfigure things. One way to test your configuration without danger to
corrupt anything or to create unwanted folders is to invoke offlineimap with the
`--info` option.
It will output a list of folders and their transformations on the screen (save them to a file with -l info.log), and will help you to tweak your rules as well as to understand your configuration. It also provides good output for bug reporting. It will output a list of folders and their transformations on the screen (save
them to a file with -l info.log), and will help you to tweak your rules as well
as to understand your configuration. It also provides good output for bug
reporting.
FAQ on nametrans FAQ on nametrans
---------------- ----------------

View File

@ -3,7 +3,10 @@
.. currentmodule:: offlineimap.ui .. currentmodule:: offlineimap.ui
OfflineImap has various ui systems, that can be selected. They offer various functionalities. They must implement all functions that the :class:`offlineimap.ui.UIBase` offers. Early on, the ui must be set using :meth:`getglobalui` OfflineImap has various ui systems, that can be selected. They offer various
functionalities. They must implement all functions that the
:class:`offlineimap.ui.UIBase` offers. Early on, the ui must be set using
:meth:`getglobalui`
.. automethod:: offlineimap.ui.setglobalui .. automethod:: offlineimap.ui.setglobalui
.. automethod:: offlineimap.ui.getglobalui .. automethod:: offlineimap.ui.getglobalui

View File

@ -1,4 +1,4 @@
# Copyright (C) 2003-2012 John Goerzen & contributors # Copyright (C) 2003-2015 John Goerzen & contributors
# #
# This program is free software; you can redistribute it and/or modify # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -14,21 +14,20 @@
# 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 SafeConfigParser, Error, NoOptionError
except ImportError: #python3
from configparser import SafeConfigParser, Error, NoOptionError
from offlineimap.localeval import LocalEval
import os import os
import re import re
try:
from ConfigParser import SafeConfigParser, Error
except ImportError: #python3
from configparser import SafeConfigParser, Error
from offlineimap.localeval import LocalEval
class CustomConfigParser(SafeConfigParser): class CustomConfigParser(SafeConfigParser):
def getdefault(self, section, option, default, *args, **kwargs): def getdefault(self, section, option, default, *args, **kwargs):
""" """Same as config.get, but returns the value of `default`
Same as config.get, but returns the value of `default` if there is no such option specified."""
if there is no such option specified.
"""
if self.has_option(section, option): if self.has_option(section, option):
return self.get(*(section, option) + args, **kwargs) return self.get(*(section, option) + args, **kwargs)
else: else:
@ -36,45 +35,37 @@ class CustomConfigParser(SafeConfigParser):
def getdefaultint(self, section, option, default, *args, **kwargs): def getdefaultint(self, section, option, default, *args, **kwargs):
""" """Same as config.getint, but returns the value of `default`
Same as config.getint, but returns the value of `default` if there is no such option specified."""
if there is no such option specified.
"""
if self.has_option(section, option): if self.has_option(section, option):
return self.getint (*(section, option) + args, **kwargs) return self.getint(*(section, option) + args, **kwargs)
else: else:
return default return default
def getdefaultfloat(self, section, option, default, *args, **kwargs): def getdefaultfloat(self, section, option, default, *args, **kwargs):
""" """Same as config.getfloat, but returns the value of `default`
Same as config.getfloat, but returns the value of `default` if there is no such option specified."""
if there is no such option specified.
"""
if self.has_option(section, option): if self.has_option(section, option):
return self.getfloat(*(section, option) + args, **kwargs) return self.getfloat(*(section, option) + args, **kwargs)
else: else:
return default return default
def getdefaultboolean(self, section, option, default, *args, **kwargs): def getdefaultboolean(self, section, option, default, *args, **kwargs):
""" """Same as config.getboolean, but returns the value of `default`
Same as config.getboolean, but returns the value of `default` if there is no such option specified."""
if there is no such option specified.
"""
if self.has_option(section, option): if self.has_option(section, option):
return self.getboolean(*(section, option) + args, **kwargs) return self.getboolean(*(section, option) + args, **kwargs)
else: else:
return default return default
def getlist(self, section, option, separator_re): def getlist(self, section, option, separator_re):
""" """Parses option as the list of values separated
Parses option as the list of values separated by the given regexp."""
by the given regexp.
"""
try: try:
val = self.get(section, option).strip() val = self.get(section, option).strip()
return re.split(separator_re, val) return re.split(separator_re, val)
@ -83,11 +74,9 @@ class CustomConfigParser(SafeConfigParser):
(separator_re, e)) (separator_re, e))
def getdefaultlist(self, section, option, default, separator_re): def getdefaultlist(self, section, option, default, separator_re):
""" """Same as getlist, but returns the value of `default`
Same as getlist, but returns the value of `default` if there is no such option specified."""
if there is no such option specified.
"""
if self.has_option(section, option): if self.has_option(section, option):
return self.getlist(*(section, option, separator_re)) return self.getlist(*(section, option, separator_re))
else: else:
@ -104,40 +93,48 @@ class CustomConfigParser(SafeConfigParser):
def getlocaleval(self): def getlocaleval(self):
xforms = [os.path.expanduser, os.path.expandvars] xforms = [os.path.expanduser, os.path.expandvars]
if self.has_option("general", "pythonfile"): if self.has_option("general", "pythonfile"):
path = self.apply_xforms(self.get("general", "pythonfile"), xforms) if globals.options.use_unicode:
path = uni.fsEncode(self.get("general", "pythonfile"),
exception_msg="cannot convert character for pythonfile")
else:
path = self.get("general", "pythonfile")
path = self.apply_xforms(path, xforms)
else: else:
path = None path = None
return LocalEval(path) return LocalEval(path)
def getsectionlist(self, key): def getsectionlist(self, key):
""" """Returns a list of sections that start with (str) key + " ".
Returns a list of sections that start with key + " ".
That is, if key is "Account", returns all section names that That is, if key is "Account", returns all section names that
start with "Account ", but strips off the "Account ". start with "Account ", but strips off the "Account ".
For instance, for "Account Test", returns "Test". For instance, for "Account Test", returns "Test"."""
"""
key = key + ' ' key = key + ' '
if globals.options.use_unicode:
sections = []
for section in self.sections():
sections.append(uni.uni2str(section, exception_msg=
"non ASCII character in section %s"% section))
return [x[len(key):] for x in sections \
if x.startswith(key)]
else:
return [x[len(key):] for x in self.sections() \ return [x[len(key):] for x in self.sections() \
if x.startswith(key)] if x.startswith(key)]
def set_if_not_exists(self, section, option, value): def set_if_not_exists(self, section, option, value):
""" """Set a value if it does not exist yet.
Set a value if it does not exist yet
This allows to set default if the user has not explicitly This allows to set default if the user has not explicitly
configured anything. configured anything."""
"""
if not self.has_option(section, option): if not self.has_option(section, option):
self.set(section, option, value) self.set(section, option, value)
def apply_xforms(self, string, transforms): def apply_xforms(self, string, transforms):
""" """Applies set of transformations to a string.
Applies set of transformations to a string.
Arguments: Arguments:
- string: source string; if None, then no processing will - string: source string; if None, then no processing will
@ -145,9 +142,8 @@ class CustomConfigParser(SafeConfigParser):
- transforms: iterable that returns transformation function - transforms: iterable that returns transformation function
on each turn. on each turn.
Returns transformed string. Returns transformed string."""
"""
if string == None: if string == None:
return None return None
for f in transforms: for f in transforms:
@ -157,21 +153,18 @@ class CustomConfigParser(SafeConfigParser):
def CustomConfigDefault(): def CustomConfigDefault():
""" """Just a constant that won't occur anywhere else.
Just a constant that won't occur anywhere else.
This allows us to differentiate if the user has passed in any This allows us to differentiate if the user has passed in any
default value to the getconf* functions in ConfigHelperMixin default value to the getconf* functions in ConfigHelperMixin
derived classes. derived classes."""
"""
pass pass
class ConfigHelperMixin: class ConfigHelperMixin:
""" """Allow comfortable retrieving of config values pertaining
Allow comfortable retrieving of config values pertaining
to a section. to a section.
If a class inherits from cls:`ConfigHelperMixin`, it needs If a class inherits from cls:`ConfigHelperMixin`, it needs
@ -181,13 +174,10 @@ class ConfigHelperMixin:
the section to look up). the section to look up).
All calls to getconf* will then return the configuration values All calls to getconf* will then return the configuration values
for the CustomConfigParser object in the specific section. for the CustomConfigParser object in the specific section.
""" """
def _confighelper_runner(self, option, default, defaultfunc, mainfunc, *args): def _confighelper_runner(self, option, default, defaultfunc, mainfunc, *args):
""" """Returns configuration or default value for option
Returns configuration or default value for option
that contains in section identified by getsection(). that contains in section identified by getsection().
Arguments: Arguments:
@ -201,8 +191,8 @@ class ConfigHelperMixin:
- defaultfunc and mainfunc: processing helpers. - defaultfunc and mainfunc: processing helpers.
- args: additional trailing arguments that will be passed - args: additional trailing arguments that will be passed
to all processing helpers. to all processing helpers.
""" """
lst = [self.getsection(), option] lst = [self.getsection(), option]
if default == CustomConfigDefault: if default == CustomConfigDefault:
return mainfunc(*(lst + list(args))) return mainfunc(*(lst + list(args)))
@ -210,50 +200,43 @@ class ConfigHelperMixin:
lst.append(default) lst.append(default)
return defaultfunc(*(lst + list(args))) return defaultfunc(*(lst + list(args)))
def getconfig(self): def getconfig(self):
""" """Returns CustomConfigParser object that we will use
Returns CustomConfigParser object that we will use
for all our actions. for all our actions.
Must be overriden in all classes that use this mix-in. Must be overriden in all classes that use this mix-in."""
"""
raise NotImplementedError("ConfigHelperMixin.getconfig() " raise NotImplementedError("ConfigHelperMixin.getconfig() "
"is to be overriden") "is to be overriden")
def getsection(self): def getsection(self):
""" """Returns name of configuration section in which our
Returns name of configuration section in which our
class keeps its configuration. class keeps its configuration.
Must be overriden in all classes that use this mix-in. Must be overriden in all classes that use this mix-in."""
"""
raise NotImplementedError("ConfigHelperMixin.getsection() " raise NotImplementedError("ConfigHelperMixin.getsection() "
"is to be overriden") "is to be overriden")
def getconf(self, option, default = CustomConfigDefault): def getconf(self, option, default = CustomConfigDefault):
""" """Retrieves string from the configuration.
Retrieves string from the configuration.
Arguments: Arguments:
- option: option name whose value is to be retrieved; - option: option name whose value is to be retrieved;
- default: default return value if no such option - default: default return value if no such option
exists. exists.
""" """
return self._confighelper_runner(option, default, return self._confighelper_runner(option, default,
self.getconfig().getdefault, self.getconfig().getdefault,
self.getconfig().get) self.getconfig().get)
def getconf_xform(self, option, xforms, default = CustomConfigDefault): def getconf_xform(self, option, xforms, default = CustomConfigDefault):
""" """Retrieves string from the configuration transforming the result.
Retrieves string from the configuration transforming the result.
Arguments: Arguments:
- option: option name whose value is to be retrieved; - option: option name whose value is to be retrieved;
@ -262,22 +245,21 @@ class ConfigHelperMixin:
both retrieved and default one; both retrieved and default one;
- default: default value for string if no such option - default: default value for string if no such option
exists. exists.
""" """
value = self.getconf(option, default) 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): def getconfboolean(self, option, default = CustomConfigDefault):
""" """Retrieves boolean value from the configuration.
Retrieves boolean value from the configuration.
Arguments: Arguments:
- option: option name whose value is to be retrieved; - option: option name whose value is to be retrieved;
- default: default return value if no such option - default: default return value if no such option
exists. exists.
""" """
return self._confighelper_runner(option, default, return self._confighelper_runner(option, default,
self.getconfig().getdefaultboolean, self.getconfig().getdefaultboolean,
self.getconfig().getboolean) self.getconfig().getboolean)
@ -293,21 +275,21 @@ class ConfigHelperMixin:
exists. exists.
""" """
return self._confighelper_runner(option, default, return self._confighelper_runner(option, default,
self.getconfig().getdefaultint, self.getconfig().getdefaultint,
self.getconfig().getint) self.getconfig().getint)
def getconffloat(self, option, default = CustomConfigDefault): def getconffloat(self, option, default = CustomConfigDefault):
""" """Retrieves floating-point value from the configuration.
Retrieves floating-point value from the configuration.
Arguments: Arguments:
- option: option name whose value is to be retrieved; - option: option name whose value is to be retrieved;
- default: default return value if no such option - default: default return value if no such option
exists. exists.
""" """
return self._confighelper_runner(option, default, return self._confighelper_runner(option, default,
self.getconfig().getdefaultfloat, self.getconfig().getdefaultfloat,
self.getconfig().getfloat) self.getconfig().getfloat)
@ -315,8 +297,7 @@ class ConfigHelperMixin:
def getconflist(self, option, separator_re, def getconflist(self, option, separator_re,
default = CustomConfigDefault): default = CustomConfigDefault):
""" """Retrieves strings from the configuration and splits it
Retrieves strings from the configuration and splits it
into the list of strings. into the list of strings.
Arguments: Arguments:
@ -325,8 +306,8 @@ class ConfigHelperMixin:
to be used for split operation; to be used for split operation;
- default: default return value if no such option - default: default return value if no such option
exists. exists.
""" """
return self._confighelper_runner(option, default, return self._confighelper_runner(option, default,
self.getconfig().getdefaultlist, self.getconfig().getdefaultlist,
self.getconfig().getlist, separator_re) self.getconfig().getlist, separator_re)

View File

@ -1,10 +1,10 @@
__all__ = ['OfflineImap'] __all__ = ['OfflineImap']
__productname__ = 'OfflineIMAP' __productname__ = 'OfflineIMAP'
__version__ = "6.5.6.1" __version__ = "6.5.7"
__revision__ = "-devel" __revision__ = "-rc1"
__bigversion__ = __version__ + __revision__ __bigversion__ = __version__ + __revision__
__copyright__ = "Copyright 2002-2013 John Goerzen & contributors" __copyright__ = "Copyright 2002-2015 John Goerzen & contributors"
__author__ = "John Goerzen" __author__ = "John Goerzen"
__author_email__= "john@complete.org" __author_email__= "john@complete.org"
__description__ = "Disconnected Universal IMAP Mail Synchronization/Reader Support" __description__ = "Disconnected Universal IMAP Mail Synchronization/Reader Support"

View File

@ -1,4 +1,4 @@
# Copyright (C) 2003-2011 John Goerzen & contributors # Copyright (C) 2003-2015 John Goerzen & contributors
# #
# This program is free software; you can redistribute it and/or modify # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -14,29 +14,33 @@
# 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 offlineimap import mbnames, CustomConfig, OfflineImapError
from offlineimap import globals
from offlineimap.repository import Repository
from offlineimap.ui import getglobalui
from offlineimap.threadutil import InstanceLimitedThread
from subprocess import Popen, PIPE from subprocess import Popen, PIPE
from threading import Event from threading import Event
import os import os
from sys import exc_info from sys import exc_info
import traceback import traceback
from offlineimap import mbnames, CustomConfig, OfflineImapError
from offlineimap import globals
from offlineimap.repository import Repository
from offlineimap.ui import getglobalui
from offlineimap.threadutil import InstanceLimitedThread
try: try:
import fcntl import fcntl
except: except:
pass # ok if this fails, we can do without pass # ok if this fails, we can do without
# FIXME: spaghetti code alert!
def getaccountlist(customconfig): def getaccountlist(customconfig):
return customconfig.getsectionlist('Account') return customconfig.getsectionlist('Account')
# FIXME: spaghetti code alert!
def AccountListGenerator(customconfig): def AccountListGenerator(customconfig):
return [Account(customconfig, accountname) return [Account(customconfig, accountname)
for accountname in getaccountlist(customconfig)] for accountname in getaccountlist(customconfig)]
# FIXME: spaghetti code alert!
def AccountHashGenerator(customconfig): def AccountHashGenerator(customconfig):
retval = {} retval = {}
for item in AccountListGenerator(customconfig): for item in AccountListGenerator(customconfig):

View File

@ -10,6 +10,7 @@ class OfflineImapError(Exception):
* **REPO**: Abort repository sync, continue with next account * **REPO**: Abort repository sync, continue with next account
* **CRITICAL**: Immediately exit offlineimap * **CRITICAL**: Immediately exit offlineimap
""" """
MESSAGE, FOLDER_RETRY, FOLDER, REPO, CRITICAL = 0, 10, 15, 20, 30 MESSAGE, FOLDER_RETRY, FOLDER, REPO, CRITICAL = 0, 10, 15, 20, 30
def __init__(self, reason, severity, errcode=None): def __init__(self, reason, severity, errcode=None):
@ -26,6 +27,7 @@ class OfflineImapError(Exception):
value). So far, no errcodes have been defined yet. value). So far, no errcodes have been defined yet.
:type severity: OfflineImapError.ERROR value""" :type severity: OfflineImapError.ERROR value"""
self.errcode = errcode self.errcode = errcode
self.severity = severity self.severity = severity

View File

@ -1,5 +1,5 @@
# Base folder support # Base folder support
# Copyright (C) 2002-2011 John Goerzen & contributors # Copyright (C) 2002-2015 John Goerzen & contributors
# #
# This program is free software; you can redistribute it and/or modify # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -23,7 +23,6 @@ import offlineimap.accounts
import os.path import os.path
import re import re
from sys import exc_info from sys import exc_info
import traceback
class BaseFolder(object): class BaseFolder(object):
@ -113,6 +112,7 @@ class BaseFolder(object):
def quickchanged(self, statusfolder): def quickchanged(self, statusfolder):
""" Runs quick check for folder changes and returns changed """ Runs quick check for folder changes and returns changed
status: True -- changed, False -- not changed. status: True -- changed, False -- not changed.
:param statusfolder: keeps track of the last known folder state. :param statusfolder: keeps track of the last known folder state.
""" """
return True return True
@ -129,11 +129,13 @@ class BaseFolder(object):
return 1 return 1
def getvisiblename(self): def getvisiblename(self):
"""The nametrans-transposed name of the folder's name""" """The nametrans-transposed name of the folder's name."""
return self.visiblename return self.visiblename
def getexplainedname(self): def getexplainedname(self):
""" Name that shows both real and nametrans-mangled values""" """Name that shows both real and nametrans-mangled values."""
if self.name == self.visiblename: if self.name == self.visiblename:
return self.name return self.name
else: else:
@ -401,6 +403,9 @@ class BaseFolder(object):
""" """
Adds new header to the provided message. Adds new header to the provided message.
WARNING: This function is a bit tricky, and modifying it in the wrong way,
may easily lead to data-loss.
Arguments: Arguments:
- content: message content, headers and body as a single string - content: message content, headers and body as a single string
- linebreak: string that carries line ending - linebreak: string that carries line ending
@ -512,8 +517,8 @@ class BaseFolder(object):
def getmessageheader(self, content, name): def getmessageheader(self, content, name):
""" """
Searches for the given header and returns its value. Searches for the first occurence of the given header and returns
Header name is case-insensitive. its value. Header name is case-insensitive.
Arguments: Arguments:
- contents: message itself - contents: message itself
@ -535,6 +540,27 @@ class BaseFolder(object):
return None return None
def getmessageheaderlist(self, content, name):
"""
Searches for the given header and returns a list of values for
that header.
Arguments:
- contents: message itself
- 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)
eoh = self.__find_eoh(content)
self.ui.debug('', '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)
def deletemessageheaders(self, content, header_list): def deletemessageheaders(self, content, header_list):
""" """
Deletes headers in the given list from the message content. Deletes headers in the given list from the message content.
@ -579,6 +605,7 @@ class BaseFolder(object):
:param new_uid: (optional) If given, the old UID will be changed :param new_uid: (optional) If given, the old UID will be changed
to a new UID. This allows backends efficient renaming of to a new UID. This allows backends efficient renaming of
messages if the UID has changed.""" messages if the UID has changed."""
raise NotImplementedError raise NotImplementedError
def deletemessage(self, uid): def deletemessage(self, uid):
@ -586,6 +613,7 @@ class BaseFolder(object):
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 so you need to ensure that it is never called in a
dryrun mode.""" dryrun mode."""
raise NotImplementedError raise NotImplementedError
def deletemessages(self, uidlist): def deletemessages(self, uidlist):
@ -593,6 +621,7 @@ class BaseFolder(object):
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 so you need to ensure that it is never called in a
dryrun mode.""" dryrun mode."""
for uid in uidlist: for uid in uidlist:
self.deletemessage(uid) self.deletemessage(uid)
@ -608,6 +637,7 @@ class BaseFolder(object):
:param statusfolder: A LocalStatusFolder instance :param statusfolder: A LocalStatusFolder instance
:param register: whether we should register a new thread." :param register: whether we should register a new thread."
:returns: Nothing on success, or raises an Exception.""" :returns: Nothing on success, or raises an Exception."""
# Sometimes, it could be the case that if a sync takes awhile, # Sometimes, it could be the case that if a sync takes awhile,
# a message might be deleted from the maildir before it can be # a message might be deleted from the maildir before it can be
# synced to the status cache. This is only a problem with # synced to the status cache. This is only a problem with

View File

@ -93,6 +93,10 @@ class GmailFolder(IMAPFolder):
labels = set() labels = set()
labels = labels - self.ignorelabels 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.
body = self.deletemessageheaders(body, self.labelsheader)
body = self.addmessageheader(body, '\n', self.labelsheader, labels_str) body = self.addmessageheader(body, '\n', self.labelsheader, labels_str)
if len(body)>200: if len(body)>200:
@ -189,8 +193,9 @@ class GmailFolder(IMAPFolder):
if not self.synclabels: if not self.synclabels:
return super(GmailFolder, self).savemessage(uid, content, flags, rtime) return super(GmailFolder, self).savemessage(uid, content, flags, rtime)
labels = imaputil.labels_from_header(self.labelsheader, labels = set()
self.getmessageheader(content, self.labelsheader)) for hstr in self.getmessageheaderlist(content, self.labelsheader):
labels.update(imaputil.labels_from_header(self.labelsheader, hstr))
ret = super(GmailFolder, self).savemessage(uid, content, flags, rtime) ret = super(GmailFolder, self).savemessage(uid, content, flags, rtime)
self.savemessagelabels(ret, labels) self.savemessagelabels(ret, labels)

View File

@ -87,9 +87,10 @@ class GmailMaildirFolder(MaildirFolder):
content = file.read() content = file.read()
file.close() file.close()
self.messagelist[uid]['labels'] = \ self.messagelist[uid]['labels'] = set()
imaputil.labels_from_header(self.labelsheader, for hstr in self.getmessageheaderlist(content, self.labelsheader):
self.getmessageheader(content, self.labelsheader)) self.messagelist[uid]['labels'].update(
imaputil.labels_from_header(self.labelsheader, hstr))
self.messagelist[uid]['labels_cached'] = True self.messagelist[uid]['labels_cached'] = True
return self.messagelist[uid]['labels'] return self.messagelist[uid]['labels']
@ -111,8 +112,10 @@ class GmailMaildirFolder(MaildirFolder):
if not self.synclabels: if not self.synclabels:
return super(GmailMaildirFolder, self).savemessage(uid, content, flags, rtime) return super(GmailMaildirFolder, self).savemessage(uid, content, flags, rtime)
labels = imaputil.labels_from_header(self.labelsheader, labels = set()
self.getmessageheader(content, self.labelsheader)) for hstr in self.getmessageheaderlist(content, self.labelsheader):
labels.update(imaputil.labels_from_header(self.labelsheader, hstr))
ret = super(GmailMaildirFolder, self).savemessage(uid, content, flags, rtime) ret = super(GmailMaildirFolder, self).savemessage(uid, content, flags, rtime)
# Update the mtime and labels # Update the mtime and labels
@ -135,9 +138,9 @@ class GmailMaildirFolder(MaildirFolder):
content = file.read() content = file.read()
file.close() file.close()
oldlabels = imaputil.labels_from_header(self.labelsheader, oldlabels = set()
self.getmessageheader(content, self.labelsheader)) for hstr in self.getmessageheaderlist(content, self.labelsheader):
oldlabels.update(imaputil.labels_from_header(self.labelsheader, hstr))
labels = labels - ignorelabels labels = labels - ignorelabels
ignoredlabels = oldlabels & ignorelabels ignoredlabels = oldlabels & ignorelabels
@ -150,7 +153,11 @@ class GmailMaildirFolder(MaildirFolder):
# Change labels into content # Change labels into content
labels_str = imaputil.format_labels_string(self.labelsheader, labels_str = imaputil.format_labels_string(self.labelsheader,
sorted(labels | ignoredlabels)) sorted(labels | ignoredlabels))
# First remove old labels header, and then add the new one
content = self.deletemessageheaders(content, self.labelsheader)
content = self.addmessageheader(content, '\n', self.labelsheader, labels_str) content = self.addmessageheader(content, '\n', self.labelsheader, labels_str)
rtime = self.messagelist[uid].get('rtime', None) rtime = self.messagelist[uid].get('rtime', None)
# write file with new labels to a unique file in tmp # write file with new labels to a unique file in tmp

View File

@ -1,5 +1,5 @@
# Local status cache virtual folder # Local status cache virtual folder
# Copyright (C) 2002 - 2011 John Goerzen & contributors # Copyright (C) 2002-2015 John Goerzen & contributors
# #
# This program is free software; you can redistribute it and/or modify # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -15,10 +15,10 @@
# 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 .Base import BaseFolder
import os import os
import threading import threading
from .Base import BaseFolder
class LocalStatusFolder(BaseFolder): class LocalStatusFolder(BaseFolder):
"""LocalStatus backend implemented as a plain text file""" """LocalStatus backend implemented as a plain text file"""
@ -33,9 +33,9 @@ class LocalStatusFolder(BaseFolder):
self.filename = os.path.join(self.getroot(), self.getfolderbasename()) self.filename = os.path.join(self.getroot(), self.getfolderbasename())
self.messagelist = {} self.messagelist = {}
self.savelock = threading.Lock() self.savelock = threading.Lock()
self.doautosave = self.config.getdefaultboolean("general", "fsync", # Should we perform fsyncs as often as possible?
False) self.doautosave = self.config.getdefaultboolean(
"""Should we perform fsyncs as often as possible?""" "general", "fsync", False)
# Interface from BaseFolder # Interface from BaseFolder
def storesmessages(self): def storesmessages(self):
@ -63,13 +63,12 @@ class LocalStatusFolder(BaseFolder):
def readstatus_v1(self, fp): def readstatus_v1(self, fp):
""" """Read status folder in format version 1.
Read status folder in format version 1.
Arguments: Arguments:
- fp: I/O object that points to the opened database file. - fp: I/O object that points to the opened database file.
""" """
for line in fp.xreadlines(): for line in fp.xreadlines():
line = line.strip() line = line.strip()
try: try:
@ -86,13 +85,12 @@ class LocalStatusFolder(BaseFolder):
def readstatus(self, fp): def readstatus(self, fp):
""" """Read status file in the current format.
Read status file in the current format.
Arguments: Arguments:
- fp: I/O object that points to the opened database file. - fp: I/O object that points to the opened database file.
""" """
for line in fp.xreadlines(): for line in fp.xreadlines():
line = line.strip() line = line.strip()
try: try:
@ -164,11 +162,13 @@ class LocalStatusFolder(BaseFolder):
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."""
self.saveall() self.saveall()
def saveall(self): def saveall(self):
"""Saves the entire messagelist to disk""" """Saves the entire messagelist to disk."""
with self.savelock: with self.savelock:
file = open(self.filename + ".tmp", "wt") file = open(self.filename + ".tmp", "wt")
file.write((self.magicline % self.cur_version) + "\n") file.write((self.magicline % self.cur_version) + "\n")
@ -198,6 +198,7 @@ class LocalStatusFolder(BaseFolder):
See folder/Base for detail. Note that savemessage() does not See folder/Base for detail. Note that savemessage() does not
check against dryrun settings, so you need to ensure that 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."""
if uid < 0: if uid < 0:
# We cannot assign a uid. # We cannot assign a uid.
return uid return uid
@ -235,6 +236,7 @@ class LocalStatusFolder(BaseFolder):
def savemessageslabelsbulk(self, labels): def savemessageslabelsbulk(self, labels):
"""Saves labels from a dictionary in a single database operation.""" """Saves labels from a dictionary in a single database operation."""
for uid, lb in labels.items(): for uid, lb in labels.items():
self.messagelist[uid]['labels'] = lb self.messagelist[uid]['labels'] = lb
self.save() self.save()
@ -254,6 +256,7 @@ class LocalStatusFolder(BaseFolder):
def savemessagesmtimebulk(self, mtimes): def savemessagesmtimebulk(self, mtimes):
"""Saves mtimes from the mtimes dictionary in a single database operation.""" """Saves mtimes from the mtimes dictionary in a single database operation."""
for uid, mt in mtimes.items(): for uid, mt in mtimes.items():
self.messagelist[uid]['mtime'] = mt self.messagelist[uid]['mtime'] = mt
self.save() self.save()

View File

@ -1,5 +1,5 @@
# Maildir folder support # Maildir folder support
# Copyright (C) 2002 - 2011 John Goerzen & contributors # Copyright (C) 2002-2015 John Goerzen & contributors
# #
# This program is free software; you can redistribute it and/or modify # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -19,15 +19,12 @@ import socket
import time import time
import re import re
import os import os
import tempfile
from .Base import BaseFolder from .Base import BaseFolder
from threading import Lock from threading import Lock
try: try:
from hashlib import md5 from hashlib import md5
except ImportError: except ImportError:
from md5 import md5 from md5 import md5
try: # python 2.6 has set() built in try: # python 2.6 has set() built in
set set
except NameError: except NameError:
@ -131,6 +128,7 @@ class MaildirFolder(BaseFolder):
:returns: (prefix, UID, FMD5, flags). UID is a numeric "long" :returns: (prefix, UID, FMD5, flags). UID is a numeric "long"
type. flags is a set() of Maildir flags""" type. flags is a set() of Maildir flags"""
prefix, uid, fmd5, flags = None, None, None, set() prefix, uid, fmd5, flags = None, None, None, set()
prefixmatch = self.re_prefixmatch.match(filename) prefixmatch = self.re_prefixmatch.match(filename)
if prefixmatch: if prefixmatch:
@ -227,7 +225,8 @@ class MaildirFolder(BaseFolder):
# Interface from BaseFolder # Interface from BaseFolder
def getmessage(self, uid): def getmessage(self, uid):
"""Return the content of the message""" """Return the content of the message."""
filename = self.messagelist[uid]['filename'] filename = self.messagelist[uid]['filename']
filepath = os.path.join(self.getfullname(), filename) filepath = os.path.join(self.getfullname(), filename)
file = open(filepath, 'rt') file = open(filepath, 'rt')
@ -249,6 +248,7 @@ class MaildirFolder(BaseFolder):
:param uid: The UID`None`, or a set of maildir flags :param uid: The UID`None`, or a set of maildir flags
:param flags: A set of maildir flags :param flags: A set of maildir flags
:returns: String containing unique message filename""" :returns: String containing unique message filename"""
timeval, timeseq = _gettimeseq() timeval, timeseq = _gettimeseq()
return '%d_%d.%d.%s,U=%d,FMD5=%s%s2,%s' % \ return '%d_%d.%d.%s,U=%d,FMD5=%s%s2,%s' % \
(timeval, timeseq, os.getpid(), socket.gethostname(), (timeval, timeseq, os.getpid(), socket.gethostname(),
@ -256,8 +256,7 @@ class MaildirFolder(BaseFolder):
def save_to_tmp_file(self, filename, content): def save_to_tmp_file(self, filename, content):
""" """Saves given content to the named temporary file in the
Saves given content to the named temporary file in the
'tmp' subdirectory of $CWD. 'tmp' subdirectory of $CWD.
Arguments: Arguments:
@ -265,9 +264,7 @@ class MaildirFolder(BaseFolder):
- content: data to be saved. - content: data to be saved.
Returns: relative path to the temporary file Returns: relative path to the temporary file
that was created. that was created."""
"""
tmpname = os.path.join('tmp', filename) tmpname = os.path.join('tmp', filename)
# open file and write it out # open file and write it out
@ -364,7 +361,7 @@ class MaildirFolder(BaseFolder):
infomatch = self.re_flagmatch.search(filename) infomatch = self.re_flagmatch.search(filename)
if infomatch: if infomatch:
filename = filename[:-len(infomatch.group())] #strip off filename = filename[:-len(infomatch.group())] #strip off
infostr = '%s2,%s' % (self.infosep, ''.join(sorted(flags))) infostr = '%s2,%s'% (self.infosep, ''.join(sorted(flags)))
filename += infostr filename += infostr
newfilename = os.path.join(dir_prefix, filename) newfilename = os.path.join(dir_prefix, filename)
@ -386,8 +383,10 @@ class MaildirFolder(BaseFolder):
This will not update the statusfolder UID, you need to do that yourself. This will not update the statusfolder UID, you need to do that yourself.
:param new_uid: (optional) If given, the old UID will be changed :param new_uid: (optional) If given, the old UID will be changed
to a new UID. The Maildir backend can implement this as an efficient to a new UID. The Maildir backend can implement this as
rename.""" an efficient rename.
"""
if not uid in self.messagelist: if not uid in self.messagelist:
raise OfflineImapError("Cannot change unknown Maildir UID %s" % uid) raise OfflineImapError("Cannot change unknown Maildir UID %s" % uid)
if uid == new_uid: return if uid == new_uid: return

View File

@ -1,5 +1,5 @@
# Base folder support # Base folder support
# Copyright (C) 2002-2012 John Goerzen & contributors # Copyright (C) 2002-2015 John Goerzen & contributors
# #
# This program is free software; you can redistribute it and/or modify # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -59,8 +59,8 @@ class MappedIMAPFolder(IMAPFolder):
try: try:
line = line.strip() line = line.strip()
except ValueError: except ValueError:
raise Exception("Corrupt line '%s' in UID mapping file '%s'" \ raise Exception("Corrupt line '%s' in UID mapping file '%s'"%
%(line, mapfilename)) (line, mapfilename))
(str1, str2) = line.split(':') (str1, str2) = line.split(':')
loc = long(str1) loc = long(str1)
rem = long(str2) rem = long(str2)

View File

@ -1,6 +1,5 @@
# imaplib utilities # imaplib utilities
# Copyright (C) 2002-2007 John Goerzen <jgoerzen@complete.org> # Copyright (C) 2002-2015 John Goerzen & contributors
# 2012-2012 Sebastian Spaeth <Sebastian@SSpaeth.de>
# This program is free software; you can redistribute it and/or modify # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or # the Free Software Foundation; either version 2 of the License, or
@ -17,9 +16,6 @@
import os import os
import fcntl import fcntl
import re
import socket
import ssl
import time import time
import subprocess import subprocess
import threading import threading
@ -27,7 +23,7 @@ from hashlib import sha1
from offlineimap.ui import getglobalui from offlineimap.ui import getglobalui
from offlineimap import OfflineImapError from offlineimap import OfflineImapError
from offlineimap.imaplib2 import IMAP4, IMAP4_SSL, zlib, IMAP4_PORT, InternalDate, Mon2num from offlineimap.imaplib2 import IMAP4, IMAP4_SSL, zlib, InternalDate, Mon2num
class UsefulIMAPMixIn(object): class UsefulIMAPMixIn(object):
@ -36,11 +32,12 @@ class UsefulIMAPMixIn(object):
return self.mailbox return self.mailbox
return None return None
def select(self, mailbox='INBOX', readonly=False, force = False): def select(self, mailbox='INBOX', readonly=False, force=False):
"""Selects a mailbox on the IMAP server """Selects a mailbox on the IMAP server
:returns: 'OK' on success, nothing if the folder was already :returns: 'OK' on success, nothing if the folder was already
selected or raises an :exc:`OfflineImapError`""" selected or raises an :exc:`OfflineImapError`."""
if self.__getselectedfolder() == mailbox and self.is_readonly == readonly \ if self.__getselectedfolder() == mailbox and self.is_readonly == readonly \
and not force: and not force:
# No change; return. # No change; return.
@ -70,6 +67,7 @@ class UsefulIMAPMixIn(object):
def _mesg(self, s, tn=None, secs=None): def _mesg(self, s, tn=None, secs=None):
new_mesg(self, s, tn, secs) new_mesg(self, s, tn, secs)
class IMAP4_Tunnel(UsefulIMAPMixIn, IMAP4): class IMAP4_Tunnel(UsefulIMAPMixIn, IMAP4):
"""IMAP4 client class over a tunnel """IMAP4 client class over a tunnel
@ -83,6 +81,7 @@ class IMAP4_Tunnel(UsefulIMAPMixIn, IMAP4):
def open(self, host, port): def open(self, host, port):
"""The tunnelcmd comes in on host!""" """The tunnelcmd comes in on host!"""
self.host = host self.host = host
self.process = subprocess.Popen(host, shell=True, close_fds=True, self.process = subprocess.Popen(host, shell=True, close_fds=True,
stdin=subprocess.PIPE, stdout=subprocess.PIPE) stdin=subprocess.PIPE, stdout=subprocess.PIPE)
@ -93,7 +92,8 @@ class IMAP4_Tunnel(UsefulIMAPMixIn, IMAP4):
self.set_nonblocking(self.read_fd) self.set_nonblocking(self.read_fd)
def set_nonblocking(self, fd): def set_nonblocking(self, fd):
"Mark fd as nonblocking" """Mark fd as nonblocking"""
# get the file's current flag settings # get the file's current flag settings
fl = fcntl.fcntl(fd, fcntl.F_GETFL) fl = fcntl.fcntl(fd, fcntl.F_GETFL)
# clear non-blocking mode from flags # clear non-blocking mode from flags
@ -118,10 +118,8 @@ class IMAP4_Tunnel(UsefulIMAPMixIn, IMAP4):
if self.compressor is not None: if self.compressor is not None:
data = self.compressor.compress(data) data = self.compressor.compress(data)
data += self.compressor.flush(zlib.Z_SYNC_FLUSH) data += self.compressor.flush(zlib.Z_SYNC_FLUSH)
self.outfd.write(data) self.outfd.write(data)
def shutdown(self): def shutdown(self):
self.infd.close() self.infd.close()
self.outfd.close() self.outfd.close()
@ -138,7 +136,8 @@ def new_mesg(self, s, tn=None, secs=None):
class WrappedIMAP4_SSL(UsefulIMAPMixIn, IMAP4_SSL): class WrappedIMAP4_SSL(UsefulIMAPMixIn, IMAP4_SSL):
"""Improved version of imaplib.IMAP4_SSL overriding select()""" """Improved version of imaplib.IMAP4_SSL overriding select()."""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self._fingerprint = kwargs.get('fingerprint', None) self._fingerprint = kwargs.get('fingerprint', None)
if type(self._fingerprint) != type([]): if type(self._fingerprint) != type([]):
@ -149,32 +148,34 @@ class WrappedIMAP4_SSL(UsefulIMAPMixIn, IMAP4_SSL):
def open(self, host=None, port=None): def open(self, host=None, port=None):
if not self.ca_certs and not self._fingerprint: if not self.ca_certs and not self._fingerprint:
raise OfflineImapError("No CA certificates " + \ raise OfflineImapError("No CA certificates "
"and no server fingerprints configured. " + \ "and no server fingerprints configured. "
"You must configure at least something, otherwise " + \ "You must configure at least something, otherwise "
"having SSL helps nothing.", OfflineImapError.ERROR.REPO) "having SSL helps nothing.", OfflineImapError.ERROR.REPO)
super(WrappedIMAP4_SSL, self).open(host, port) super(WrappedIMAP4_SSL, self).open(host, port)
if self._fingerprint: if self._fingerprint:
# compare fingerprints # compare fingerprints
fingerprint = sha1(self.sock.getpeercert(True)).hexdigest() fingerprint = sha1(self.sock.getpeercert(True)).hexdigest()
if fingerprint not in self._fingerprint: if fingerprint not in self._fingerprint:
raise OfflineImapError("Server SSL fingerprint '%s' " % fingerprint + \ raise OfflineImapError("Server SSL fingerprint '%s' "
"for hostname '%s' " % host + \ "for hostname '%s' "
"does not match configured fingerprint(s) %s. " % self._fingerprint + \ "does not match configured fingerprint(s) %s. "
"Please verify and set 'cert_fingerprint' accordingly " + \ "Please verify and set 'cert_fingerprint' accordingly "
"if not set yet.", OfflineImapError.ERROR.REPO) "if not set yet."%
(fingerprint, host, self._fingerprint),
OfflineImapError.ERROR.REPO)
class WrappedIMAP4(UsefulIMAPMixIn, IMAP4): class WrappedIMAP4(UsefulIMAPMixIn, IMAP4):
"""Improved version of imaplib.IMAP4 overriding select()""" """Improved version of imaplib.IMAP4 overriding select()."""
pass pass
def Internaldate2epoch(resp): def Internaldate2epoch(resp):
"""Convert IMAP4 INTERNALDATE to UT. """Convert IMAP4 INTERNALDATE to UT.
Returns seconds since the epoch. Returns seconds since the epoch."""
"""
mo = InternalDate.match(resp) mo = InternalDate.match(resp)
if not mo: if not mo:

View File

@ -1,6 +1,5 @@
# IMAP utility module # IMAP utility module
# Copyright (C) 2002 John Goerzen # Copyright (C) 2002-2015 John Goerzen & contributors
# <jgoerzen@complete.org>
# #
# This program is free software; you can redistribute it and/or modify # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -37,8 +36,8 @@ def dequote(string):
"""Takes string which may or may not be quoted and unquotes it. """Takes string which may or may not be quoted and unquotes it.
It only considers double quotes. This function does NOT consider It only considers double quotes. This function does NOT consider
parenthised lists to be quoted. parenthised lists to be quoted."""
"""
if string and string.startswith('"') and string.endswith('"'): if string and string.startswith('"') and string.endswith('"'):
string = string[1:-1] # Strip off the surrounding quotes. string = string[1:-1] # Strip off the surrounding quotes.
string = string.replace('\\"', '"') string = string.replace('\\"', '"')
@ -49,8 +48,8 @@ def quote(string):
"""Takes an unquoted string and quotes it. """Takes an unquoted string and quotes it.
It only adds double quotes. This function does NOT consider It only adds double quotes. This function does NOT consider
parenthised lists to be quoted. parenthised lists to be quoted."""
"""
string = string.replace('"', '\\"') string = string.replace('"', '\\"')
string = string.replace('\\', '\\\\') string = string.replace('\\', '\\\\')
return '"%s"' % string return '"%s"' % string
@ -62,12 +61,14 @@ def flagsplit(string):
(FLAGS (\\Seen Old) UID 4807) returns (FLAGS (\\Seen Old) UID 4807) returns
['FLAGS,'(\\Seen Old)','UID', '4807'] ['FLAGS,'(\\Seen Old)','UID', '4807']
""" """
if string[0] != '(' or string[-1] != ')': if string[0] != '(' or string[-1] != ')':
raise ValueError("Passed string '%s' is not a flag list" % string) raise ValueError("Passed string '%s' is not a flag list" % string)
return imapsplit(string[1:-1]) return imapsplit(string[1:-1])
def __options2hash(list): def __options2hash(list):
"""convert list [1,2,3,4,5,6] to {1:2, 3:4, 5:6}""" """convert list [1,2,3,4,5,6] to {1:2, 3:4, 5:6}"""
# effectively this does dict(zip(l[::2],l[1::2])), however # effectively this does dict(zip(l[::2],l[1::2])), however
# measurements seemed to have indicated that the manual variant is # measurements seemed to have indicated that the manual variant is
# faster for mosly small lists. # faster for mosly small lists.
@ -84,6 +85,7 @@ def flags2hash(flags):
E.g. '(FLAGS (\\Seen Old) UID 4807)' leads to E.g. '(FLAGS (\\Seen Old) UID 4807)' leads to
{'FLAGS': '(\\Seen Old)', 'UID': '4807'}""" {'FLAGS': '(\\Seen Old)', 'UID': '4807'}"""
return __options2hash(flagsplit(flags)) return __options2hash(flagsplit(flags))
def imapsplit(imapstring): def imapsplit(imapstring):
@ -182,7 +184,8 @@ flagmap = [('\\Seen', 'S'),
('\\Draft', 'D')] ('\\Draft', 'D')]
def flagsimap2maildir(flagstring): def flagsimap2maildir(flagstring):
"""Convert string '(\\Draft \\Deleted)' into a flags set(DR)""" """Convert string '(\\Draft \\Deleted)' into a flags set(DR)."""
retval = set() retval = set()
imapflaglist = flagstring[1:-1].split() imapflaglist = flagstring[1:-1].split()
for imapflag, maildirflag in flagmap: for imapflag, maildirflag in flagmap:
@ -191,7 +194,8 @@ def flagsimap2maildir(flagstring):
return retval return retval
def flagsmaildir2imap(maildirflaglist): def flagsmaildir2imap(maildirflaglist):
"""Convert set of flags ([DR]) into a string '(\\Deleted \\Draft)'""" """Convert set of flags ([DR]) into a string '(\\Deleted \\Draft)'."""
retval = [] retval = []
for imapflag, maildirflag in flagmap: for imapflag, maildirflag in flagmap:
if maildirflag in maildirflaglist: if maildirflag in maildirflaglist:
@ -203,7 +207,8 @@ def uid_sequence(uidlist):
[1,2,3,4,5,10,12,13] will return "1:5,10,12:13". This function sorts [1,2,3,4,5,10,12,13] will return "1:5,10,12:13". This function sorts
the list, and only collapses if subsequent entries form a range. the list, and only collapses if subsequent entries form a range.
:returns: The collapsed UID list as string""" :returns: The collapsed UID list as string."""
def getrange(start, end): def getrange(start, end):
if start == end: if start == end:
return(str(start)) return(str(start))
@ -230,8 +235,7 @@ def uid_sequence(uidlist):
def __split_quoted(string): def __split_quoted(string):
""" """Looks for the ending quote character in the string that starts
Looks for the ending quote character in the string that starts
with quote character, splitting out quoted component and the with quote character, splitting out quoted component and the
rest of the string (without possible space between these two rest of the string (without possible space between these two
parts. parts.
@ -241,7 +245,6 @@ def __split_quoted(string):
Examples: Examples:
- "this is \" a test" (\\None) => ("this is \" a test", (\\None)) - "this is \" a test" (\\None) => ("this is \" a test", (\\None))
- "\\" => ("\\", ) - "\\" => ("\\", )
""" """
if len(string) == 0: if len(string) == 0:
@ -269,17 +272,15 @@ def __split_quoted(string):
def format_labels_string(header, labels): def format_labels_string(header, labels):
""" """Formats labels for embedding into a message,
Formats labels for embedding into a message,
with format according to header name. with format according to header name.
Headers from SPACE_SEPARATED_LABEL_HEADERS keep space-separated list Headers from SPACE_SEPARATED_LABEL_HEADERS keep space-separated list
of labels, the rest uses comma (',') as the separator. of labels, the rest uses comma (',') as the separator.
Also see parse_labels_string() and modify it accordingly Also see parse_labels_string() and modify it accordingly
if logics here gets changed. if logics here gets changed."""
"""
if header in SPACE_SEPARATED_LABEL_HEADERS: if header in SPACE_SEPARATED_LABEL_HEADERS:
sep = ' ' sep = ' '
else: else:
@ -289,18 +290,16 @@ def format_labels_string(header, labels):
def parse_labels_string(header, labels_str): def parse_labels_string(header, labels_str):
""" """Parses a string into a set of labels, with a format according to
Parses a string into a set of labels, with a format according to
the name of the header. the name of the header.
See __format_labels_string() for explanation on header handling See __format_labels_string() for explanation on header handling
and keep these two functions synced with each other. and keep these two functions synced with each other.
TODO: add test to ensure that TODO: add test to ensure that
format_labels_string * parse_labels_string is unity - format_labels_string * parse_labels_string is unity
and and
parse_labels_string * format_labels_string is unity - parse_labels_string * format_labels_string is unity
""" """
if header in SPACE_SEPARATED_LABEL_HEADERS: if header in SPACE_SEPARATED_LABEL_HEADERS:
@ -314,15 +313,13 @@ def parse_labels_string(header, labels_str):
def labels_from_header(header_name, header_value): def labels_from_header(header_name, header_value):
""" """Helper that builds label set from the corresponding header value.
Helper that builds label set from the corresponding header value.
Arguments: Arguments:
- header_name: name of the header that keeps labels; - header_name: name of the header that keeps labels;
- header_value: value of the said header, can be None - header_value: value of the said header, can be None
Returns: set of labels parsed from the header (or empty set). Returns: set of labels parsed from the header (or empty set).
""" """
if header_value: if header_value:

View File

@ -1,5 +1,5 @@
# OfflineIMAP initialization code # OfflineIMAP initialization code
# Copyright (C) 2002-2011 John Goerzen & contributors # Copyright (C) 2002-2015 John Goerzen & contributors
# #
# This program is free software; you can redistribute it and/or modify # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -26,7 +26,6 @@ from optparse import OptionParser
import offlineimap import offlineimap
from offlineimap import accounts, threadutil, syncmaster from offlineimap import accounts, threadutil, syncmaster
from offlineimap import globals from offlineimap import globals
from offlineimap.error import OfflineImapError
from offlineimap.ui import UI_LIST, setglobalui, getglobalui from offlineimap.ui import UI_LIST, setglobalui, getglobalui
from offlineimap.CustomConfig import CustomConfigParser from offlineimap.CustomConfig import CustomConfigParser
from offlineimap.utils import stacktrace from offlineimap.utils import stacktrace
@ -215,7 +214,7 @@ class OfflineImap:
config.set(section, key, value) config.set(section, key, value)
#which ui to use? cmd line option overrides config file #which ui to use? cmd line option overrides config file
ui_type = config.getdefault('general','ui', 'ttyui') ui_type = config.getdefault('general', 'ui', 'ttyui')
if options.interface != None: if options.interface != None:
ui_type = options.interface ui_type = options.interface
if '.' in ui_type: if '.' in ui_type:
@ -223,13 +222,13 @@ class OfflineImap:
ui_type = ui_type.split('.')[-1] ui_type = ui_type.split('.')[-1]
# TODO, make use of chosen ui for logging # TODO, make use of chosen ui for logging
logging.warning('Using old interface name, consider using one ' logging.warning('Using old interface name, consider using one '
'of %s' % ', '.join(UI_LIST.keys())) 'of %s'% ', '.join(UI_LIST.keys()))
if options.diagnostics: ui_type = 'basic' # enforce basic UI for --info if options.diagnostics: ui_type = 'basic' # enforce basic UI for --info
#dry-run? Set [general]dry-run=True #dry-run? Set [general]dry-run=True
if options.dryrun: if options.dryrun:
dryrun = config.set('general','dry-run', "True") dryrun = config.set('general', 'dry-run', 'True')
config.set_if_not_exists('general','dry-run','False') config.set_if_not_exists('general', 'dry-run', 'False')
try: try:
# create the ui class # create the ui class
@ -265,7 +264,7 @@ class OfflineImap:
imaplib.Debug = 5 imaplib.Debug = 5
if options.runonce: if options.runonce:
# FIXME: maybe need a better # FIXME: spaghetti code alert!
for section in accounts.getaccountlist(config): for section in accounts.getaccountlist(config):
config.remove_option('Account ' + section, "autorefresh") config.remove_option('Account ' + section, "autorefresh")
@ -276,7 +275,7 @@ class OfflineImap:
#custom folder list specified? #custom folder list specified?
if options.folders: if options.folders:
foldernames = options.folders.split(",") foldernames = options.folders.split(",")
folderfilter = "lambda f: f in %s" % foldernames folderfilter = "lambda f: f in %s"% foldernames
folderincludes = "[]" folderincludes = "[]"
for accountname in accounts.getaccountlist(config): for accountname in accounts.getaccountlist(config):
account_section = 'Account ' + accountname account_section = 'Account ' + accountname
@ -356,12 +355,12 @@ class OfflineImap:
"take a few seconds)...") "take a few seconds)...")
accounts.Account.set_abort_event(self.config, 3) accounts.Account.set_abort_event(self.config, 3)
elif sig == signal.SIGQUIT: elif sig == signal.SIGQUIT:
stacktrace.dump (sys.stderr) stacktrace.dump(sys.stderr)
os.abort() os.abort()
signal.signal(signal.SIGHUP,sig_handler) signal.signal(signal.SIGHUP, sig_handler)
signal.signal(signal.SIGUSR1,sig_handler) signal.signal(signal.SIGUSR1, sig_handler)
signal.signal(signal.SIGUSR2,sig_handler) signal.signal(signal.SIGUSR2, sig_handler)
signal.signal(signal.SIGTERM, sig_handler) signal.signal(signal.SIGTERM, sig_handler)
signal.signal(signal.SIGINT, sig_handler) signal.signal(signal.SIGINT, sig_handler)
signal.signal(signal.SIGQUIT, sig_handler) signal.signal(signal.SIGQUIT, sig_handler)
@ -395,7 +394,7 @@ class OfflineImap:
for accountname in accs: for accountname in accs:
account = offlineimap.accounts.SyncableAccount(self.config, account = offlineimap.accounts.SyncableAccount(self.config,
accountname) accountname)
threading.currentThread().name = "Account sync %s" % accountname threading.currentThread().name = "Account sync %s"% accountname
account.syncrunner() account.syncrunner()
def __serverdiagnostics(self, options): def __serverdiagnostics(self, options):

View File

@ -1,7 +1,6 @@
"""Eval python code with global namespace of a python source file.""" """Eval python code with global namespace of a python source file."""
# Copyright (C) 2002 John Goerzen # Copyright (C) 2002-2014 John Goerzen & contributors
# <jgoerzen@complete.org>
# #
# This program is free software; you can redistribute it and/or modify # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -24,18 +23,24 @@ except:
pass pass
class LocalEval: class LocalEval:
"""Here is a powerfull but very dangerous option, of course.
Assume source file to be ASCII encoded."""
def __init__(self, path=None): def __init__(self, path=None):
self.namespace={} self.namespace = {}
if path is not None: if path is not None:
file=open(path, 'r') # FIXME: limit opening files owned by current user with rights set
module=imp.load_module( # to fixed mode 644.
file = open(path, 'r')
module = imp.load_module(
'<none>', '<none>',
file, file,
path, path,
('', 'r', imp.PY_SOURCE)) ('', 'r', imp.PY_SOURCE))
for attr in dir(module): for attr in dir(module):
self.namespace[attr]=getattr(module, attr) self.namespace[attr] = getattr(module, attr)
def eval(self, text, namespace=None): def eval(self, text, namespace=None):
names = {} names = {}

View File

@ -1,6 +1,6 @@
# Mailbox name generator # Mailbox name generator
# Copyright (C) 2002 John Goerzen #
# <jgoerzen@complete.org> # Copyright (C) 2002-2015 John Goerzen & contributors
# #
# This program is free software; you can redistribute it and/or modify # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -49,13 +49,14 @@ def write():
def __genmbnames(): def __genmbnames():
"""Takes a configparser object and a boxlist, which is a list of hashes """Takes a configparser object and a boxlist, which is a list of hashes
containing 'accountname' and 'foldername' keys.""" containing 'accountname' and 'foldername' keys."""
xforms = [os.path.expanduser, os.path.expandvars] xforms = [os.path.expanduser, os.path.expandvars]
mblock.acquire() mblock.acquire()
try: try:
localeval = config.getlocaleval() localeval = config.getlocaleval()
if not config.getdefaultboolean("mbnames", "enabled", 0): if not config.getdefaultboolean("mbnames", "enabled", 0):
return return
path = config.apply_xform(config.get("mbnames", "filename"), xforms) path = config.apply_xforms(config.get("mbnames", "filename"), xforms)
file = open(path, "wt") file = open(path, "wt")
file.write(localeval.eval(config.get("mbnames", "header"))) file.write(localeval.eval(config.get("mbnames", "header")))
folderfilter = lambda accountname, foldername: 1 folderfilter = lambda accountname, foldername: 1

View File

@ -1,5 +1,5 @@
# Base repository support # Base repository support
# Copyright (C) 2002-2012 John Goerzen & contributors # Copyright (C) 2002-2015 John Goerzen & contributors
# #
# This program is free software; you can redistribute it and/or modify # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -17,8 +17,8 @@
import re import re
import os.path import os.path
import traceback
from sys import exc_info from sys import exc_info
from offlineimap import CustomConfig from offlineimap import CustomConfig
from offlineimap.ui import getglobalui from offlineimap.ui import getglobalui
from offlineimap.error import OfflineImapError from offlineimap.error import OfflineImapError
@ -114,6 +114,7 @@ class BaseRepository(CustomConfig.ConfigHelperMixin, object):
@property @property
def readonly(self): def readonly(self):
"""Is the repository readonly?""" """Is the repository readonly?"""
return self._readonly return self._readonly
def getlocaleval(self): def getlocaleval(self):
@ -121,11 +122,13 @@ class BaseRepository(CustomConfig.ConfigHelperMixin, object):
def getfolders(self): def getfolders(self):
"""Returns a list of ALL folders on this server.""" """Returns a list of ALL folders on this server."""
return [] return []
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."""
pass pass
def getsep(self): def getsep(self):
@ -133,6 +136,7 @@ class BaseRepository(CustomConfig.ConfigHelperMixin, object):
def should_sync_folder(self, fname): def should_sync_folder(self, fname):
"""Should this folder be synced?""" """Should this folder be synced?"""
return fname in self.folderincludes or self.folderfilter(fname) return fname in self.folderincludes or self.folderfilter(fname)
def get_create_folders(self): def get_create_folders(self):
@ -140,11 +144,13 @@ class BaseRepository(CustomConfig.ConfigHelperMixin, object):
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 (not self._readonly) and \ return (not self._readonly) and \
self.getconfboolean('createfolders', True) self.getconfboolean('createfolders', True)
def makefolder(self, foldername): def makefolder(self, foldername):
"""Create a new folder""" """Create a new folder"""
raise NotImplementedError raise NotImplementedError
def deletefolder(self, foldername): def deletefolder(self, foldername):

View File

@ -18,7 +18,6 @@
from offlineimap.repository.Maildir import MaildirRepository from offlineimap.repository.Maildir import MaildirRepository
from offlineimap.folder.GmailMaildir import GmailMaildirFolder from offlineimap.folder.GmailMaildir import GmailMaildirFolder
from offlineimap.error import OfflineImapError
class GmailMaildirRepository(MaildirRepository): class GmailMaildirRepository(MaildirRepository):
def __init__(self, reposname, account): def __init__(self, reposname, account):

View File

@ -1,5 +1,5 @@
# IMAP repository support # IMAP repository support
# Copyright (C) 2002-2011 John Goerzen & contributors # Copyright (C) 2002-2015 John Goerzen & contributors
# #
# This program is free software; you can redistribute it and/or modify # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -15,17 +15,19 @@
# 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 offlineimap.repository.Base import BaseRepository
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
from threading import Event from threading import Event
import os import os
from sys import exc_info from sys import exc_info
import netrc import netrc
import errno import errno
from offlineimap.repository.Base import BaseRepository
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
class IMAPRepository(BaseRepository): class IMAPRepository(BaseRepository):
def __init__(self, reposname, account): def __init__(self, reposname, account):
"""Initialize an IMAPRepository object.""" """Initialize an IMAPRepository object."""
@ -116,14 +118,10 @@ class IMAPRepository(BaseRepository):
"'%s' specified." % self, "'%s' specified." % self,
OfflineImapError.ERROR.REPO) OfflineImapError.ERROR.REPO)
def get_remote_identity(self): def get_remote_identity(self):
""" """Remote identity is used for certain SASL mechanisms
Remote identity is used for certain SASL mechanisms
(currently -- PLAIN) to inform server about the ID (currently -- PLAIN) to inform server about the ID
we want to authorize as instead of our login name. we want to authorize as instead of our login name."""
"""
return self.getconf('remote_identity', default=None) return self.getconf('remote_identity', default=None)
@ -218,13 +216,10 @@ class IMAPRepository(BaseRepository):
return self.getconf('ssl_version', None) return self.getconf('ssl_version', None)
def get_ssl_fingerprint(self): def get_ssl_fingerprint(self):
""" """Return array of possible certificate fingerprints.
Return array of possible certificate fingerprints.
Configuration item cert_fingerprint can contain multiple Configuration item cert_fingerprint can contain multiple
comma-separated fingerprints in hex form. comma-separated fingerprints in hex form."""
"""
value = self.getconf('cert_fingerprint', "") value = self.getconf('cert_fingerprint', "")
return [f.strip().lower() for f in value.split(',') if f] return [f.strip().lower() for f in value.split(',') if f]
@ -262,8 +257,8 @@ class IMAPRepository(BaseRepository):
5. read password from /etc/netrc 5. read password from /etc/netrc
On success we return the password. On success we return the password.
If all strategies fail we return None. If all strategies fail we return None."""
"""
# 1. evaluate Repository 'remotepasseval' # 1. evaluate Repository 'remotepasseval'
passwd = self.getconf('remotepasseval', None) passwd = self.getconf('remotepasseval', None)
if passwd != None: if passwd != None:
@ -304,7 +299,6 @@ class IMAPRepository(BaseRepository):
# no strategy yielded a password! # no strategy yielded a password!
return None return None
def getfolder(self, foldername): def getfolder(self, foldername):
return self.getfoldertype()(self.imapserver, foldername, self) return self.getfoldertype()(self.imapserver, foldername, self)
@ -392,6 +386,7 @@ class IMAPRepository(BaseRepository):
when you are done creating folders yourself. when you are done creating folders yourself.
:param foldername: Full path of the folder to be created.""" :param foldername: Full path of the folder to be created."""
if self.getreference(): if self.getreference():
foldername = self.getreference() + self.getsep() + foldername foldername = self.getreference() + self.getsep() + foldername
if not foldername: # Create top level folder as folder separator if not foldername: # Create top level folder as folder separator

View File

@ -1,6 +1,5 @@
# Local status cache repository support # Local status cache repository support
# Copyright (C) 2002 John Goerzen # Copyright (C) 2002-2015 John Goerzen & contributors
# <jgoerzen@complete.org>
# #
# This program is free software; you can redistribute it and/or modify # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -16,11 +15,11 @@
# along with this program; if not, write to the Free Software # along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import os
from offlineimap.folder.LocalStatus import LocalStatusFolder from offlineimap.folder.LocalStatus import LocalStatusFolder
from offlineimap.folder.LocalStatusSQLite import LocalStatusSQLiteFolder from offlineimap.folder.LocalStatusSQLite import LocalStatusSQLiteFolder
from offlineimap.repository.Base import BaseRepository from offlineimap.repository.Base import BaseRepository
import os
import re
class LocalStatusRepository(BaseRepository): class LocalStatusRepository(BaseRepository):
def __init__(self, reposname, account): def __init__(self, reposname, account):
@ -81,7 +80,7 @@ class LocalStatusRepository(BaseRepository):
return '.' return '.'
def makefolder(self, foldername): def makefolder(self, foldername):
"""Create a LocalStatus Folder""" """Create a LocalStatus Folder."""
if self.account.dryrun: if self.account.dryrun:
return # bail out in dry-run mode return # bail out in dry-run mode
@ -114,9 +113,11 @@ class LocalStatusRepository(BaseRepository):
(see getfolderfilename) so we can not derive folder names from (see getfolderfilename) so we can not derive folder names from
the file names that we have available. TODO: need to store a the file names that we have available. TODO: need to store a
list of folder names somehow?""" list of folder names somehow?"""
pass 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 = {} self._folders = {}

View File

@ -1,6 +1,5 @@
# Maildir repository support # Maildir repository support
# Copyright (C) 2002 John Goerzen # Copyright (C) 2002-2015 John Goerzen & contributors
# <jgoerzen@complete.org>
# #
# This program is free software; you can redistribute it and/or modify # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -27,6 +26,7 @@ class MaildirRepository(BaseRepository):
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."""
BaseRepository.__init__(self, reposname, account) BaseRepository.__init__(self, reposname, account)
self.root = self.getlocalroot() self.root = self.getlocalroot()
@ -41,6 +41,7 @@ class MaildirRepository(BaseRepository):
def _append_folder_atimes(self, foldername): 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"""
p = os.path.join(self.root, foldername) p = os.path.join(self.root, foldername)
new = os.path.join(p, 'new') new = os.path.join(p, 'new')
cur = os.path.join(p, 'cur') cur = os.path.join(p, 'cur')
@ -51,6 +52,7 @@ class MaildirRepository(BaseRepository):
"""Sets folders' atime back to their values after a sync """Sets folders' atime back to their values after a sync
Controlled by the 'restoreatime' config parameter.""" Controlled by the 'restoreatime' config parameter."""
if not self.getconfboolean('restoreatime', False): if not self.getconfboolean('restoreatime', False):
return # not configured to restore return # not configured to restore
@ -82,6 +84,7 @@ class MaildirRepository(BaseRepository):
levels will be created if they do not exist yet. 'cur', levels will be created if they do not exist yet. 'cur',
'tmp', and 'new' subfolders will be created in the maildir. 'tmp', and 'new' subfolders will be created in the maildir.
""" """
self.ui.makefolder(self, foldername) self.ui.makefolder(self, foldername)
if self.account.dryrun: if self.account.dryrun:
return return
@ -134,7 +137,7 @@ class MaildirRepository(BaseRepository):
"folder '%s'." % foldername, "folder '%s'." % foldername,
OfflineImapError.ERROR.FOLDER) OfflineImapError.ERROR.FOLDER)
def _getfolders_scandir(self, root, extension = None): def _getfolders_scandir(self, root, extension=None):
"""Recursively scan folder 'root'; return a list of MailDirFolder """Recursively scan folder 'root'; return a list of MailDirFolder
:param root: (absolute) path to Maildir root :param root: (absolute) path to Maildir root
@ -200,4 +203,5 @@ class MaildirRepository(BaseRepository):
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 = None

View File

@ -1,5 +1,5 @@
# Curses-based interfaces # Curses-based interfaces
# Copyright (C) 2003-2011 John Goerzen & contributors # Copyright (C) 2003-2015 John Goerzen & contributors
# #
# This program is free software; you can redistribute it and/or modify # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -20,15 +20,15 @@ from collections import deque
import time import time
import sys import sys
import os import os
import signal
import curses import curses
import logging import logging
from offlineimap.ui.UIBase import UIBase from offlineimap.ui.UIBase import UIBase
from offlineimap.threadutil import ExitNotifyThread from offlineimap.threadutil import ExitNotifyThread
import offlineimap import offlineimap
class CursesUtil:
class CursesUtil:
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
# iolock protects access to the # iolock protects access to the
self.iolock = RLock() self.iolock = RLock()
@ -323,6 +323,7 @@ class Blinkenlights(UIBase, CursesUtil):
Sets up things and adds them to self.logger. Sets up things and adds them to self.logger.
:returns: The logging.Handler() for console output""" :returns: The logging.Handler() for console output"""
# create console handler with a higher log level # create console handler with a higher log level
ch = CursesLogHandler() ch = CursesLogHandler()
#ch.setLevel(logging.DEBUG) #ch.setLevel(logging.DEBUG)
@ -337,6 +338,7 @@ class Blinkenlights(UIBase, CursesUtil):
def isusable(s): def isusable(s):
"""Returns true if the backend is usable ie Curses works""" """Returns true if the backend is usable ie Curses works"""
# Not a terminal? Can't use curses. # Not a terminal? Can't use curses.
if not sys.stdout.isatty() and sys.stdin.isatty(): if not sys.stdout.isatty() and sys.stdin.isatty():
return False return False
@ -392,6 +394,7 @@ class Blinkenlights(UIBase, CursesUtil):
def acct(self, *args): def acct(self, *args):
"""Output that we start syncing an account (and start counting)""" """Output that we start syncing an account (and start counting)"""
self.gettf().setcolor('purple') self.gettf().setcolor('purple')
super(Blinkenlights, self).acct(*args) super(Blinkenlights, self).acct(*args)

View File

@ -1,4 +1,4 @@
# Copyright (C) 2007-2011 John Goerzen & contributors # Copyright (C) 2007-2015 John Goerzen & contributors
# #
# This program is free software; you can redistribute it and/or modify # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by

View File

@ -1,5 +1,5 @@
# TTY UI # TTY UI
# Copyright (C) 2002-2011 John Goerzen & contributors # Copyright (C) 2002-2015 John Goerzen & contributors
# #
# This program is free software; you can redistribute it and/or modify # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -24,6 +24,7 @@ from offlineimap.ui.UIBase import UIBase
class TTYFormatter(logging.Formatter): class TTYFormatter(logging.Formatter):
"""Specific Formatter that adds thread information to the log output""" """Specific Formatter that adds thread information to the log output"""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
#super() doesn't work in py2.6 as 'logging' uses old-style class #super() doesn't work in py2.6 as 'logging' uses old-style class
logging.Formatter.__init__(self, *args, **kwargs) logging.Formatter.__init__(self, *args, **kwargs)
@ -46,12 +47,14 @@ class TTYFormatter(logging.Formatter):
log_str = " %s" % log_str log_str = " %s" % log_str
return log_str return log_str
class TTYUI(UIBase): class TTYUI(UIBase):
def setup_consolehandler(self): def setup_consolehandler(self):
"""Backend specific console handler """Backend specific console handler
Sets up things and adds them to self.logger. Sets up things and adds them to self.logger.
:returns: The logging.Handler() for console output""" :returns: The logging.Handler() for console output"""
# create console handler with a higher log level # create console handler with a higher log level
ch = logging.StreamHandler() ch = logging.StreamHandler()
#ch.setLevel(logging.DEBUG) #ch.setLevel(logging.DEBUG)
@ -67,10 +70,12 @@ class TTYUI(UIBase):
def isusable(self): def isusable(self):
"""TTYUI is reported as usable when invoked on a terminal""" """TTYUI is reported as usable when invoked on a terminal"""
return sys.stdout.isatty() and sys.stdin.isatty() return sys.stdout.isatty() and sys.stdin.isatty()
def getpass(self, accountname, config, errmsg = None): def getpass(self, accountname, config, errmsg=None):
"""TTYUI backend is capable of querying the password""" """TTYUI backend is capable of querying the password"""
if errmsg: if errmsg:
self.warn("%s: %s" % (accountname, errmsg)) self.warn("%s: %s" % (accountname, errmsg))
self._log_con_handler.acquire() # lock the console output self._log_con_handler.acquire() # lock the console output
@ -97,6 +102,7 @@ class TTYUI(UIBase):
implementations return 0 for successful sleep and 1 for an implementations return 0 for successful sleep and 1 for an
'abort', ie a request to sync immediately. 'abort', ie a request to sync immediately.
""" """
if sleepsecs > 0: if sleepsecs > 0:
if remainingsecs//60 != (remainingsecs-sleepsecs)//60: if remainingsecs//60 != (remainingsecs-sleepsecs)//60:
self.logger.info("Next refresh in %.1f minutes" % ( self.logger.info("Next refresh in %.1f minutes" % (

View File

@ -1,5 +1,5 @@
# UI base class # UI base class
# Copyright (C) 2002-2011 John Goerzen & contributors # Copyright (C) 2002-2015 John Goerzen & contributors
# #
# This program is free software; you can redistribute it and/or modify # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -19,7 +19,6 @@ import logging
import re import re
import time import time
import sys import sys
import os
import traceback import traceback
import threading import threading
try: try:
@ -46,22 +45,22 @@ def getglobalui():
return globalui return globalui
class UIBase(object): class UIBase(object):
def __init__(self, config, loglevel = logging.INFO): def __init__(self, config, loglevel=logging.INFO):
self.config = config self.config = config
# Is this a 'dryrun'? # Is this a 'dryrun'?
self.dryrun = config.getdefaultboolean('general', 'dry-run', False) self.dryrun = config.getdefaultboolean('general', 'dry-run', False)
self.debuglist = [] self.debuglist = []
"""list of debugtypes we are supposed to log""" # list of debugtypes we are supposed to log
self.debugmessages = {} self.debugmessages = {}
"""debugmessages in a deque(v) per thread(k)""" # debugmessages in a deque(v) per thread(k)
self.debugmsglen = 15 self.debugmsglen = 15
self.threadaccounts = {} self.threadaccounts = {}
"""dict linking active threads (k) to account names (v)""" # dict linking active threads (k) to account names (v)
self.acct_startimes = {} self.acct_startimes = {}
"""linking active accounts with the time.time() when sync started""" # linking active accounts with the time.time() when sync started
self.logfile = None self.logfile = None
self.exc_queue = Queue() self.exc_queue = Queue()
"""saves all occuring exceptions, so we can output them at the end""" # saves all occuring exceptions, so we can output them at the end
# create logger with 'OfflineImap' app # create logger with 'OfflineImap' app
self.logger = logging.getLogger('OfflineImap') self.logger = logging.getLogger('OfflineImap')
self.logger.setLevel(loglevel) self.logger.setLevel(loglevel)
@ -74,6 +73,7 @@ class UIBase(object):
Sets up things and adds them to self.logger. Sets up things and adds them to self.logger.
:returns: The logging.Handler() for console output""" :returns: The logging.Handler() for console output"""
# create console handler with a higher log level # create console handler with a higher log level
ch = logging.StreamHandler(sys.stdout) ch = logging.StreamHandler(sys.stdout)
#ch.setLevel(logging.DEBUG) #ch.setLevel(logging.DEBUG)
@ -95,12 +95,13 @@ class UIBase(object):
# write out more verbose initial info blurb on the log file # write out more verbose initial info blurb on the log file
p_ver = ".".join([str(x) for x in sys.version_info[0:3]]) p_ver = ".".join([str(x) for x in sys.version_info[0:3]])
msg = "OfflineImap %s starting...\n Python: %s Platform: %s\n "\ msg = "OfflineImap %s starting...\n Python: %s Platform: %s\n "\
"Args: %s" % (offlineimap.__bigversion__, p_ver, sys.platform, "Args: %s"% (offlineimap.__bigversion__, p_ver, sys.platform,
" ".join(sys.argv)) " ".join(sys.argv))
self.logger.info(msg) self.logger.info(msg)
def _msg(self, msg): def _msg(self, msg):
"""Display a message.""" """Display a message."""
# TODO: legacy function, rip out. # TODO: legacy function, rip out.
self.info(msg) self.info(msg)
@ -150,7 +151,8 @@ class UIBase(object):
self._msg(traceback.format_tb(instant_traceback)) self._msg(traceback.format_tb(instant_traceback))
def registerthread(self, account): def registerthread(self, account):
"""Register current thread as being associated with an account name""" """Register current thread as being associated with an account name."""
cur_thread = threading.currentThread() cur_thread = threading.currentThread()
if cur_thread in self.threadaccounts: if cur_thread in self.threadaccounts:
# was already associated with an old account, update info # was already associated with an old account, update info
@ -163,15 +165,17 @@ class UIBase(object):
self.threadaccounts[cur_thread] = account self.threadaccounts[cur_thread] = account
def unregisterthread(self, thr): def unregisterthread(self, thr):
"""Unregister a thread as being associated with an account name""" """Unregister a thread as being associated with an account name."""
if thr in self.threadaccounts: if thr in self.threadaccounts:
del self.threadaccounts[thr] del self.threadaccounts[thr]
self.debug('thread', "Unregister thread '%s'" % thr.getName()) self.debug('thread', "Unregister thread '%s'" % thr.getName())
def getthreadaccount(self, thr = None): def getthreadaccount(self, thr=None):
"""Get Account() for a thread (current if None) """Get Account() for a thread (current if None)
If no account has been registered with this thread, return 'None'""" If no account has been registered with this thread, return 'None'."""
if thr == None: if thr == None:
thr = threading.currentThread() thr = threading.currentThread()
if thr in self.threadaccounts: if thr in self.threadaccounts:
@ -215,6 +219,7 @@ class UIBase(object):
"""Return the type of a repository or Folder as string """Return the type of a repository or Folder as string
(IMAP, Gmail, Maildir, etc...)""" (IMAP, Gmail, Maildir, etc...)"""
prelimname = object.__class__.__name__.split('.')[-1] prelimname = object.__class__.__name__.split('.')[-1]
# Strip off extra stuff. # Strip off extra stuff.
return re.sub('(Folder|Repository)', '', prelimname) return re.sub('(Folder|Repository)', '', prelimname)
@ -223,6 +228,7 @@ class UIBase(object):
"""Returns true if this UI object is usable in the current """Returns true if this UI object is usable in the current
environment. For instance, an X GUI would return true if it's environment. For instance, an X GUI would return true if it's
being run in X with a valid DISPLAY setting, and false otherwise.""" being run in X with a valid DISPLAY setting, and false otherwise."""
return True return True
################################################## INPUT ################################################## INPUT
@ -231,9 +237,9 @@ class UIBase(object):
raise NotImplementedError("Prompting for a password is not supported"\ raise NotImplementedError("Prompting for a password is not supported"\
" in this UI backend.") " in this UI backend.")
def folderlist(self, list): def folderlist(self, folder_list):
return ', '.join(["%s[%s]" % \ return ', '.join(["%s[%s]"% \
(self.getnicename(x), x.getname()) for x in list]) (self.getnicename(x), x.getname()) for x in folder_list])
################################################## WARNINGS ################################################## WARNINGS
def msgtoreadonly(self, destfolder, uid, content, flags): def msgtoreadonly(self, destfolder, uid, content, flags):
@ -282,7 +288,8 @@ class UIBase(object):
pass pass
def connecting(self, hostname, port): def connecting(self, hostname, port):
"""Log 'Establishing connection to'""" """Log 'Establishing connection to'."""
if not self.logger.isEnabledFor(logging.INFO): return if not self.logger.isEnabledFor(logging.INFO): return
displaystr = '' displaystr = ''
hostname = hostname if hostname else '' hostname = hostname if hostname else ''
@ -292,19 +299,22 @@ class UIBase(object):
self.logger.info("Establishing connection%s" % displaystr) self.logger.info("Establishing connection%s" % displaystr)
def acct(self, account): def acct(self, account):
"""Output that we start syncing an account (and start counting)""" """Output that we start syncing an account (and start counting)."""
self.acct_startimes[account] = time.time() self.acct_startimes[account] = time.time()
self.logger.info("*** Processing account %s" % account) self.logger.info("*** Processing account %s" % account)
def acctdone(self, account): def acctdone(self, account):
"""Output that we finished syncing an account (in which time)""" """Output that we finished syncing an account (in which time)."""
sec = time.time() - self.acct_startimes[account] sec = time.time() - self.acct_startimes[account]
del self.acct_startimes[account] del self.acct_startimes[account]
self.logger.info("*** Finished account '%s' in %d:%02d" % self.logger.info("*** Finished account '%s' in %d:%02d" %
(account, sec // 60, sec % 60)) (account, sec // 60, sec % 60))
def syncfolders(self, src_repo, dst_repo): def syncfolders(self, src_repo, dst_repo):
"""Log 'Copying folder structure...'""" """Log 'Copying folder structure...'."""
if self.logger.isEnabledFor(logging.DEBUG): if self.logger.isEnabledFor(logging.DEBUG):
self.debug('', "Copying folder structure from %s to %s" %\ self.debug('', "Copying folder structure from %s to %s" %\
(src_repo, dst_repo)) (src_repo, dst_repo))
@ -329,12 +339,12 @@ class UIBase(object):
def validityproblem(self, folder): def validityproblem(self, folder):
self.logger.warning("UID validity problem for folder %s (repo %s) " self.logger.warning("UID validity problem for folder %s (repo %s) "
"(saved %d; got %d); skipping it. Please see FAQ " "(saved %d; got %d); skipping it. Please see FAQ "
"and manual on how to handle this." % \ "and manual on how to handle this."% \
(folder, folder.getrepository(), (folder, folder.getrepository(),
folder.get_saveduidvalidity(), folder.get_uidvalidity())) folder.get_saveduidvalidity(), folder.get_uidvalidity()))
def loadmessagelist(self, repos, folder): def loadmessagelist(self, repos, folder):
self.logger.debug("Loading message list for %s[%s]" % ( self.logger.debug(u"Loading message list for %s[%s]"% (
self.getnicename(repos), self.getnicename(repos),
folder)) folder))
@ -390,7 +400,8 @@ class UIBase(object):
self.logger.info("Collecting data from messages on %s" % source) self.logger.info("Collecting data from messages on %s" % source)
def serverdiagnostics(self, repository, type): def serverdiagnostics(self, repository, type):
"""Connect to repository and output useful information for debugging""" """Connect to repository and output useful information for debugging."""
conn = None conn = None
self._msg("%s repository '%s': type '%s'" % (type, repository.name, self._msg("%s repository '%s': type '%s'" % (type, repository.name,
self.getnicename(repository))) self.getnicename(repository)))
@ -441,8 +452,9 @@ class UIBase(object):
repository.imapserver.close() repository.imapserver.close()
def savemessage(self, debugtype, uid, flags, folder): def savemessage(self, debugtype, uid, flags, folder):
"""Output a log line stating that we save a msg""" """Output a log line stating that we save a msg."""
self.debug(debugtype, "Write mail '%s:%d' with flags %s" %
self.debug(debugtype, u"Write mail '%s:%d' with flags %s"%
(folder, uid, repr(flags))) (folder, uid, repr(flags)))
################################################## Threads ################################################## Threads
@ -462,42 +474,46 @@ class UIBase(object):
del self.debugmessages[thread] del self.debugmessages[thread]
def getThreadExceptionString(self, thread): def getThreadExceptionString(self, thread):
message = "Thread '%s' terminated with exception:\n%s" % \ message = u"Thread '%s' terminated with exception:\n%s"% \
(thread.getName(), thread.exit_stacktrace) (thread.getName(), thread.exit_stacktrace)
message += "\n" + self.getThreadDebugLog(thread) message += u"\n" + self.getThreadDebugLog(thread)
return message return message
def threadException(self, thread): def threadException(self, thread):
"""Called when a thread has terminated with an exception. """Called when a thread has terminated with an exception.
The argument is the ExitNotifyThread that has so terminated.""" The argument is the ExitNotifyThread that has so terminated."""
self.warn(self.getThreadExceptionString(thread)) self.warn(self.getThreadExceptionString(thread))
self.delThreadDebugLog(thread) self.delThreadDebugLog(thread)
self.terminate(100) self.terminate(100)
def terminate(self, exitstatus = 0, errortitle = None, errormsg = None): def terminate(self, exitstatus = 0, errortitle = None, errormsg = None):
"""Called to terminate the application.""" """Called to terminate the application."""
#print any exceptions that have occurred over the run #print any exceptions that have occurred over the run
if not self.exc_queue.empty(): if not self.exc_queue.empty():
self.warn("ERROR: Exceptions occurred during the run!") self.warn(u"ERROR: Exceptions occurred during the run!")
while not self.exc_queue.empty(): while not self.exc_queue.empty():
msg, exc, exc_traceback = self.exc_queue.get() msg, exc, exc_traceback = self.exc_queue.get()
if msg: if msg:
self.warn("ERROR: %s\n %s" % (msg, exc)) self.warn(u"ERROR: %s\n %s"% (msg, exc))
else: else:
self.warn("ERROR: %s" % (exc)) self.warn(u"ERROR: %s"% (exc))
if exc_traceback: if exc_traceback:
self.warn("\nTraceback:\n%s" %"".join( self.warn(u"\nTraceback:\n%s"% "".join(
traceback.format_tb(exc_traceback))) traceback.format_tb(exc_traceback)))
if errormsg and errortitle: 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: elif errormsg:
self.warn('%s\n' % errormsg) self.warn(u'%s\n' % errormsg)
sys.exit(exitstatus) sys.exit(exitstatus)
def threadExited(self, thread): def threadExited(self, thread):
"""Called when a thread has exited normally. Many UIs will """Called when a thread has exited normally.
just ignore this."""
Many UIs will just ignore this."""
self.delThreadDebugLog(thread) self.delThreadDebugLog(thread)
self.unregisterthread(thread) self.unregisterthread(thread)
@ -519,6 +535,7 @@ class UIBase(object):
:returns: 0/False if timeout expired, 1/2/True if there is a :returns: 0/False if timeout expired, 1/2/True if there is a
request to cancel the timer. request to cancel the timer.
""" """
abortsleep = False abortsleep = False
while sleepsecs > 0 and not abortsleep: while sleepsecs > 0 and not abortsleep:
if account.get_abort_event(): if account.get_abort_event():
@ -539,6 +556,7 @@ class UIBase(object):
implementations return 0 for successful sleep and 1 for an implementations return 0 for successful sleep and 1 for an
'abort', ie a request to sync immediately. 'abort', ie a request to sync immediately.
""" """
if sleepsecs > 0: if sleepsecs > 0:
if remainingsecs//60 != (remainingsecs-sleepsecs)//60: if remainingsecs//60 != (remainingsecs-sleepsecs)//60:
self.logger.debug("Next refresh in %.1f minutes" % ( self.logger.debug("Next refresh in %.1f minutes" % (

View File

@ -1,6 +1,5 @@
# Locking debugging code -- temporary # Locking debugging code -- temporary
# Copyright (C) 2003 John Goerzen # Copyright (C) 2003-2015 John Goerzen & contributors
# <jgoerzen@complete.org>
# #
# This program is free software; you can redistribute it and/or modify # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by

View File

@ -1,40 +1,37 @@
# Copyright 2013 Eygene A. Ryabinkin. # Copyright (C) 2013-2014 Eygene A. Ryabinkin and contributors
# #
# Collection of classes that implement const-like behaviour # Collection of classes that implement const-like behaviour
# for various objects. # for various objects.
import copy import copy
class ConstProxy (object): class ConstProxy(object):
""" """Implements read-only access to a given object
Implements read-only access to a given object that can be attached to each instance only once."""
that can be attached to each instance only once.
""" def __init__(self):
def __init__ (self):
self.__dict__['__source'] = None self.__dict__['__source'] = None
def __getattr__ (self, name): def __getattr__(self, name):
src = self.__dict__['__source'] src = self.__dict__['__source']
if src == None: if src == None:
raise ValueError ("using non-initialized ConstProxy() object") raise ValueError("using non-initialized ConstProxy() object")
return copy.deepcopy (getattr (src, name)) return copy.deepcopy(getattr(src, name))
def __setattr__ (self, name, value): 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)) (name, value))
def __delattr__ (self, name): 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)) (name))
def set_source (self, source): def set_source(self, source):
""" Sets source object for this instance. """ """ Sets source object for this instance. """
if (self.__dict__['__source'] != None): if (self.__dict__['__source'] != None):
raise ValueError ("source object is already set") raise ValueError("source object is already set")
self.__dict__['__source'] = source self.__dict__['__source'] = source