Merge tag 'v6.7.0' into maint
v6.7.0
This commit is contained in:
commit
cb8678a5b5
28
.github/ISSUE_TEMPLATE.md
vendored
Normal file
28
.github/ISSUE_TEMPLATE.md
vendored
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
> This v1.0 template stands in `.github/`.
|
||||||
|
|
||||||
|
### General informations
|
||||||
|
|
||||||
|
- OfflineIMAP version:
|
||||||
|
- server name or domain:
|
||||||
|
- CLI options:
|
||||||
|
|
||||||
|
```
|
||||||
|
Configuration file offlineimaprc goes here. REMOVE PRIVATE DATA.
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
The pythonfile file goes here (if any). REMOVE PRIVATE DATA.
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Log error
|
||||||
|
|
||||||
|
```
|
||||||
|
Logs go here. REMOVE PRIVATE DATA.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Steps to reproduce the error
|
||||||
|
|
||||||
|
-
|
||||||
|
-
|
||||||
|
|
29
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
29
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
> This v1.0 template stands in `.github/`.
|
||||||
|
|
||||||
|
### Peer reviews
|
||||||
|
|
||||||
|
Trick to [fetch the pull
|
||||||
|
request](https://help.github.com/articles/checking-out-pull-requests-locally):
|
||||||
|
there is a (read-only) `refs/pull/` namespace.
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
git fetch OFFICIAL_REPOSITORY_NAME pull/PULL_ID/head:LOCAL_BRANCH_NAME
|
||||||
|
```
|
||||||
|
|
||||||
|
### This PR
|
||||||
|
|
||||||
|
> Add character x `[x]`.
|
||||||
|
|
||||||
|
- [] I've read the [DCO](http://www.offlineimap.org/doc/dco.html).
|
||||||
|
- [] I've read the [Coding Guidelines](http://www.offlineimap.org/doc/CodingGuidelines.html)
|
||||||
|
- [] The relevant informations about the changes stands in the commit message, not here in the message of the pull request.
|
||||||
|
- [] Code changes follow the style of the files they change.
|
||||||
|
- [] Code is tested (provide details).
|
||||||
|
|
||||||
|
### References
|
||||||
|
|
||||||
|
- Issue #no_space
|
||||||
|
|
||||||
|
### Additional information
|
||||||
|
|
||||||
|
|
18
CODE_OF_CONDUCT.md
Normal file
18
CODE_OF_CONDUCT.md
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
|
||||||
|
# Realistic Code of Conduct
|
||||||
|
|
||||||
|
1. We mostly care about making our softwares better.
|
||||||
|
|
||||||
|
2. Everybody is free to decide how to contribute.
|
||||||
|
|
||||||
|
3. Free speech owns to anyone of us.
|
||||||
|
|
||||||
|
4. Feel offended? This might be very well-deserved.
|
||||||
|
|
||||||
|
5. We don't need a code of conduct imposed on us, thanks.
|
||||||
|
|
||||||
|
6. Ignoring this Realistic Code of Conduct is welcome.
|
||||||
|
|
||||||
|
<!--
|
||||||
|
vim: expandtab ts=2
|
||||||
|
-->
|
@ -7,11 +7,11 @@
|
|||||||
.. _maintainers: https://github.com/OfflineIMAP/offlineimap/blob/next/MAINTAINERS.rst
|
.. _maintainers: https://github.com/OfflineIMAP/offlineimap/blob/next/MAINTAINERS.rst
|
||||||
.. _mailing list: http://lists.alioth.debian.org/mailman/listinfo/offlineimap-project
|
.. _mailing list: http://lists.alioth.debian.org/mailman/listinfo/offlineimap-project
|
||||||
.. _Developer's Certificate of Origin: https://github.com/OfflineIMAP/offlineimap/blob/next/docs/doc-src/dco.rst
|
.. _Developer's Certificate of Origin: https://github.com/OfflineIMAP/offlineimap/blob/next/docs/doc-src/dco.rst
|
||||||
.. _Community's website: https://offlineimap.org
|
.. _Community's website: http://www.offlineimap.org
|
||||||
.. _APIs in OfflineIMAP: http://offlineimap.org/documentation.html#available-apis
|
.. _APIs in OfflineIMAP: http://www.offlineimap.org/documentation.html#available-apis
|
||||||
.. _documentation: https://offlineimap.org/documentation.html
|
.. _documentation: http://www.offlineimap.org/documentation.html
|
||||||
.. _Coding Guidelines: http://offlineimap.org/doc/CodingGuidelines.html
|
.. _Coding Guidelines: http://www.offlineimap.org/doc/CodingGuidelines.html
|
||||||
.. _Know the status of your patches: http://offlineimap.org/doc/GitAdvanced.html#know-the-status-of-your-patch-after-submission
|
.. _Know the status of your patches: http://www.offlineimap.org/doc/GitAdvanced.html#know-the-status-of-your-patch-after-submission
|
||||||
|
|
||||||
|
|
||||||
=================
|
=================
|
||||||
@ -27,6 +27,15 @@ contributions.
|
|||||||
.. contents:: :depth: 3
|
.. contents:: :depth: 3
|
||||||
|
|
||||||
|
|
||||||
|
Submit issues
|
||||||
|
=============
|
||||||
|
|
||||||
|
Issues are welcome to both Github_ and the `mailing list`_, at your own
|
||||||
|
convenience.
|
||||||
|
|
||||||
|
You might help closing some issues, too. :-)
|
||||||
|
|
||||||
|
|
||||||
For the imaptients
|
For the imaptients
|
||||||
==================
|
==================
|
||||||
|
|
||||||
@ -36,13 +45,6 @@ For the imaptients
|
|||||||
- All the `documentation`_
|
- All the `documentation`_
|
||||||
|
|
||||||
|
|
||||||
Submit issues
|
|
||||||
=============
|
|
||||||
|
|
||||||
Issues are welcome to both Github_ and the `mailing list`_, at your own
|
|
||||||
convenience.
|
|
||||||
|
|
||||||
|
|
||||||
Community
|
Community
|
||||||
=========
|
=========
|
||||||
|
|
||||||
|
266
Changelog.md
266
Changelog.md
@ -15,6 +15,272 @@ Note to mainainers:
|
|||||||
* The following excerpt is only usefull when rendered in the website.
|
* The following excerpt is only usefull when rendered in the website.
|
||||||
{:toc}
|
{:toc}
|
||||||
|
|
||||||
|
### OfflineIMAP v6.7.0 (2016-03-10)
|
||||||
|
|
||||||
|
#### Notes
|
||||||
|
|
||||||
|
New stable release out!
|
||||||
|
|
||||||
|
With the work of Ilias, maintainer at Debian, OfflineIMAP is learning a new CLI
|
||||||
|
option to help fixing filenames for the users using nametrans and updating from
|
||||||
|
versions prior to v6.3.5. Distribution maintainers might want to backport this
|
||||||
|
feature for their packaged versions out after v6.3.5. Have a look at commit
|
||||||
|
c84d23b65670f to know more.
|
||||||
|
|
||||||
|
OfflineIMAP earns the slogan "Get the emails where you need them", authored by
|
||||||
|
Norbert Preining.
|
||||||
|
|
||||||
|
Julien Danjou, the author of the book _The Hacker’s Guide To Python_, shared us
|
||||||
|
his screenshot of a running session of OfflineIMAP.
|
||||||
|
|
||||||
|
I recently created rooms for chat sessions at Gitter. It appears to be really
|
||||||
|
cool, supports seamless authentication with a github account, persistent logs,
|
||||||
|
desktop/mobile clients and many more usefull features. Join us at Gitter!
|
||||||
|
|
||||||
|
- https://gitter.im/OfflineIMAP/offlineimap [NEW]
|
||||||
|
- https://gitter.im/OfflineIMAP/imapfw [NEW]
|
||||||
|
|
||||||
|
Now, the OfflineIMAP community has 2 official websites:
|
||||||
|
|
||||||
|
- http://www.offlineimap.org (for offlineimap)
|
||||||
|
- http://imapfw.offlineimap.org (for imapfw) [NEW]
|
||||||
|
|
||||||
|
The Twitter account was resurrected, too. Feel free to join us:
|
||||||
|
|
||||||
|
https://twitter.com/OfflineIMAP
|
||||||
|
|
||||||
|
Finally, the teams of the OfflineIMAP organization at Github were renewed to
|
||||||
|
facilitate the integration of new contributors and directly improve both the
|
||||||
|
documentation and the websites.
|
||||||
|
|
||||||
|
As a side note, the [imapfw repository](https://github.com/OfflineIMAP/imapfw)
|
||||||
|
has now more than 50 stargazers. This is very encouraging.
|
||||||
|
|
||||||
|
Thank you much everybody for your various contributions into OfflineIMAP!
|
||||||
|
|
||||||
|
#### Authors
|
||||||
|
|
||||||
|
- Ben Boeckel (1)
|
||||||
|
- Ebben Aries (1)
|
||||||
|
- Ilias Tsitsimpis (1)
|
||||||
|
|
||||||
|
#### Features
|
||||||
|
|
||||||
|
- Introduce a code of conduct.
|
||||||
|
- Add github templates.
|
||||||
|
- Change hard coding of AF_UNSPEC to user-defined address-families per repository. [Ebben Aries]
|
||||||
|
- Add documentation for the ipv6 configuration option.
|
||||||
|
|
||||||
|
#### Fixes
|
||||||
|
|
||||||
|
- Identify and fix messages with FMD5 inconsistencies. [Ilias Tsitsimpis]
|
||||||
|
- Curses, UIBase: remove references to __bigversion__. [Ben Boeckel]
|
||||||
|
- Sphinx doc: remove usage of __bigversion__.
|
||||||
|
- MANIFEST: exclude rfcs (used for Pypi packages).
|
||||||
|
- Changelog: fix typo.
|
||||||
|
|
||||||
|
#### Changes
|
||||||
|
|
||||||
|
- release.sh: move the authors section up.
|
||||||
|
- release.sh: add pypi instructions.
|
||||||
|
- MAINTAINERS: update.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### OfflineIMAP v6.7.0-rc2 (2016-02-22)
|
||||||
|
|
||||||
|
#### Notes
|
||||||
|
|
||||||
|
Learn to abruptly abort on multiple Ctrl+C.
|
||||||
|
|
||||||
|
Some bugs got fixed. XOAUTH2 now honors the proxy configuration option. Error
|
||||||
|
message was improved when it fails to write a new mail in a local Maildir.
|
||||||
|
|
||||||
|
I've enabled the hook for integration with Github. You'll get notifications on
|
||||||
|
updates of the master branch of the repository (mostly for new releases). I may
|
||||||
|
write some tweets about OfflineIMAP sometimes.
|
||||||
|
|
||||||
|
#### Features
|
||||||
|
|
||||||
|
- Abort after three Ctrl-C keystrokes.
|
||||||
|
|
||||||
|
#### Fixes
|
||||||
|
|
||||||
|
- Fix year of copyright.
|
||||||
|
- Versioning: avoid confusing pip by spliting out __version__ with __revision__.
|
||||||
|
- Fix: exceptions.OSError might not have attribute EEXIST defined.
|
||||||
|
- XOAUTH2 handler: urlopen with proxied socket.
|
||||||
|
- Manual: small grammar fix.
|
||||||
|
- Fix typos in offlineimap(1) manpage.
|
||||||
|
|
||||||
|
#### Changes
|
||||||
|
|
||||||
|
- Update links to the new URL www.offlineimap.org.
|
||||||
|
|
||||||
|
|
||||||
|
### OfflineIMAP v6.7.0-rc1 (2016-01-24)
|
||||||
|
|
||||||
|
#### Notes
|
||||||
|
|
||||||
|
Starting a new cycle with all EXPERIMENTAL and TESTING stuff marked stable.
|
||||||
|
Otherwise, not much exciting yet. There's pending work that would need some
|
||||||
|
love by contributors:
|
||||||
|
|
||||||
|
- https://github.com/OfflineIMAP/offlineimap/issues/211
|
||||||
|
- https://github.com/OfflineIMAP/offlineimap/pull/111
|
||||||
|
- https://github.com/OfflineIMAP/offlineimap/issues/184
|
||||||
|
|
||||||
|
#### Features
|
||||||
|
|
||||||
|
- Allow authorization via XOAUTH2 using access token.
|
||||||
|
|
||||||
|
#### Fixes
|
||||||
|
|
||||||
|
- Revert "Don't output initial blurb in "quiet" mode".
|
||||||
|
- Fix Changelog.
|
||||||
|
|
||||||
|
#### Changes
|
||||||
|
|
||||||
|
- Declare newmail_hook option stable.
|
||||||
|
- Declare utime_from_header option stable.
|
||||||
|
- Decode foldernames is removed EXPERIMENTAL flag.
|
||||||
|
- Declare XOAUTH2 stable.
|
||||||
|
- Declare tls_level option stable.
|
||||||
|
- Declare IMAP Keywords option stable.
|
||||||
|
|
||||||
|
|
||||||
|
### OfflineIMAP v6.6.1 (2015-12-28)
|
||||||
|
|
||||||
|
#### Notes
|
||||||
|
|
||||||
|
This is a very small new stable release for two fixes.
|
||||||
|
|
||||||
|
Amending support for BINARY APPEND which is not correctly implemented. Also,
|
||||||
|
remove potential harms from dot files in a local maildir.
|
||||||
|
|
||||||
|
#### Fixes
|
||||||
|
|
||||||
|
- Bump imaplib2 from 2.53 to 2.52. Remove support for binary send.
|
||||||
|
- Ignore aloo dot files in the Maildir while scanning for mails.
|
||||||
|
|
||||||
|
|
||||||
|
### OfflineIMAP v6.6.0 (2015-12-05)
|
||||||
|
|
||||||
|
#### Features
|
||||||
|
|
||||||
|
- Maildir learns to mimic Dovecot's format of lower-case letters (a,b,c..) for
|
||||||
|
"custom flags" or user keywords.
|
||||||
|
|
||||||
|
#### Fixes
|
||||||
|
|
||||||
|
- Broken retry loop would break connection management.
|
||||||
|
- Replace rogue `print` statement by `self.ui.debug`.
|
||||||
|
|
||||||
|
#### Changes
|
||||||
|
|
||||||
|
- Bump imaplib2 from v2.52 to v2.53.
|
||||||
|
- Code cleanups.
|
||||||
|
- Add a full stack of all thread dump upon EXIT or KILL signal in thread debug
|
||||||
|
mode.
|
||||||
|
|
||||||
|
|
||||||
|
### OfflineIMAP v6.6.0-rc3 (2015-11-05)
|
||||||
|
|
||||||
|
#### Notes
|
||||||
|
|
||||||
|
Changes are slowing down and the code is under serious testing by some new
|
||||||
|
contributors. Everything expected at this time in the release cycle. Thanks to
|
||||||
|
them.
|
||||||
|
|
||||||
|
SSL is now enabled by default to prevent from sending private data in clear
|
||||||
|
stream to the wild.
|
||||||
|
|
||||||
|
#### Features
|
||||||
|
|
||||||
|
- Add new config option `filename_use_mail_timestamp`.
|
||||||
|
|
||||||
|
#### Fixes
|
||||||
|
|
||||||
|
- Bump from imaplib2 v2.51 to v2.52.
|
||||||
|
- Minor fixes.
|
||||||
|
|
||||||
|
#### Changes
|
||||||
|
|
||||||
|
- Enable SSL by default.
|
||||||
|
- Fix: avoid writing password to log.
|
||||||
|
- offlineimap.conf: improve namtrans doc a bit.
|
||||||
|
|
||||||
|
|
||||||
|
### OfflineIMAP v6.6.0-rc2 (2015-10-15)
|
||||||
|
|
||||||
|
#### Notes
|
||||||
|
|
||||||
|
Interesting job was done in this release with 3 new features:
|
||||||
|
|
||||||
|
- Support for XOAUTH2;
|
||||||
|
- New 'tls_level' configuration option to automatically discard insecure SSL protocols;
|
||||||
|
- New interface 'syslog' comes in, next to the -s CLI option. This allows better
|
||||||
|
integration with systemd.
|
||||||
|
|
||||||
|
I won't merge big changes until the stable is out. IOW, you can seriously start
|
||||||
|
testing this rc2.
|
||||||
|
|
||||||
|
#### Features
|
||||||
|
|
||||||
|
- Add a new syslog ui.
|
||||||
|
- Introduce the 'tls_level' configuration option.
|
||||||
|
- Learn XOAUTH2 authentication (used by Gmail servers).
|
||||||
|
- Manual IDLE section improved (minor).
|
||||||
|
|
||||||
|
#### Fixes
|
||||||
|
|
||||||
|
- Configuration option utime_from_header handles out-of-bounds dates.
|
||||||
|
- offlineimap.conf: fix erroneous assumption about ssl23.
|
||||||
|
- Fix status code to reflect success or failure of a sync.
|
||||||
|
- contrib/release.sh: fix changelog edition.
|
||||||
|
|
||||||
|
#### Changes
|
||||||
|
|
||||||
|
- Bump imaplib2 from v2.48 to v2.51.
|
||||||
|
- README: new section status and future.
|
||||||
|
- Minor code cleanups.
|
||||||
|
- Makefile: improve building of targz.
|
||||||
|
- systemd: log to syslog rather than stderr for better integration.
|
||||||
|
|
||||||
|
|
||||||
|
### OfflineIMAP v6.6.0-rc1 (2015-09-28)
|
||||||
|
|
||||||
|
#### Notes
|
||||||
|
|
||||||
|
Let's go with a new release.
|
||||||
|
|
||||||
|
Basic UTF support was implemented while it is still exeprimental. Use this with
|
||||||
|
care. OfflineIMAP can now send the logs to syslog and notify on new mail.
|
||||||
|
|
||||||
|
|
||||||
|
#### Features
|
||||||
|
|
||||||
|
- logging: add a switch to log to syslog.
|
||||||
|
- Added the newmail_hook.
|
||||||
|
- utf-7 feature is set experimental.
|
||||||
|
|
||||||
|
#### Fixes
|
||||||
|
|
||||||
|
- offlineimap.conf: fix a typo in the new mail hook example.
|
||||||
|
- Fix language.
|
||||||
|
- Fix spelling inconsistency.
|
||||||
|
- offlineimap.conf: don't use quotes for sep option.
|
||||||
|
- man page: fingerprint can be used with SSL.
|
||||||
|
- fix #225 « Runonce (offlineimap -o) does not stop if autorefresh is declared in DEFAULT section ».
|
||||||
|
- CONTRIBUTING: fix links to offlineimap.org.
|
||||||
|
|
||||||
|
#### Changes
|
||||||
|
|
||||||
|
- Bump imaplib2 from 2.43 to 2.48
|
||||||
|
- README: small improvements
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### OfflineIMAP v6.5.7 (2015-05-15)
|
### OfflineIMAP v6.5.7 (2015-05-15)
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
.. -*- coding: utf-8 -*-
|
.. -*- coding: utf-8 -*-
|
||||||
|
|
||||||
Official maintainers
|
Maintainers
|
||||||
====================
|
===========
|
||||||
|
|
||||||
Eygene Ryabinkin
|
Eygene Ryabinkin
|
||||||
email: rea at freebsd.org
|
email: rea at freebsd.org
|
||||||
@ -15,15 +15,31 @@ Nicolas Sebrecht
|
|||||||
email: nicolas.s-dev at laposte.net
|
email: nicolas.s-dev at laposte.net
|
||||||
github: nicolas33
|
github: nicolas33
|
||||||
|
|
||||||
Mailing List maintainers
|
|
||||||
========================
|
|
||||||
|
|
||||||
Eygene Ryabinkin
|
Github
|
||||||
email: rea at freebsd.org
|
------
|
||||||
|
|
||||||
Sebastian Spaeth
|
- Eygene Ryabinkin
|
||||||
email: sebastian at sspaeth.de
|
- Sebastian Spaeth
|
||||||
|
- Nicolas Sebrecht
|
||||||
|
|
||||||
Nicolas Sebrecht
|
|
||||||
email: nicolas.s-dev at laposte.net
|
|
||||||
|
|
||||||
|
Mailing List
|
||||||
|
------------
|
||||||
|
|
||||||
|
- Eygene Ryabinkin
|
||||||
|
- Sebastian Spaeth
|
||||||
|
- Nicolas Sebrecht
|
||||||
|
|
||||||
|
|
||||||
|
Twitter
|
||||||
|
-------
|
||||||
|
|
||||||
|
- Nicolas Sebrecht
|
||||||
|
|
||||||
|
|
||||||
|
Pypi
|
||||||
|
----
|
||||||
|
|
||||||
|
- Nicolas Sebrecht
|
||||||
|
- Sebastian Spaeth
|
||||||
|
@ -8,7 +8,9 @@ include Makefile
|
|||||||
include README.md
|
include README.md
|
||||||
include offlineimap.conf*
|
include offlineimap.conf*
|
||||||
include offlineimap.py
|
include offlineimap.py
|
||||||
|
recursive-include contrib *
|
||||||
recursive-include offlineimap *.py
|
recursive-include offlineimap *.py
|
||||||
recursive-include bin *
|
recursive-include bin *
|
||||||
recursive-include docs *
|
recursive-include docs *
|
||||||
recursive-include test *
|
recursive-include test *
|
||||||
|
prune docs/rfcs
|
||||||
|
15
Makefile
15
Makefile
@ -15,8 +15,9 @@
|
|||||||
# 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
|
||||||
|
|
||||||
VERSION=`./offlineimap.py --version`
|
VERSION=$(shell ./offlineimap.py --version)
|
||||||
TARGZ=offlineimap_$(VERSION).tar.gz
|
ABBREV=$(shell git log --format='%h' HEAD~1..)
|
||||||
|
TARGZ=offlineimap-$(VERSION)-$(ABBREV)
|
||||||
SHELL=/bin/bash
|
SHELL=/bin/bash
|
||||||
RST2HTML=`type rst2html >/dev/null 2>&1 && echo rst2html || echo rst2html.py`
|
RST2HTML=`type rst2html >/dev/null 2>&1 && echo rst2html || echo rst2html.py`
|
||||||
|
|
||||||
@ -30,12 +31,12 @@ build:
|
|||||||
|
|
||||||
clean:
|
clean:
|
||||||
-python setup.py clean --all
|
-python setup.py clean --all
|
||||||
-rm -f bin/offlineimapc
|
-rm -f bin/offlineimapc 2>/dev/null
|
||||||
-find . -name '*.pyc' -exec rm -f {} \;
|
-find . -name '*.pyc' -exec rm -f {} \;
|
||||||
-find . -name '*.pygc' -exec rm -f {} \;
|
-find . -name '*.pygc' -exec rm -f {} \;
|
||||||
-find . -name '*.class' -exec rm -f {} \;
|
-find . -name '*.class' -exec rm -f {} \;
|
||||||
-find . -name '.cache*' -exec rm -f {} \;
|
-find . -name '.cache*' -exec rm -f {} \;
|
||||||
-rm -f manpage.links manpage.refs
|
-rm -f manpage.links manpage.refs 2>/dev/null
|
||||||
-find . -name auth -exec rm -vf {}/password {}/username \;
|
-find . -name auth -exec rm -vf {}/password {}/username \;
|
||||||
@$(MAKE) -C clean
|
@$(MAKE) -C clean
|
||||||
|
|
||||||
@ -47,11 +48,7 @@ websitedoc:
|
|||||||
|
|
||||||
targz: ../$(TARGZ)
|
targz: ../$(TARGZ)
|
||||||
../$(TARGZ):
|
../$(TARGZ):
|
||||||
if ! pwd | grep -q "/offlineimap-$(VERSION)$$"; then \
|
cd .. && tar -zhcv --transform s,^offlineimap,$(TARGZ), -f $(TARGZ).tar.gz --exclude '*.pyc' offlineimap/{bin,Changelog.md,contrib,CONTRIBUTING.rst,COPYING,docs,MAINTAINERS.rst,MANIFEST.in,offlineimap,offlineimap.conf,offlineimap.conf.minimal,offlineimap.py,README.md,scripts,setup.py,test,TODO.rst}
|
||||||
echo "Containing directory must be called offlineimap-$(VERSION)"; \
|
|
||||||
exit 1; \
|
|
||||||
fi; \
|
|
||||||
pwd && cd .. && pwd && tar -zhcv --exclude '.git' --exclude 'website' --exclude 'wiki' -f $(TARGZ) offlineimap-$(VERSION)
|
|
||||||
|
|
||||||
rpm: targz
|
rpm: targz
|
||||||
cd .. && sudo rpmbuild -ta $(TARGZ)
|
cd .. && sudo rpmbuild -ta $(TARGZ)
|
||||||
|
62
README.md
62
README.md
@ -1,22 +1,46 @@
|
|||||||
[offlineimap]: https://github.com/OfflineIMAP/offlineimap
|
[offlineimap]: http://github.com/OfflineIMAP/offlineimap
|
||||||
[website]: http://offlineimap.org
|
[website]: http://www.offlineimap.org
|
||||||
[wiki]: http://github.com/OfflineIMAP/offlineimap/wiki
|
[wiki]: http://github.com/OfflineIMAP/offlineimap/wiki
|
||||||
|
[blog]: http://www.offlineimap.org/posts.html
|
||||||
|
|
||||||
# OfflineImap
|
# OfflineIMAP
|
||||||
|
|
||||||
|
***Get the emails where you need them.***
|
||||||
|
|
||||||
## Description
|
## Description
|
||||||
|
|
||||||
OfflineIMAP is a software to dispose your e-mail mailbox(es) as a **local
|
OfflineIMAP is a software to dispose your e-mail mailbox(es) as a **local
|
||||||
Maildir**. OfflineIMAP will synchronize both sides via *IMAP*.
|
Maildir**. OfflineIMAP will synchronize both sides via *IMAP*.
|
||||||
|
|
||||||
The main downside about IMAP is that you have to **trust** your MAIL provider to
|
The main downside about IMAP is that you have to **trust** your email provider to
|
||||||
not loose your mails. This is not something impossible while not very common.
|
not lose your mails. This is not something impossible while not very common.
|
||||||
With OfflineIMAP, you can download your Mailboxes and make you own backups of
|
With OfflineIMAP, you can download your Mailboxes and make you own backups of
|
||||||
the Maildir.
|
the [Maildir](https://en.wikipedia.org/wiki/Maildir).
|
||||||
|
|
||||||
This allows reading your mails while offline without the need for the mail
|
This allows reading your email while offline without the need for the mail
|
||||||
reader (MUA) to support IMAP disconnected operations. Need an attachement from a
|
reader (MUA) to support IMAP disconnected operations. Need an attachment from a
|
||||||
message without internet? It's fine, the message is still there.
|
message without internet connection? It's fine, the message is still there.
|
||||||
|
|
||||||
|
|
||||||
|
## Project status and future
|
||||||
|
|
||||||
|
> As one of the maintainer of OfflineIMAP, I'd like to put my efforts into
|
||||||
|
> [imapfw](http://github.com/OfflineIMAP/imapfw). **imapfw** is a software in
|
||||||
|
> development that I intend to replace OfflineIMAP in the long term.
|
||||||
|
>
|
||||||
|
> That's why I'm not going to do development in OfflineIMAP. I continue to do
|
||||||
|
> the maintenance job in OfflineIMAP: fixing small bugs, (quick)
|
||||||
|
> reviewing/merging patches and rolling out new releases, but that's all.
|
||||||
|
>
|
||||||
|
> While I keep tracking issues for OfflineIMAP, you should not expect support
|
||||||
|
> much from me anymore.
|
||||||
|
>
|
||||||
|
> You won't be left at the side. OfflineIMAP's community is large enough so that
|
||||||
|
> you'll find people for most of your issues.
|
||||||
|
>
|
||||||
|
> Get news from the [blog][blog].
|
||||||
|
>
|
||||||
|
> Nicolas Sebrecht. ,-)
|
||||||
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
@ -31,17 +55,16 @@ GNU General Public License v2.
|
|||||||
* It is **flexible**.
|
* It is **flexible**.
|
||||||
* It is **safe**.
|
* It is **safe**.
|
||||||
|
|
||||||
|
|
||||||
## Downloads
|
## Downloads
|
||||||
|
|
||||||
You should first check if your distribution already package OfflineIMAP for you.
|
You should first check if your distribution already packages OfflineIMAP for you.
|
||||||
Downloads releases as [tarball or zipball](https://github.com/OfflineIMAP/offlineimap/tags).
|
Downloads releases as [tarball or zipball](https://github.com/OfflineIMAP/offlineimap/tags).
|
||||||
|
|
||||||
|
|
||||||
## Feedbacks and contributions
|
## Feedbacks and contributions
|
||||||
|
|
||||||
**The user discussions, development, announces and all the exciting stuff take
|
**The user discussions, development, announcements and all the exciting stuff take
|
||||||
place in the mailing list.** While not mandatory to send emails, you can
|
place on the mailing list.** While not mandatory to send emails, you can
|
||||||
[subscribe here](http://lists.alioth.debian.org/mailman/listinfo/offlineimap-project).
|
[subscribe here](http://lists.alioth.debian.org/mailman/listinfo/offlineimap-project).
|
||||||
|
|
||||||
Bugs, issues and contributions can be requested to both the mailing list or the
|
Bugs, issues and contributions can be requested to both the mailing list or the
|
||||||
@ -59,20 +82,21 @@ Bugs, issues and contributions can be requested to both the mailing list or the
|
|||||||
|
|
||||||
* Python v2.7
|
* Python v2.7
|
||||||
* Python SQlite (optional while recommended)
|
* Python SQlite (optional while recommended)
|
||||||
|
* Python json and urllib (used for XOAuth2 authentication)
|
||||||
|
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
All the current and updated documentation is at the [community's website][website].
|
All the current and updated documentation is at the [community's website][website].
|
||||||
|
|
||||||
### Dispose locally
|
### Read documentation locally
|
||||||
|
|
||||||
You might want to dispose the documentation locally. Get the sources of the website.
|
You might want to read the documentation locally. Get the sources of the website.
|
||||||
For the other documentations, run the approppriate make target:
|
For the other documentation, run the appropriate make target:
|
||||||
```
|
```
|
||||||
$ ./scripts/get-repository.sh website
|
$ ./scripts/get-repository.sh website
|
||||||
$ cd docs
|
$ cd docs
|
||||||
$ make html # Require rst2html
|
$ make html # Requires rst2html
|
||||||
$ make man # Require a2x
|
$ make man # Requires a2x
|
||||||
$ make api # Require sphinx
|
$ make api # Requires sphinx
|
||||||
```
|
```
|
||||||
|
4
TODO.rst
4
TODO.rst
@ -120,8 +120,4 @@ TODO list
|
|||||||
so don't matter much about that if you don't get the point or what could be
|
so don't matter much about that if you don't get the point or what could be
|
||||||
done.
|
done.
|
||||||
|
|
||||||
|
|
||||||
* Support Python 3.
|
|
||||||
|
|
||||||
|
|
||||||
* Support Unicode.
|
* Support Unicode.
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
# TODO: move configuration out and source it.
|
# TODO: move configuration out and source it.
|
||||||
# TODO: implement rollback.
|
# TODO: implement rollback.
|
||||||
|
|
||||||
__VERSION__='v0.2'
|
__VERSION__='v0.3'
|
||||||
|
|
||||||
SPHINXBUILD=sphinx-build
|
SPHINXBUILD=sphinx-build
|
||||||
|
|
||||||
@ -29,6 +29,7 @@ CHANGELOG='Changelog.md'
|
|||||||
CACHEDIR='.git/offlineimap-release'
|
CACHEDIR='.git/offlineimap-release'
|
||||||
WEBSITE='website'
|
WEBSITE='website'
|
||||||
WEBSITE_LATEST="${WEBSITE}/_data/latest.yml"
|
WEBSITE_LATEST="${WEBSITE}/_data/latest.yml"
|
||||||
|
ME='Nicolas Sebrecht'
|
||||||
|
|
||||||
TMP_CHANGELOG_EXCERPT="${CACHEDIR}/changelog.excerpt.md"
|
TMP_CHANGELOG_EXCERPT="${CACHEDIR}/changelog.excerpt.md"
|
||||||
TMP_CHANGELOG_EXCERPT_OLD="${TMP_CHANGELOG_EXCERPT}.old"
|
TMP_CHANGELOG_EXCERPT_OLD="${TMP_CHANGELOG_EXCERPT}.old"
|
||||||
@ -154,7 +155,19 @@ function update_offlineimap_version () {
|
|||||||
#
|
#
|
||||||
function get_git_history () {
|
function get_git_history () {
|
||||||
debug 'in get_git_history'
|
debug 'in get_git_history'
|
||||||
git log --oneline "${1}.." | sed -r -e 's,^(.),\- \1,'
|
git log --format='- %h %s. [%aN]' --no-merges "${1}.." | \
|
||||||
|
sed -r -e "s, \[${ME}\]$,,"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# $1: previous version
|
||||||
|
#
|
||||||
|
function get_git_who () {
|
||||||
|
debug 'in get_git_who'
|
||||||
|
echo
|
||||||
|
git shortlog --no-merges -sn "${1}.." | \
|
||||||
|
sed -r -e 's, +([0-9]+)\t(.*),- \2 (\1),'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -178,8 +191,15 @@ function changelog_template () {
|
|||||||
|
|
||||||
#### Notes
|
#### Notes
|
||||||
|
|
||||||
// Add some notes. Good notes are about what was done in this release.
|
// Add some notes. Good notes are about what was done in this release from the
|
||||||
// HINT: explain big changes.
|
// bigger perspective.
|
||||||
|
// HINT: explain most important changes.
|
||||||
|
|
||||||
|
#### Authors
|
||||||
|
|
||||||
|
The authors of this release.
|
||||||
|
|
||||||
|
// Use list syntax with '- '
|
||||||
|
|
||||||
#### Features
|
#### Features
|
||||||
|
|
||||||
@ -193,8 +213,8 @@ function changelog_template () {
|
|||||||
|
|
||||||
// Use list syntax with '- '
|
// Use list syntax with '- '
|
||||||
|
|
||||||
// The preformatted shortlog was added below.
|
// The preformatted log was added below. Make use of this to fill the sections
|
||||||
// Make use of this to fill the sections 'Features' and 'Fixes' above.
|
// above.
|
||||||
|
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
@ -213,6 +233,7 @@ function update_changelog () {
|
|||||||
then
|
then
|
||||||
changelog_template "$1" > "$TMP_CHANGELOG_EXCERPT"
|
changelog_template "$1" > "$TMP_CHANGELOG_EXCERPT"
|
||||||
get_git_history "$2" >> "$TMP_CHANGELOG_EXCERPT"
|
get_git_history "$2" >> "$TMP_CHANGELOG_EXCERPT"
|
||||||
|
get_git_who "$2" >> "$TMP_CHANGELOG_EXCERPT"
|
||||||
edit_file "the Changelog excerpt" $TMP_CHANGELOG_EXCERPT
|
edit_file "the Changelog excerpt" $TMP_CHANGELOG_EXCERPT
|
||||||
|
|
||||||
# Remove comments.
|
# Remove comments.
|
||||||
@ -231,12 +252,13 @@ function update_changelog () {
|
|||||||
|
|
||||||
# Check and edit Changelog.
|
# Check and edit Changelog.
|
||||||
ask "Next step: you'll be asked to review the diff of $CHANGELOG"
|
ask "Next step: you'll be asked to review the diff of $CHANGELOG"
|
||||||
action=$No
|
while true
|
||||||
while test ! $action -eq $Yes
|
|
||||||
do
|
do
|
||||||
git diff -- "$CHANGELOG" | less
|
git diff -- "$CHANGELOG" | less
|
||||||
ask 'edit Changelog?' $CHANGELOG
|
ask 'edit Changelog?' $CHANGELOG
|
||||||
action=$?
|
test ! $? -eq $Yes && break
|
||||||
|
# Asked to edit the Changelog; will loop again.
|
||||||
|
$EDITOR "$CHANGELOG"
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -352,6 +374,9 @@ OfflineIMAP $1 is out.
|
|||||||
Downloads:
|
Downloads:
|
||||||
http://github.com/OfflineIMAP/offlineimap/archive/${1}.tar.gz
|
http://github.com/OfflineIMAP/offlineimap/archive/${1}.tar.gz
|
||||||
http://github.com/OfflineIMAP/offlineimap/archive/${1}.zip
|
http://github.com/OfflineIMAP/offlineimap/archive/${1}.zip
|
||||||
|
|
||||||
|
Pip:
|
||||||
|
pip install --user git+https://github.com/OfflineIMAP/offlineimap.git@${1}
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -429,6 +454,17 @@ cat <<EOF
|
|||||||
Release is ready!
|
Release is ready!
|
||||||
Make your checks and push the changes for both offlineimap and the website.
|
Make your checks and push the changes for both offlineimap and the website.
|
||||||
Announce template stands in '$TMP_ANNOUNCE'.
|
Announce template stands in '$TMP_ANNOUNCE'.
|
||||||
|
Command samples to do manually:
|
||||||
|
- git push <remote> master:master
|
||||||
|
- git push <remote> next:next
|
||||||
|
- git push <remote> $new_version
|
||||||
|
- python setup.py sdist && twine upload dist/* && rm -rf dist MANIFEST
|
||||||
|
- cd website
|
||||||
|
- git checkout master
|
||||||
|
- git merge $branch_name
|
||||||
|
- git push <remote> master:master
|
||||||
|
- cd ..
|
||||||
|
- git send-email $TMP_ANNOUNCE
|
||||||
Have fun! ,-)
|
Have fun! ,-)
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ Description=Offlineimap Service
|
|||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=oneshot
|
Type=oneshot
|
||||||
ExecStart=/usr/bin/offlineimap -o
|
ExecStart=/usr/bin/offlineimap -o -u syslog
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=mail.target
|
WantedBy=mail.target
|
||||||
|
@ -3,7 +3,7 @@ Description=Offlineimap Service for account %i
|
|||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=oneshot
|
Type=oneshot
|
||||||
ExecStart=/usr/bin/offlineimap -o -a %i
|
ExecStart=/usr/bin/offlineimap -o -a %i -u syslog
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=mail.target
|
WantedBy=mail.target
|
||||||
|
@ -18,7 +18,7 @@ import sys, os
|
|||||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||||
sys.path.insert(0, os.path.abspath('../..'))
|
sys.path.insert(0, os.path.abspath('../..'))
|
||||||
|
|
||||||
from offlineimap import __version__, __bigversion__, __author__, __copyright__
|
from offlineimap import __version__, __author__, __copyright__
|
||||||
# -- General configuration -----------------------------------------------------
|
# -- General configuration -----------------------------------------------------
|
||||||
|
|
||||||
# Add any Sphinx extension module names here, as strings. They can be extensions
|
# Add any Sphinx extension module names here, as strings. They can be extensions
|
||||||
@ -50,7 +50,7 @@ copyright = __copyright__
|
|||||||
# The short X.Y version.
|
# The short X.Y version.
|
||||||
version = __version__
|
version = __version__
|
||||||
# The full version, including alpha/beta/rc tags.
|
# The full version, including alpha/beta/rc tags.
|
||||||
release = __bigversion__
|
release = __version__
|
||||||
|
|
||||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||||
# for a list of supported languages.
|
# for a list of supported languages.
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
.. OfflineImap documentation master file
|
.. OfflineImap documentation master file
|
||||||
.. _OfflineIMAP: http://offlineimap.org
|
.. _OfflineIMAP: http://www.offlineimap.org
|
||||||
|
|
||||||
|
|
||||||
Welcome to OfflineIMAP's developer documentation
|
Welcome to OfflineIMAP's developer documentation
|
||||||
|
@ -77,7 +77,7 @@ amounts of data. This option implies the -1 option.
|
|||||||
|
|
||||||
Overrides the accounts section in the config file.
|
Overrides the accounts section in the config file.
|
||||||
+
|
+
|
||||||
Allows to specify a particular account or set of accounts to sync without
|
Allows one to specify a particular account or set of accounts to sync without
|
||||||
having to edit the config file.
|
having to edit the config file.
|
||||||
|
|
||||||
|
|
||||||
@ -105,6 +105,10 @@ included), implies the single-thread option -1.
|
|||||||
|
|
||||||
Send logs to <file.log>.
|
Send logs to <file.log>.
|
||||||
|
|
||||||
|
-s::
|
||||||
|
|
||||||
|
Send logs to syslog.
|
||||||
|
|
||||||
|
|
||||||
-f <folder1[,folder1[,...]]>::
|
-f <folder1[,folder1[,...]]>::
|
||||||
|
|
||||||
@ -145,7 +149,7 @@ option is ignored if maxage is set.
|
|||||||
+
|
+
|
||||||
This overrides the default specified in the configuration file. The UI
|
This overrides the default specified in the configuration file. The UI
|
||||||
specified with -u will be forced to be used, even if checks determine that it
|
specified with -u will be forced to be used, even if checks determine that it
|
||||||
is not usable. Possible interface choices are: quiet, basic, ttyui,
|
is not usable. Possible interface choices are: quiet, basic, syslog, ttyui,
|
||||||
blinkenlights, machineui.
|
blinkenlights, machineui.
|
||||||
|
|
||||||
|
|
||||||
@ -159,6 +163,20 @@ blinkenlights, machineui.
|
|||||||
This option is only applicable in non-verbose mode.
|
This option is only applicable in non-verbose mode.
|
||||||
|
|
||||||
|
|
||||||
|
--migrate-fmd5-using-nametrans::
|
||||||
|
Migrate FMD5 hashes from versions prior to 6.3.5.
|
||||||
|
+
|
||||||
|
The way that FMD5 hashes are calculated was changed in version 6.3.5 (now using
|
||||||
|
the nametrans folder name) introducing a regression which may lead to
|
||||||
|
re-uploading all messages. Try and fix the above regression by calculating the
|
||||||
|
correct FMD5 values and renaming the corresponding messages.
|
||||||
|
|
||||||
|
CAUTION: Since the FMD5 part of the filename changes, this may lead to UID
|
||||||
|
conflicts. Ensure to dispose a proper backup of both the cache and the Maildir
|
||||||
|
before running this fix as well as verify the results using the `--dry-run'
|
||||||
|
flag first.
|
||||||
|
|
||||||
|
|
||||||
Synchronization Performance
|
Synchronization Performance
|
||||||
---------------------------
|
---------------------------
|
||||||
|
|
||||||
@ -207,7 +225,7 @@ in between.
|
|||||||
5. Turn off fsync.
|
5. Turn off fsync.
|
||||||
+
|
+
|
||||||
In the [general] section you can set fsync to True or False. If you want to
|
In the [general] section you can set fsync to True or False. If you want to
|
||||||
play 110% safe and wait for all operations to hit the disk before continueing,
|
play 110% safe and wait for all operations to hit the disk before continuing,
|
||||||
you can set this to True. If you set it to False, you lose some of that
|
you can set this to True. If you set it to False, you lose some of that
|
||||||
safety, trading it for speed.
|
safety, trading it for speed.
|
||||||
|
|
||||||
@ -215,7 +233,7 @@ safety, trading it for speed.
|
|||||||
Upgrading from plain text to SQLite cache format
|
Upgrading from plain text to SQLite cache format
|
||||||
------------------------------------------------
|
------------------------------------------------
|
||||||
|
|
||||||
OfflineImap uses a cache to store the last know status of mails (flags etc).
|
OfflineImap uses a cache to store the last known status of mails (flags etc).
|
||||||
|
|
||||||
Historically that has meant plain text files, but recently we introduced
|
Historically that has meant plain text files, but recently we introduced
|
||||||
sqlite-based cache, which helps with performance and CPU usage on large
|
sqlite-based cache, which helps with performance and CPU usage on large
|
||||||
@ -259,8 +277,8 @@ out the connection that is used by default.
|
|||||||
+
|
+
|
||||||
Unfortunately, by default we will not verify the certificate of an IMAP
|
Unfortunately, by default we will not verify the certificate of an IMAP
|
||||||
TLS/SSL server we connect to, so connecting by SSL is no guarantee against
|
TLS/SSL server we connect to, so connecting by SSL is no guarantee against
|
||||||
man-in-the-middle attacks. While verifying a server certificate fingerprint is
|
man-in-the-middle attacks. While verifying a server certificate checking the
|
||||||
being planned, it is not implemented yet. There is currently only one safe way
|
fingerprint is recommended. There is currently only one safe way
|
||||||
to ensure that you connect to the correct server in an encrypted manner: you
|
to ensure that you connect to the correct server in an encrypted manner: you
|
||||||
can specify a 'sslcacertfile' setting in your repository section of
|
can specify a 'sslcacertfile' setting in your repository section of
|
||||||
offlineimap.conf pointing to a file that contains (among others) a CA
|
offlineimap.conf pointing to a file that contains (among others) a CA
|
||||||
@ -340,6 +358,8 @@ Email will show up, but may not be processed until the next refresh cycle.
|
|||||||
|
|
||||||
- IMAP IDLE <-> IMAP IDLE doesn't work yet.
|
- IMAP IDLE <-> IMAP IDLE doesn't work yet.
|
||||||
|
|
||||||
|
- IDLE might stop syncing on a system suspend/resume.
|
||||||
|
|
||||||
- IDLE may only work "once" per refresh.
|
- IDLE may only work "once" per refresh.
|
||||||
+
|
+
|
||||||
If you encounter this bug, please send a report to the list!
|
If you encounter this bug, please send a report to the list!
|
||||||
@ -376,7 +396,7 @@ You should enable this option with a value like 10.
|
|||||||
|
|
||||||
* OfflineIMAP confused when mails change while in a sync.
|
* OfflineIMAP confused when mails change while in a sync.
|
||||||
+
|
+
|
||||||
When OfflineIMAP is syncing, some events happening since the invokation on
|
When OfflineIMAP is syncing, some events happening since the invocation on
|
||||||
remote or local side are badly handled. OfflineIMAP won't track for changes
|
remote or local side are badly handled. OfflineIMAP won't track for changes
|
||||||
during the sync.
|
during the sync.
|
||||||
|
|
||||||
@ -422,4 +442,4 @@ See Also
|
|||||||
--------
|
--------
|
||||||
|
|
||||||
offlineimapui(7), openssl(1), signal(7), sqlite3(1).
|
offlineimapui(7), openssl(1), signal(7), sqlite3(1).
|
||||||
http://offlineimap.org
|
http://www.offlineimap.org
|
||||||
|
@ -127,6 +127,17 @@ It will output nothing except errors and serious warnings. Like Basic, this
|
|||||||
user interface is not capable of reading a password from the keyboard; account
|
user interface is not capable of reading a password from the keyboard; account
|
||||||
passwords must be specified using one of the configuration file options.
|
passwords must be specified using one of the configuration file options.
|
||||||
|
|
||||||
|
|
||||||
|
Syslog
|
||||||
|
------
|
||||||
|
|
||||||
|
Syslog is designed for situations where OfflineIMAP is run as a daemon (e.g.,
|
||||||
|
as a systemd --user service), but errors should be forwarded to the system log.
|
||||||
|
Like Basic, this user interface is not capable of reading a password from the
|
||||||
|
keyboard; account passwords must be specified using one of the configuration
|
||||||
|
file options.
|
||||||
|
|
||||||
|
|
||||||
MachineUI
|
MachineUI
|
||||||
---------
|
---------
|
||||||
|
|
||||||
|
164
offlineimap.conf
164
offlineimap.conf
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
# This file documents *all* possible options and can be quite scary.
|
# This file documents *all* possible options and can be quite scary.
|
||||||
# Looking for a quick start? Take a look at offlineimap.conf.minimal.
|
# Looking for a quick start? Take a look at offlineimap.conf.minimal.
|
||||||
# More details can be found at http://offlineimap.org .
|
# More details can be found at http://www.offlineimap.org .
|
||||||
|
|
||||||
##################################################
|
##################################################
|
||||||
# Overview
|
# Overview
|
||||||
@ -295,6 +295,17 @@ remoterepository = RemoteExample
|
|||||||
#postsynchook = notifysync.sh
|
#postsynchook = notifysync.sh
|
||||||
|
|
||||||
|
|
||||||
|
# This option stands in the [Account Test] section.
|
||||||
|
#
|
||||||
|
# You can specify a newmail hook to execute an external command upon receipt
|
||||||
|
# of new mail in the INBOX.
|
||||||
|
#
|
||||||
|
# This example plays a sound file of your chosing when new mail arrives.
|
||||||
|
#
|
||||||
|
#newmail_hook = lambda: os.system("cvlc --play-and-stop --play-and-exit /path/to/sound/file.mp3" +
|
||||||
|
# " > /dev/null 2>&1")
|
||||||
|
|
||||||
|
|
||||||
# This option stands in the [Account Test] section.
|
# This option stands in the [Account Test] section.
|
||||||
#
|
#
|
||||||
# OfflineImap caches the state of the synchronisation to e.g. be able to
|
# OfflineImap caches the state of the synchronisation to e.g. be able to
|
||||||
@ -452,7 +463,9 @@ localfolders = ~/Test
|
|||||||
# ignored for IMAP repositories, as it is queried automatically.
|
# ignored for IMAP repositories, as it is queried automatically.
|
||||||
# Otherwise, default value is ".".
|
# Otherwise, default value is ".".
|
||||||
#
|
#
|
||||||
#sep = "."
|
# Don't use quotes.
|
||||||
|
#
|
||||||
|
#sep = .
|
||||||
|
|
||||||
|
|
||||||
# This option stands in the [Repository LocalExample] section.
|
# This option stands in the [Repository LocalExample] section.
|
||||||
@ -492,13 +505,58 @@ localfolders = ~/Test
|
|||||||
# file/message content.
|
# file/message content.
|
||||||
#
|
#
|
||||||
# If enabled, this forbid the -q (quick mode) CLI option to work correctly.
|
# If enabled, this forbid the -q (quick mode) CLI option to work correctly.
|
||||||
# This option is still "TESTING" feature.
|
|
||||||
#
|
#
|
||||||
# Default: no.
|
# Default: no.
|
||||||
#
|
#
|
||||||
#utime_from_header = no
|
#utime_from_header = no
|
||||||
|
|
||||||
|
|
||||||
|
# This option stands in the [Repository LocalExample] section.
|
||||||
|
#
|
||||||
|
# This option is similar to "utime_from_header" and could be use as a
|
||||||
|
# complementary feature to keep track of a message date. This option only
|
||||||
|
# makes sense for the Maildir type.
|
||||||
|
#
|
||||||
|
# By default each message is stored in a file which prefix is the fetch
|
||||||
|
# timestamp and an order rank such as "1446590057_0". In a multithreading
|
||||||
|
# environment message are fetched in a random order, then you can't trust
|
||||||
|
# the file name to sort your boxes.
|
||||||
|
#
|
||||||
|
# If set to "yes" the file name prefix if build on the message "Date" header
|
||||||
|
# (which should be present) or the "Received-date" if "Date" is not
|
||||||
|
# found. If neither "Received-date" nor "Date" is found, the current system
|
||||||
|
# date is used. Now you can quickly sort your messages using their file
|
||||||
|
# names.
|
||||||
|
#
|
||||||
|
# Used in combination with "utime_from_header" all your message would be in
|
||||||
|
# order with the correct mtime attribute.
|
||||||
|
#
|
||||||
|
#filename_use_mail_timestamp = no
|
||||||
|
|
||||||
|
# This option stands in the [Repository LocalExample] section.
|
||||||
|
#
|
||||||
|
# Map IMAP [user-defined] keywords to lowercase letters, similar to Dovecot's
|
||||||
|
# format described in http://wiki2.dovecot.org/MailboxFormat/Maildir . This
|
||||||
|
# option makes sense for the Maildir type, only.
|
||||||
|
#
|
||||||
|
# Configuration example:
|
||||||
|
# customflag_x = some_keyword
|
||||||
|
#
|
||||||
|
# With the configuration example above enabled, all IMAP messages that have
|
||||||
|
# 'some_keyword' in their FLAGS field will have an 'x' in the flags part of the
|
||||||
|
# maildir filename:
|
||||||
|
# 1234567890.M20046P2137.mailserver,S=4542,W=4642:2,Sx
|
||||||
|
#
|
||||||
|
# Valid fields are customflag_[a-z], valid values are whatever the IMAP server
|
||||||
|
# allows.
|
||||||
|
#
|
||||||
|
# Comparison in offlineimap is case-sensitive.
|
||||||
|
#
|
||||||
|
#customflag_a = some_keyword
|
||||||
|
#customflag_b = $OtherKeyword
|
||||||
|
#customflag_c = NonJunk
|
||||||
|
#customflag_d = ToDo
|
||||||
|
|
||||||
[Repository GmailLocalExample]
|
[Repository GmailLocalExample]
|
||||||
|
|
||||||
# This type of repository enables syncing of Gmail. All Maildir
|
# This type of repository enables syncing of Gmail. All Maildir
|
||||||
@ -522,6 +580,18 @@ type = GmailMaildir
|
|||||||
type = IMAP
|
type = IMAP
|
||||||
|
|
||||||
|
|
||||||
|
# This option stands in the [Repository RemoteExample] section.
|
||||||
|
#
|
||||||
|
# Configure which address family to use for the connection. If not specified,
|
||||||
|
# AF_UNSPEC is used as a fallback (default).
|
||||||
|
#
|
||||||
|
# AF_INET6:
|
||||||
|
#ipv6 = True
|
||||||
|
#
|
||||||
|
# AF_INET:
|
||||||
|
#ipv6 = False
|
||||||
|
|
||||||
|
|
||||||
# These options stands in the [Repository RemoteExample] section.
|
# These options stands in the [Repository RemoteExample] section.
|
||||||
#
|
#
|
||||||
# The following can fetch the account credentials via a python expression that
|
# The following can fetch the account credentials via a python expression that
|
||||||
@ -622,15 +692,35 @@ remotehost = examplehost
|
|||||||
|
|
||||||
# This option stands in the [Repository RemoteExample] section.
|
# This option stands in the [Repository RemoteExample] section.
|
||||||
#
|
#
|
||||||
# SSL version (optional).
|
# Set SSL version to use (optional).
|
||||||
#
|
#
|
||||||
# It is best to leave this unset, in which case the correct version will be
|
# It is best to leave this unset, in which case the correct version will be
|
||||||
# automatically detected. In rare cases, it may be necessary to specify a
|
# automatically detected. In rare cases, it may be necessary to specify a
|
||||||
# particular version from: tls1, ssl2, ssl3, ssl23 (SSLv2 or SSLv3)
|
# particular version from: tls1, ssl2, ssl3, ssl23.
|
||||||
|
#
|
||||||
|
# ssl23 is the highest protocol version that both the client and server support.
|
||||||
|
# Despite the name, this option can select “TLS” protocols as well as “SSL”.
|
||||||
|
#
|
||||||
|
# See the configuration option tls_level to automatically disable insecure
|
||||||
|
# protocols.
|
||||||
#
|
#
|
||||||
#ssl_version = ssl23
|
#ssl_version = ssl23
|
||||||
|
|
||||||
|
|
||||||
|
# This option stands in the [Repository RemoteExample] section.
|
||||||
|
#
|
||||||
|
# TLS support level (optional).
|
||||||
|
#
|
||||||
|
# Specify the level of support that should be allowed for this repository.
|
||||||
|
# Can be used to disallow insecure SSL versions as defined by IETF
|
||||||
|
# (see https://tools.ietf.org/html/rfc6176).
|
||||||
|
#
|
||||||
|
# Supported values are:
|
||||||
|
# tls_secure, tls_no_ssl, tls_compat (the default).
|
||||||
|
#
|
||||||
|
#tls_level = tls_compat
|
||||||
|
|
||||||
|
|
||||||
# This option stands in the [Repository RemoteExample] section.
|
# This option stands in the [Repository RemoteExample] section.
|
||||||
#
|
#
|
||||||
# Specify the port. If not specified, use a default port.
|
# Specify the port. If not specified, use a default port.
|
||||||
@ -673,9 +763,47 @@ remoteuser = username
|
|||||||
# limitations, if GSSAPI is set, it will be tried first, no matter where it was
|
# limitations, if GSSAPI is set, it will be tried first, no matter where it was
|
||||||
# specified in the list.
|
# specified in the list.
|
||||||
#
|
#
|
||||||
#auth_mechanisms = GSSAPI, CRAM-MD5, PLAIN, LOGIN
|
#auth_mechanisms = GSSAPI, CRAM-MD5, XOAUTH2, PLAIN, LOGIN
|
||||||
|
|
||||||
|
|
||||||
|
# This option stands in the [Repository RemoteExample] section.
|
||||||
|
#
|
||||||
|
# XOAuth2 authentication (for instance, to use with Gmail).
|
||||||
|
#
|
||||||
|
# This option was tested on Gmail only, but should work
|
||||||
|
# with type = IMAP for compatible servers.
|
||||||
|
#
|
||||||
|
# Mandatory parameters are "oauth2_client_id", "oauth2_client_secret" and
|
||||||
|
# either "oauth2_refresh_token" or "oauth2_access_token".
|
||||||
|
# See below to learn how to get those.
|
||||||
|
#
|
||||||
|
# Specify the OAuth2 client id and secret to use for the connection..
|
||||||
|
# Here's how to register an OAuth2 client for Gmail, as of 10-2-2016:
|
||||||
|
# - Go to the Google developer console
|
||||||
|
# https://console.developers.google.com/project
|
||||||
|
# - Create a new project
|
||||||
|
# - In API & Auth, select Credentials
|
||||||
|
# - Setup the OAuth Consent Screen
|
||||||
|
# - Then add Credentials of type OAuth 2.0 Client ID
|
||||||
|
# - Choose application type Other; type in a name for your client
|
||||||
|
# - You now have a client ID and client secret
|
||||||
|
#
|
||||||
|
#oauth2_client_id = YOUR_CLIENT_ID
|
||||||
|
#oauth2_client_secret = YOUR_CLIENT_SECRET
|
||||||
|
|
||||||
|
# Specify the refresh token to use for the connection to the mail server.
|
||||||
|
# Here's an example of a way to get a refresh token:
|
||||||
|
# - Clone this project: https://github.com/google/gmail-oauth2-tools
|
||||||
|
# - Type the following command-line in a terminal and follow the instructions
|
||||||
|
# python python/oauth2.py --generate_oauth2_token \
|
||||||
|
# --client_id=YOUR_CLIENT_ID --client_secret=YOUR_CLIENT_SECRET
|
||||||
|
# - Access token can be obtained using refresh token with command
|
||||||
|
# python python/oauth2.py --user=YOUR_EMAIL --client_id=YOUR_CLIENT_ID
|
||||||
|
# --client_secret=YOUR_CLIENT_SECRET --refresh_token=REFRESH_TOKEN
|
||||||
|
#
|
||||||
|
#oauth2_refresh_token = REFRESH_TOKEN
|
||||||
|
#oauth2_access_token = ACCESS_TOKEN
|
||||||
|
|
||||||
########## Passwords
|
########## Passwords
|
||||||
|
|
||||||
# There are six ways to specify the password for the IMAP server:
|
# There are six ways to specify the password for the IMAP server:
|
||||||
@ -762,6 +890,21 @@ remoteuser = username
|
|||||||
#reference = Mail
|
#reference = Mail
|
||||||
|
|
||||||
|
|
||||||
|
# This option stands in the [Repository RemoteExample] section.
|
||||||
|
#
|
||||||
|
# IMAP defines an encoding for non-ASCII ("international") characters. Enable
|
||||||
|
# this option if you want to decode them to the nowadays ubiquitous UTF-8.
|
||||||
|
#
|
||||||
|
# Note that the IMAP 4rev1 specification (RFC 3501) allows both UTF-8 and
|
||||||
|
# modified UTF-7 folder names.
|
||||||
|
#
|
||||||
|
# WARNING: with this option enabled:
|
||||||
|
# - compatibility with any other version is NOT GUARANTED (including newer);
|
||||||
|
# - no support is provided.
|
||||||
|
#
|
||||||
|
#decodefoldernames = no
|
||||||
|
|
||||||
|
|
||||||
# This option stands in the [Repository RemoteExample] section.
|
# This option stands in the [Repository RemoteExample] section.
|
||||||
#
|
#
|
||||||
# In between synchronisations, OfflineIMAP can monitor mailboxes for new
|
# In between synchronisations, OfflineIMAP can monitor mailboxes for new
|
||||||
@ -855,16 +998,17 @@ remoteuser = username
|
|||||||
# folders, UNLESS the second values are filtered out by folderfilter below.
|
# folders, UNLESS the second values are filtered out by folderfilter below.
|
||||||
# Failure to follow this rule will result in undefined behavior.
|
# Failure to follow this rule will result in undefined behavior.
|
||||||
#
|
#
|
||||||
# See the user documentation for details and use cases. They are also online at:
|
# If you enable nametrans, you will likely need to set the reversed nametrans on
|
||||||
# http://docs.offlineimap.org/en/latest/nametrans.html
|
# the other side. See the user documentation for details and use cases. They
|
||||||
|
# are also online at: http://www.offlineimap.org/doc/nametrans.html
|
||||||
#
|
#
|
||||||
# This example below will remove "INBOX." from the leading edge of folders
|
# This example below will remove "INBOX." from the leading edge of folders
|
||||||
# (great for Courier IMAP users).
|
# (great for Courier IMAP users).
|
||||||
#
|
#
|
||||||
#nametrans = lambda foldername: re.sub('^INBOX\.', '', foldername)
|
#nametrans = lambda foldername: re.sub('^INBOX\.', '', foldername)
|
||||||
#
|
#
|
||||||
# Using Courier remotely and want to duplicate its mailbox naming
|
# Using Courier remotely and want to duplicate its mailbox naming locally? Try
|
||||||
# locally? Try this:
|
# this:
|
||||||
#
|
#
|
||||||
#nametrans = lambda foldername: re.sub('^INBOX\.*', '.', foldername)
|
#nametrans = lambda foldername: re.sub('^INBOX\.*', '.', foldername)
|
||||||
|
|
||||||
|
@ -1,17 +1,16 @@
|
|||||||
__all__ = ['OfflineImap']
|
__all__ = ['OfflineImap']
|
||||||
|
|
||||||
__productname__ = 'OfflineIMAP'
|
__productname__ = 'OfflineIMAP'
|
||||||
__version__ = "6.5.7"
|
# Expecting trailing "-rcN" or "" for stable releases.
|
||||||
__revision__ = ""
|
__version__ = "6.7.0"
|
||||||
__bigversion__ = __version__ + __revision__
|
__copyright__ = "Copyright 2002-2016 John Goerzen & contributors"
|
||||||
__copyright__ = "Copyright 2002-2015 John Goerzen & contributors"
|
|
||||||
__author__ = "John Goerzen"
|
__author__ = "John Goerzen"
|
||||||
__author_email__= "offlineimap-project@lists.alioth.debian.org"
|
__author_email__= "offlineimap-project@lists.alioth.debian.org"
|
||||||
__description__ = "Disconnected Universal IMAP Mail Synchronization/Reader Support"
|
__description__ = "Disconnected Universal IMAP Mail Synchronization/Reader Support"
|
||||||
__license__ = "Licensed under the GNU GPL v2 or any later version (with an OpenSSL exception)"
|
__license__ = "Licensed under the GNU GPL v2 or any later version (with an OpenSSL exception)"
|
||||||
__bigcopyright__ = """%(__productname__)s %(__bigversion__)s
|
__bigcopyright__ = """%(__productname__)s %(__version__)s
|
||||||
%(__license__)s""" % locals()
|
%(__license__)s""" % locals()
|
||||||
__homepage__ = "http://offlineimap.org"
|
__homepage__ = "http://www.offlineimap.org"
|
||||||
|
|
||||||
banner = __bigcopyright__
|
banner = __bigcopyright__
|
||||||
|
|
||||||
|
@ -40,6 +40,11 @@ class BaseFolder(object):
|
|||||||
# Top level dir name is always ''
|
# Top level dir name is always ''
|
||||||
self.root = None
|
self.root = None
|
||||||
self.name = name if not name == self.getsep() else ''
|
self.name = name if not name == self.getsep() else ''
|
||||||
|
self.newmail_hook = None
|
||||||
|
# Only set the newmail_hook if the IMAP folder is named 'INBOX'
|
||||||
|
if self.name == 'INBOX':
|
||||||
|
self.newmail_hook = repository.newmail_hook
|
||||||
|
self.have_newmail = False
|
||||||
self.repository = repository
|
self.repository = repository
|
||||||
self.visiblename = repository.nametrans(name)
|
self.visiblename = repository.nametrans(name)
|
||||||
# In case the visiblename becomes '.' or '/' (top-level) we use
|
# In case the visiblename becomes '.' or '/' (top-level) we use
|
||||||
@ -55,6 +60,13 @@ class BaseFolder(object):
|
|||||||
self._utime_from_header = self.config.getdefaultboolean(repo,
|
self._utime_from_header = self.config.getdefaultboolean(repo,
|
||||||
"utime_from_header", utime_from_header_global)
|
"utime_from_header", utime_from_header_global)
|
||||||
|
|
||||||
|
# Do we need to use mail timestamp for filename prefix?
|
||||||
|
filename_use_mail_timestamp_global = self.config.getdefaultboolean(
|
||||||
|
"general", "filename_use_mail_timestamp", False)
|
||||||
|
repo = "Repository " + repository.name
|
||||||
|
self._filename_use_mail_timestamp = self.config.getdefaultboolean(repo,
|
||||||
|
"filename_use_mail_timestamp", filename_use_mail_timestamp_global)
|
||||||
|
|
||||||
# Determine if we're running static or dynamic folder filtering
|
# Determine if we're running static or dynamic folder filtering
|
||||||
# and check filtering status
|
# and check filtering status
|
||||||
self._dynamic_folderfilter = self.config.getdefaultboolean(
|
self._dynamic_folderfilter = self.config.getdefaultboolean(
|
||||||
@ -408,6 +420,11 @@ class BaseFolder(object):
|
|||||||
|
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def getmessagekeywords(self, uid):
|
||||||
|
"""Returns the keywords for the specified message."""
|
||||||
|
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
def savemessageflags(self, uid, flags):
|
def savemessageflags(self, uid, flags):
|
||||||
"""Sets the specified message's flags to the given set.
|
"""Sets the specified message's flags to the given set.
|
||||||
|
|
||||||
@ -781,6 +798,9 @@ class BaseFolder(object):
|
|||||||
# Got new UID, change the local uid.
|
# Got new UID, change the local uid.
|
||||||
# Save uploaded status in the statusfolder
|
# Save uploaded status in the statusfolder
|
||||||
statusfolder.savemessage(new_uid, message, flags, rtime)
|
statusfolder.savemessage(new_uid, message, flags, rtime)
|
||||||
|
# Check whether the mail has been seen
|
||||||
|
if 'S' not in flags:
|
||||||
|
self.have_newmail = True
|
||||||
elif new_uid == 0:
|
elif new_uid == 0:
|
||||||
# Message was stored to dstfolder, but we can't find it's UID
|
# Message was stored to dstfolder, but we can't find it's UID
|
||||||
# This means we can't link current message to the one created
|
# This means we can't link current message to the one created
|
||||||
@ -817,6 +837,9 @@ class BaseFolder(object):
|
|||||||
|
|
||||||
This function checks and protects us from action in dryrun mode."""
|
This function checks and protects us from action in dryrun mode."""
|
||||||
|
|
||||||
|
# We have no new mail yet
|
||||||
|
self.have_newmail = False
|
||||||
|
|
||||||
threads = []
|
threads = []
|
||||||
|
|
||||||
copylist = filter(lambda uid: not statusfolder.uidexists(uid),
|
copylist = filter(lambda uid: not statusfolder.uidexists(uid),
|
||||||
@ -854,6 +877,11 @@ class BaseFolder(object):
|
|||||||
for thread in threads:
|
for thread in threads:
|
||||||
thread.join()
|
thread.join()
|
||||||
|
|
||||||
|
# Execute new mail hook if we have new mail
|
||||||
|
if self.have_newmail:
|
||||||
|
if self.newmail_hook != None:
|
||||||
|
self.newmail_hook();
|
||||||
|
|
||||||
def __syncmessagesto_delete(self, dstfolder, statusfolder):
|
def __syncmessagesto_delete(self, dstfolder, statusfolder):
|
||||||
"""Pass 2: Remove locally deleted messages on dst.
|
"""Pass 2: Remove locally deleted messages on dst.
|
||||||
|
|
||||||
@ -880,6 +908,45 @@ class BaseFolder(object):
|
|||||||
return #don't delete messages in dry-run mode
|
return #don't delete messages in dry-run mode
|
||||||
dstfolder.deletemessages(deletelist)
|
dstfolder.deletemessages(deletelist)
|
||||||
|
|
||||||
|
def combine_flags_and_keywords(self, uid, dstfolder):
|
||||||
|
"""Combine the message's flags and keywords using the mapping for the
|
||||||
|
destination folder."""
|
||||||
|
|
||||||
|
# Take a copy of the message flag set, otherwise
|
||||||
|
# __syncmessagesto_flags() will fail because statusflags is actually a
|
||||||
|
# reference to selfflags (which it should not, but I don't have time to
|
||||||
|
# debug THAT).
|
||||||
|
selfflags = set(self.getmessageflags(uid))
|
||||||
|
|
||||||
|
try:
|
||||||
|
keywordmap = dstfolder.getrepository().getkeywordmap()
|
||||||
|
if keywordmap is None:
|
||||||
|
return selfflags
|
||||||
|
|
||||||
|
knownkeywords = set(keywordmap.keys())
|
||||||
|
|
||||||
|
selfkeywords = self.getmessagekeywords(uid)
|
||||||
|
|
||||||
|
if not knownkeywords >= selfkeywords:
|
||||||
|
#some of the message's keywords are not in the mapping, so
|
||||||
|
#skip them
|
||||||
|
|
||||||
|
skipped_keywords = list(selfkeywords - knownkeywords)
|
||||||
|
selfkeywords &= knownkeywords
|
||||||
|
|
||||||
|
self.ui.warn("Unknown keywords skipped: %s\n"
|
||||||
|
"You may want to change your configuration to include "
|
||||||
|
"those\n" % (skipped_keywords))
|
||||||
|
|
||||||
|
keywordletterset = set([keywordmap[keyw] for keyw in selfkeywords])
|
||||||
|
|
||||||
|
#add the mapped keywords to the list of message flags
|
||||||
|
selfflags |= keywordletterset
|
||||||
|
except NotImplementedError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return selfflags
|
||||||
|
|
||||||
def __syncmessagesto_flags(self, dstfolder, statusfolder):
|
def __syncmessagesto_flags(self, dstfolder, statusfolder):
|
||||||
"""Pass 3: Flag synchronization.
|
"""Pass 3: Flag synchronization.
|
||||||
|
|
||||||
@ -902,13 +969,13 @@ class BaseFolder(object):
|
|||||||
if uid < 0 or not dstfolder.uidexists(uid):
|
if uid < 0 or not dstfolder.uidexists(uid):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
selfflags = self.getmessageflags(uid)
|
|
||||||
|
|
||||||
if statusfolder.uidexists(uid):
|
if statusfolder.uidexists(uid):
|
||||||
statusflags = statusfolder.getmessageflags(uid)
|
statusflags = statusfolder.getmessageflags(uid)
|
||||||
else:
|
else:
|
||||||
statusflags = set()
|
statusflags = set()
|
||||||
|
|
||||||
|
selfflags = self.combine_flags_and_keywords(uid, dstfolder)
|
||||||
|
|
||||||
addflags = selfflags - statusflags
|
addflags = selfflags - statusflags
|
||||||
delflags = statusflags - selfflags
|
delflags = statusflags - selfflags
|
||||||
|
|
||||||
|
@ -72,11 +72,7 @@ class GmailFolder(IMAPFolder):
|
|||||||
(probably severity MESSAGE) if e.g. no message with
|
(probably severity MESSAGE) if e.g. no message with
|
||||||
this UID could be found.
|
this UID could be found.
|
||||||
"""
|
"""
|
||||||
imapobj = self.imapserver.acquireconnection()
|
data = self._fetch_from_imap(str(uid), 2)
|
||||||
try:
|
|
||||||
data = self._fetch_from_imap(imapobj, str(uid), 2)
|
|
||||||
finally:
|
|
||||||
self.imapserver.releaseconnection(imapobj)
|
|
||||||
|
|
||||||
# data looks now e.g.
|
# data looks now e.g.
|
||||||
#[('320 (X-GM-LABELS (...) UID 17061 BODY[] {2565}','msgbody....')]
|
#[('320 (X-GM-LABELS (...) UID 17061 BODY[] {2565}','msgbody....')]
|
||||||
|
@ -251,13 +251,22 @@ class IMAPFolder(BaseFolder):
|
|||||||
uid = long(options['UID'])
|
uid = long(options['UID'])
|
||||||
self.messagelist[uid] = self.msglist_item_initializer(uid)
|
self.messagelist[uid] = self.msglist_item_initializer(uid)
|
||||||
flags = imaputil.flagsimap2maildir(options['FLAGS'])
|
flags = imaputil.flagsimap2maildir(options['FLAGS'])
|
||||||
|
keywords = imaputil.flagsimap2keywords(options['FLAGS'])
|
||||||
rtime = imaplibutil.Internaldate2epoch(messagestr)
|
rtime = imaplibutil.Internaldate2epoch(messagestr)
|
||||||
self.messagelist[uid] = {'uid': uid, 'flags': flags, 'time': rtime}
|
self.messagelist[uid] = {'uid': uid, 'flags': flags, 'time': rtime,
|
||||||
|
'keywords': keywords}
|
||||||
self.ui.messagelistloaded(self.repository, self, self.getmessagecount())
|
self.ui.messagelistloaded(self.repository, self, self.getmessagecount())
|
||||||
|
|
||||||
def dropmessagelistcache(self):
|
def dropmessagelistcache(self):
|
||||||
self.messagelist = {}
|
self.messagelist = {}
|
||||||
|
|
||||||
|
# Interface from BaseFolder
|
||||||
|
def getvisiblename(self):
|
||||||
|
vname = super(IMAPFolder, self).getvisiblename()
|
||||||
|
if self.repository.getdecodefoldernames():
|
||||||
|
return imaputil.decode_mailbox_name(vname)
|
||||||
|
return vname
|
||||||
|
|
||||||
# Interface from BaseFolder
|
# Interface from BaseFolder
|
||||||
def getmessagelist(self):
|
def getmessagelist(self):
|
||||||
return self.messagelist
|
return self.messagelist
|
||||||
@ -266,18 +275,14 @@ class IMAPFolder(BaseFolder):
|
|||||||
def getmessage(self, uid):
|
def getmessage(self, uid):
|
||||||
"""Retrieve message with UID from the IMAP server (incl body).
|
"""Retrieve message with UID from the IMAP server (incl body).
|
||||||
|
|
||||||
After this function all CRLFs will be transformed to '\n'.
|
After this function all CRLFs will be transformed to '\n'.
|
||||||
|
|
||||||
:returns: the message body or throws and OfflineImapError
|
:returns: the message body or throws and OfflineImapError
|
||||||
(probably severity MESSAGE) if e.g. no message with
|
(probably severity MESSAGE) if e.g. no message with
|
||||||
this UID could be found.
|
this UID could be found.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
imapobj = self.imapserver.acquireconnection()
|
data = self._fetch_from_imap(str(uid), 2)
|
||||||
try:
|
|
||||||
data = self._fetch_from_imap(imapobj, str(uid), 2)
|
|
||||||
finally:
|
|
||||||
self.imapserver.releaseconnection(imapobj)
|
|
||||||
|
|
||||||
# data looks now e.g. [('320 (UID 17061 BODY[]
|
# data looks now e.g. [('320 (UID 17061 BODY[]
|
||||||
# {2565}','msgbody....')] we only asked for one message,
|
# {2565}','msgbody....')] we only asked for one message,
|
||||||
@ -302,6 +307,10 @@ class IMAPFolder(BaseFolder):
|
|||||||
def getmessageflags(self, uid):
|
def getmessageflags(self, uid):
|
||||||
return self.messagelist[uid]['flags']
|
return self.messagelist[uid]['flags']
|
||||||
|
|
||||||
|
# Interface from BaseFolder
|
||||||
|
def getmessagekeywords(self, uid):
|
||||||
|
return self.messagelist[uid]['keywords']
|
||||||
|
|
||||||
def __generate_randomheader(self, content):
|
def __generate_randomheader(self, content):
|
||||||
"""Returns a unique X-OfflineIMAP header
|
"""Returns a unique X-OfflineIMAP header
|
||||||
|
|
||||||
@ -667,7 +676,7 @@ class IMAPFolder(BaseFolder):
|
|||||||
return uid
|
return uid
|
||||||
|
|
||||||
|
|
||||||
def _fetch_from_imap(self, imapobj, uids, retry_num=1):
|
def _fetch_from_imap(self, uids, retry_num=1):
|
||||||
"""Fetches data from IMAP server.
|
"""Fetches data from IMAP server.
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
@ -677,22 +686,37 @@ class IMAPFolder(BaseFolder):
|
|||||||
|
|
||||||
Returns: data obtained by this query."""
|
Returns: data obtained by this query."""
|
||||||
|
|
||||||
query = "(%s)"% (" ".join(self.imap_query))
|
imapobj = self.imapserver.acquireconnection()
|
||||||
fails_left = retry_num # retry on dropped connection
|
try:
|
||||||
while fails_left:
|
query = "(%s)"% (" ".join(self.imap_query))
|
||||||
try:
|
fails_left = retry_num ## retry on dropped connection
|
||||||
imapobj.select(self.getfullname(), readonly = True)
|
while fails_left:
|
||||||
res_type, data = imapobj.uid('fetch', uids, query)
|
try:
|
||||||
fails_left = 0
|
imapobj.select(self.getfullname(), readonly = True)
|
||||||
except imapobj.abort as e:
|
res_type, data = imapobj.uid('fetch', uids, query)
|
||||||
# Release dropped connection, and get a new one
|
break
|
||||||
self.imapserver.releaseconnection(imapobj, True)
|
except imapobj.abort as e:
|
||||||
imapobj = self.imapserver.acquireconnection()
|
fails_left -= 1
|
||||||
self.ui.error(e, exc_info()[2])
|
# self.ui.error() will show the original traceback
|
||||||
fails_left -= 1
|
if fails_left <= 0:
|
||||||
# self.ui.error() will show the original traceback
|
message = ("%s, while fetching msg %r in folder %r."
|
||||||
if not fails_left:
|
" Max retry reached (%d)"%
|
||||||
raise e
|
(e, uids, self.name, retry_num))
|
||||||
|
severity = OfflineImapError.ERROR.MESSAGE
|
||||||
|
raise OfflineImapError(message,
|
||||||
|
OfflineImapError.ERROR.MESSAGE)
|
||||||
|
# Release dropped connection, and get a new one
|
||||||
|
self.imapserver.releaseconnection(imapobj, True)
|
||||||
|
imapobj = self.imapserver.acquireconnection()
|
||||||
|
self.ui.error("%s. While fetching msg %r in folder %r."
|
||||||
|
" Retrying (%d/%d)"%
|
||||||
|
(e, uids, self.name, retry_num - fails_left, retry_num))
|
||||||
|
finally:
|
||||||
|
# The imapobj here might be different than the one created before
|
||||||
|
# the ``try`` clause. So please avoid transforming this to a nice
|
||||||
|
# ``with`` without taking this into account.
|
||||||
|
self.imapserver.releaseconnection(imapobj)
|
||||||
|
|
||||||
if data == [None] or res_type != 'OK':
|
if data == [None] or res_type != 'OK':
|
||||||
#IMAP server says bad request or UID does not exist
|
#IMAP server says bad request or UID does not exist
|
||||||
severity = OfflineImapError.ERROR.MESSAGE
|
severity = OfflineImapError.ERROR.MESSAGE
|
||||||
|
@ -38,22 +38,20 @@ re_uidmatch = re.compile(',U=(\d+)')
|
|||||||
# Find a numeric timestamp in a string (filename prefix)
|
# Find a numeric timestamp in a string (filename prefix)
|
||||||
re_timestampmatch = re.compile('(\d+)');
|
re_timestampmatch = re.compile('(\d+)');
|
||||||
|
|
||||||
timeseq = 0
|
timehash = {}
|
||||||
lasttime = 0
|
|
||||||
timelock = Lock()
|
timelock = Lock()
|
||||||
|
|
||||||
def _gettimeseq():
|
def _gettimeseq(date=None):
|
||||||
global lasttime, timeseq, timelock
|
global timehash, timelock
|
||||||
timelock.acquire()
|
timelock.acquire()
|
||||||
try:
|
try:
|
||||||
thistime = long(time.time())
|
if date is None:
|
||||||
if thistime == lasttime:
|
date = long(time.time())
|
||||||
timeseq += 1
|
if timehash.has_key(date):
|
||||||
return (thistime, timeseq)
|
timehash[date] += 1
|
||||||
else:
|
else:
|
||||||
lasttime = thistime
|
timehash[date] = 0
|
||||||
timeseq = 0
|
return (date, timehash[date])
|
||||||
return (thistime, timeseq)
|
|
||||||
finally:
|
finally:
|
||||||
timelock.release()
|
timelock.release()
|
||||||
|
|
||||||
@ -137,9 +135,7 @@ class MaildirFolder(BaseFolder):
|
|||||||
uid = long(uidmatch.group(1))
|
uid = long(uidmatch.group(1))
|
||||||
flagmatch = self.re_flagmatch.search(filename)
|
flagmatch = self.re_flagmatch.search(filename)
|
||||||
if flagmatch:
|
if flagmatch:
|
||||||
# Filter out all lowercase (custom maildir) flags. We don't
|
flags = set((c for c in flagmatch.group(1)))
|
||||||
# handle them yet.
|
|
||||||
flags = set((c for c in flagmatch.group(1) if not c.islower()))
|
|
||||||
return prefix, uid, fmd5, flags
|
return prefix, uid, fmd5, flags
|
||||||
|
|
||||||
def _scanfolder(self, min_date=None, min_uid=None):
|
def _scanfolder(self, min_date=None, min_uid=None):
|
||||||
@ -151,7 +147,7 @@ class MaildirFolder(BaseFolder):
|
|||||||
with similar UID's (e.g. the UID was reassigned much later).
|
with similar UID's (e.g. the UID was reassigned much later).
|
||||||
|
|
||||||
Maildir flags are: R (replied) S (seen) T (trashed) D (draft) F
|
Maildir flags are: R (replied) S (seen) T (trashed) D (draft) F
|
||||||
(flagged).
|
(flagged), plus lower-case letters for custom flags.
|
||||||
:returns: dict that can be used as self.messagelist.
|
:returns: dict that can be used as self.messagelist.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -167,6 +163,8 @@ class MaildirFolder(BaseFolder):
|
|||||||
|
|
||||||
date_excludees = {}
|
date_excludees = {}
|
||||||
for dirannex, filename in files:
|
for dirannex, filename in files:
|
||||||
|
if filename.startswith('.'):
|
||||||
|
continue # Ignore dot files.
|
||||||
# We store just dirannex and filename, ie 'cur/123...'
|
# We store just dirannex and filename, ie 'cur/123...'
|
||||||
filepath = os.path.join(dirannex, filename)
|
filepath = os.path.join(dirannex, filename)
|
||||||
# Check maxsize if this message should be considered.
|
# Check maxsize if this message should be considered.
|
||||||
@ -269,14 +267,14 @@ class MaildirFolder(BaseFolder):
|
|||||||
filepath = os.path.join(self.getfullname(), filename)
|
filepath = os.path.join(self.getfullname(), filename)
|
||||||
return os.path.getmtime(filepath)
|
return os.path.getmtime(filepath)
|
||||||
|
|
||||||
def new_message_filename(self, uid, flags=set()):
|
def new_message_filename(self, uid, flags=set(), date=None):
|
||||||
"""Creates a new unique Maildir filename
|
"""Creates a new unique Maildir filename
|
||||||
|
|
||||||
: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(date)
|
||||||
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(),
|
||||||
uid, self._foldermd5, self.infosep, ''.join(sorted(flags)))
|
uid, self._foldermd5, self.infosep, ''.join(sorted(flags)))
|
||||||
@ -294,7 +292,8 @@ class MaildirFolder(BaseFolder):
|
|||||||
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.
|
||||||
|
# XXX: why do we need to loop 7 times?
|
||||||
tries = 7
|
tries = 7
|
||||||
while tries:
|
while tries:
|
||||||
tries = tries - 1
|
tries = tries - 1
|
||||||
@ -303,6 +302,8 @@ class MaildirFolder(BaseFolder):
|
|||||||
os.O_EXCL|os.O_CREAT|os.O_WRONLY, 0o666)
|
os.O_EXCL|os.O_CREAT|os.O_WRONLY, 0o666)
|
||||||
break
|
break
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
|
if not hasattr(e, 'EEXIST'):
|
||||||
|
raise
|
||||||
if e.errno == e.EEXIST:
|
if e.errno == e.EEXIST:
|
||||||
if tries:
|
if tries:
|
||||||
time.sleep(0.23)
|
time.sleep(0.23)
|
||||||
@ -346,13 +347,43 @@ class MaildirFolder(BaseFolder):
|
|||||||
# Otherwise, save the message in tmp/ and then call savemessageflags()
|
# Otherwise, save the message in tmp/ and then call savemessageflags()
|
||||||
# to give it a permanent home.
|
# to give it a permanent home.
|
||||||
tmpdir = os.path.join(self.getfullname(), 'tmp')
|
tmpdir = os.path.join(self.getfullname(), 'tmp')
|
||||||
messagename = self.new_message_filename(uid, flags)
|
|
||||||
|
# use the mail timestamp given by either Date or Delivery-date mail
|
||||||
|
# headers.
|
||||||
|
message_timestamp = None
|
||||||
|
if self._filename_use_mail_timestamp:
|
||||||
|
try:
|
||||||
|
message_timestamp = emailutil.get_message_date(content, 'Date')
|
||||||
|
if message_timestamp is None:
|
||||||
|
# Give a try with Delivery-date
|
||||||
|
date = emailutil.get_message_date(content, 'Delivery-date')
|
||||||
|
except:
|
||||||
|
# This should never happen
|
||||||
|
from email.Parser import Parser
|
||||||
|
from offlineimap.ui import getglobalui
|
||||||
|
datestr = Parser().parsestr(content, True).get("Date")
|
||||||
|
ui = getglobalui()
|
||||||
|
ui.warn("UID %d has invalid date %s: %s\n"
|
||||||
|
"Not using message timestamp as file prefix" % (uid, datestr, e))
|
||||||
|
# No need to check if date is None here since it would
|
||||||
|
# be overridden by _gettimeseq.
|
||||||
|
messagename = self.new_message_filename(uid, flags, date=message_timestamp)
|
||||||
tmpname = self.save_to_tmp_file(messagename, content)
|
tmpname = self.save_to_tmp_file(messagename, content)
|
||||||
|
|
||||||
if self.utime_from_header:
|
if self.utime_from_header:
|
||||||
date = emailutil.get_message_date(content, 'Date')
|
try:
|
||||||
if date != None:
|
date = emailutil.get_message_date(content, 'Date')
|
||||||
os.utime(os.path.join(self.getfullname(), tmpname), (date, date))
|
if date is not None:
|
||||||
|
os.utime(os.path.join(self.getfullname(), tmpname),
|
||||||
|
(date, date))
|
||||||
|
# In case date is wrongly so far into the future as to be > max int32
|
||||||
|
except Exception as e:
|
||||||
|
from email.Parser import Parser
|
||||||
|
from offlineimap.ui import getglobalui
|
||||||
|
datestr = Parser().parsestr(content, True).get("Date")
|
||||||
|
ui = getglobalui()
|
||||||
|
ui.warn("UID %d has invalid date %s: %s\n"
|
||||||
|
"Not changing file modification time" % (uid, datestr, e))
|
||||||
|
|
||||||
self.messagelist[uid] = self.msglist_item_initializer(uid)
|
self.messagelist[uid] = self.msglist_item_initializer(uid)
|
||||||
self.messagelist[uid]['flags'] = flags
|
self.messagelist[uid]['flags'] = flags
|
||||||
@ -386,8 +417,7 @@ class MaildirFolder(BaseFolder):
|
|||||||
|
|
||||||
if flags != self.messagelist[uid]['flags']:
|
if flags != self.messagelist[uid]['flags']:
|
||||||
# Flags have actually changed, construct new filename Strip
|
# Flags have actually changed, construct new filename Strip
|
||||||
# off existing infostring (possibly discarding small letter
|
# off existing infostring
|
||||||
# flags that dovecot uses TODO)
|
|
||||||
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
|
||||||
@ -455,3 +485,37 @@ class MaildirFolder(BaseFolder):
|
|||||||
os.unlink(filepath)
|
os.unlink(filepath)
|
||||||
# Yep -- return.
|
# Yep -- return.
|
||||||
del(self.messagelist[uid])
|
del(self.messagelist[uid])
|
||||||
|
|
||||||
|
def migratefmd5(self, dryrun=False):
|
||||||
|
"""Migrate FMD5 hashes from versions prior to 6.3.5
|
||||||
|
|
||||||
|
:param dryrun: Run in dry run mode
|
||||||
|
:type fix: Boolean
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
oldfmd5 = md5(self.name).hexdigest()
|
||||||
|
msglist = self._scanfolder()
|
||||||
|
for mkey, mvalue in msglist.iteritems():
|
||||||
|
filename = os.path.join(self.getfullname(), mvalue['filename'])
|
||||||
|
match = re.search("FMD5=([a-fA-F0-9]+)", filename)
|
||||||
|
if match is None:
|
||||||
|
self.ui.debug("maildir",
|
||||||
|
"File `%s' doesn't have an FMD5 assigned"
|
||||||
|
% filename)
|
||||||
|
elif match.group(1) == oldfmd5:
|
||||||
|
self.ui.info("Migrating file `%s' to FMD5 `%s'"
|
||||||
|
% (filename, self._foldermd5))
|
||||||
|
if not dryrun:
|
||||||
|
newfilename = filename.replace(
|
||||||
|
"FMD5=" + match.group(1), "FMD5=" + self._foldermd5)
|
||||||
|
try:
|
||||||
|
os.rename(filename, newfilename)
|
||||||
|
except OSError as e:
|
||||||
|
raise OfflineImapError(
|
||||||
|
"Can't rename file '%s' to '%s': %s" % (
|
||||||
|
filename, newfilename, e[1]),
|
||||||
|
OfflineImapError.ERROR.FOLDER), None, exc_info()[2]
|
||||||
|
elif match.group(1) != self._foldermd5:
|
||||||
|
self.ui.warn(("Inconsistent FMD5 for file `%s':"
|
||||||
|
" Neither `%s' nor `%s' found")
|
||||||
|
% (filename, oldfmd5, self._foldermd5))
|
||||||
|
246
offlineimap/imaplib2.py
Normal file → Executable file
246
offlineimap/imaplib2.py
Normal file → Executable file
@ -17,9 +17,9 @@ Public functions: Internaldate2Time
|
|||||||
__all__ = ("IMAP4", "IMAP4_SSL", "IMAP4_stream",
|
__all__ = ("IMAP4", "IMAP4_SSL", "IMAP4_stream",
|
||||||
"Internaldate2Time", "ParseFlags", "Time2Internaldate")
|
"Internaldate2Time", "ParseFlags", "Time2Internaldate")
|
||||||
|
|
||||||
__version__ = "2.43"
|
__version__ = "2.52"
|
||||||
__release__ = "2"
|
__release__ = "2"
|
||||||
__revision__ = "43"
|
__revision__ = "52"
|
||||||
__credits__ = """
|
__credits__ = """
|
||||||
Authentication code contributed by Donn Cave <donn@u.washington.edu> June 1998.
|
Authentication code contributed by Donn Cave <donn@u.washington.edu> June 1998.
|
||||||
String method conversion by ESR, February 2001.
|
String method conversion by ESR, February 2001.
|
||||||
@ -46,20 +46,27 @@ Fix for offlineimap "indexerror: string index out of range" bug provided by Eyge
|
|||||||
Fix for missing idle_lock in _handler() provided by Franklin Brook <franklin@brook.se> August 2014.
|
Fix for missing idle_lock in _handler() provided by Franklin Brook <franklin@brook.se> August 2014.
|
||||||
Conversion to Python3 provided by F. Malina <fmalina@gmail.com> February 2015.
|
Conversion to Python3 provided by F. Malina <fmalina@gmail.com> February 2015.
|
||||||
Fix for READ-ONLY error from multiple EXAMINE/SELECT calls by Pierre-Louis Bonicoli <pierre-louis.bonicoli@gmx.fr> March 2015.
|
Fix for READ-ONLY error from multiple EXAMINE/SELECT calls by Pierre-Louis Bonicoli <pierre-louis.bonicoli@gmx.fr> March 2015.
|
||||||
Fix for null strings appended to untagged responses by Pierre-Louis Bonicoli <pierre-louis.bonicoli@gmx.fr> March 2015."""
|
Fix for null strings appended to untagged responses by Pierre-Louis Bonicoli <pierre-louis.bonicoli@gmx.fr> March 2015.
|
||||||
|
Fix for correct byte encoding for _CRAM_MD5_AUTH taken from python3.5 imaplib.py June 2015.
|
||||||
|
Fix for correct Python 3 exception handling by Tobias Brink <tobias.brink@gmail.com> August 2015.
|
||||||
|
Fix to allow interruptible IDLE command by Tim Peoples <dromedary512@users.sf.net> September 2015.
|
||||||
|
Add support for TLS levels by Ben Boeckel <mathstuf@gmail.com> September 2015.
|
||||||
|
Fix for shutown exception by Sebastien Gross <seb@chezwam.org> November 2015."""
|
||||||
__author__ = "Piers Lauder <piers@janeelix.com>"
|
__author__ = "Piers Lauder <piers@janeelix.com>"
|
||||||
__URL__ = "http://imaplib2.sourceforge.net"
|
__URL__ = "http://imaplib2.sourceforge.net"
|
||||||
__license__ = "Python License"
|
__license__ = "Python License"
|
||||||
|
|
||||||
import binascii, errno, os, random, re, select, socket, sys, time, threading, zlib
|
import binascii, errno, os, random, re, select, socket, sys, time, threading, zlib
|
||||||
|
|
||||||
try:
|
if bytes != str:
|
||||||
import queue # py3
|
# Python 3, but NB assumes strings in all I/O
|
||||||
|
# for backwards compatibility with python 2 usage.
|
||||||
|
import queue
|
||||||
string_types = str
|
string_types = str
|
||||||
except ImportError:
|
else:
|
||||||
import Queue as queue # py2
|
import Queue as queue
|
||||||
string_types = basestring
|
string_types = basestring
|
||||||
|
threading.TIMEOUT_MAX = 9223372036854.0
|
||||||
|
|
||||||
select_module = select
|
select_module = select
|
||||||
|
|
||||||
@ -77,6 +84,10 @@ READ_SIZE = 32768 # Consume all available in socke
|
|||||||
|
|
||||||
DFLT_DEBUG_BUF_LVL = 3 # Level above which the logging output goes directly to stderr
|
DFLT_DEBUG_BUF_LVL = 3 # Level above which the logging output goes directly to stderr
|
||||||
|
|
||||||
|
TLS_SECURE = "tls_secure" # Recognised TLS levels
|
||||||
|
TLS_NO_SSL = "tls_no_ssl"
|
||||||
|
TLS_COMPAT = "tls_compat"
|
||||||
|
|
||||||
AllowedVersions = ('IMAP4REV1', 'IMAP4') # Most recent first
|
AllowedVersions = ('IMAP4REV1', 'IMAP4') # Most recent first
|
||||||
|
|
||||||
# Commands
|
# Commands
|
||||||
@ -179,7 +190,7 @@ class Request(object):
|
|||||||
def get_response(self, exc_fmt=None):
|
def get_response(self, exc_fmt=None):
|
||||||
self.callback = None
|
self.callback = None
|
||||||
if __debug__: self.parent._log(3, '%s:%s.ready.wait' % (self.name, self.tag))
|
if __debug__: self.parent._log(3, '%s:%s.ready.wait' % (self.name, self.tag))
|
||||||
self.ready.wait()
|
self.ready.wait(threading.TIMEOUT_MAX)
|
||||||
|
|
||||||
if self.aborted is not None:
|
if self.aborted is not None:
|
||||||
typ, val = self.aborted
|
typ, val = self.aborted
|
||||||
@ -319,6 +330,7 @@ class IMAP4(object):
|
|||||||
|
|
||||||
self.compressor = None # COMPRESS/DEFLATE if not None
|
self.compressor = None # COMPRESS/DEFLATE if not None
|
||||||
self.decompressor = None
|
self.decompressor = None
|
||||||
|
self._tls_established = False
|
||||||
|
|
||||||
# Create unique tag for this session,
|
# Create unique tag for this session,
|
||||||
# and compile tagged response matcher.
|
# and compile tagged response matcher.
|
||||||
@ -380,7 +392,7 @@ class IMAP4(object):
|
|||||||
# request and store CAPABILITY response.
|
# request and store CAPABILITY response.
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.welcome = self._request_push(tag='continuation').get_response('IMAP4 protocol error: %s')[1]
|
self.welcome = self._request_push(name='welcome', tag='continuation').get_response('IMAP4 protocol error: %s')[1]
|
||||||
|
|
||||||
if self._get_untagged_response('PREAUTH'):
|
if self._get_untagged_response('PREAUTH'):
|
||||||
self.state = AUTH
|
self.state = AUTH
|
||||||
@ -441,19 +453,22 @@ class IMAP4(object):
|
|||||||
af, socktype, proto, canonname, sa = res
|
af, socktype, proto, canonname, sa = res
|
||||||
try:
|
try:
|
||||||
s = socket.socket(af, socktype, proto)
|
s = socket.socket(af, socktype, proto)
|
||||||
except socket.error as msg:
|
except socket.error as m:
|
||||||
|
msg = m
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
for i in (0, 1):
|
for i in (0, 1):
|
||||||
try:
|
try:
|
||||||
s.connect(sa)
|
s.connect(sa)
|
||||||
break
|
break
|
||||||
except socket.error as msg:
|
except socket.error as m:
|
||||||
|
msg = m
|
||||||
if len(msg.args) < 2 or msg.args[0] != errno.EINTR:
|
if len(msg.args) < 2 or msg.args[0] != errno.EINTR:
|
||||||
raise
|
raise
|
||||||
else:
|
else:
|
||||||
raise socket.error(msg)
|
raise socket.error(msg)
|
||||||
except socket.error as msg:
|
except socket.error as m:
|
||||||
|
msg = m
|
||||||
s.close()
|
s.close()
|
||||||
continue
|
continue
|
||||||
break
|
break
|
||||||
@ -465,40 +480,60 @@ class IMAP4(object):
|
|||||||
|
|
||||||
def ssl_wrap_socket(self):
|
def ssl_wrap_socket(self):
|
||||||
|
|
||||||
# Allow sending of keep-alive messages - seems to prevent some servers
|
|
||||||
# from closing SSL, leading to deadlocks.
|
|
||||||
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import ssl
|
import ssl
|
||||||
|
|
||||||
|
TLS_MAP = {}
|
||||||
|
if hasattr(ssl, "PROTOCOL_TLSv1_2"): # py3
|
||||||
|
TLS_MAP[TLS_SECURE] = {
|
||||||
|
"tls1_2": ssl.PROTOCOL_TLSv1_2,
|
||||||
|
"tls1_1": ssl.PROTOCOL_TLSv1_1,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
TLS_MAP[TLS_SECURE] = {}
|
||||||
|
TLS_MAP[TLS_NO_SSL] = TLS_MAP[TLS_SECURE].copy()
|
||||||
|
TLS_MAP[TLS_NO_SSL].update({
|
||||||
|
"tls1": ssl.PROTOCOL_TLSv1,
|
||||||
|
})
|
||||||
|
TLS_MAP[TLS_COMPAT] = TLS_MAP[TLS_NO_SSL].copy()
|
||||||
|
TLS_MAP[TLS_COMPAT].update({
|
||||||
|
"ssl23": ssl.PROTOCOL_SSLv23,
|
||||||
|
None: ssl.PROTOCOL_SSLv23,
|
||||||
|
})
|
||||||
|
if hasattr(ssl, "PROTOCOL_SSLv3"): # Might not be available.
|
||||||
|
TLS_MAP[TLS_COMPAT].update({
|
||||||
|
"ssl3": ssl.PROTOCOL_SSLv3
|
||||||
|
})
|
||||||
|
|
||||||
if self.ca_certs is not None:
|
if self.ca_certs is not None:
|
||||||
cert_reqs = ssl.CERT_REQUIRED
|
cert_reqs = ssl.CERT_REQUIRED
|
||||||
else:
|
else:
|
||||||
cert_reqs = ssl.CERT_NONE
|
cert_reqs = ssl.CERT_NONE
|
||||||
|
|
||||||
if self.ssl_version == "tls1":
|
if self.tls_level not in TLS_MAP:
|
||||||
ssl_version = ssl.PROTOCOL_TLSv1
|
raise RuntimeError("unknown tls_level: %s" % self.tls_level)
|
||||||
elif self.ssl_version == "ssl2":
|
|
||||||
ssl_version = ssl.PROTOCOL_SSLv2
|
if self.ssl_version not in TLS_MAP[self.tls_level]:
|
||||||
elif self.ssl_version == "ssl3":
|
raise socket.sslerror("Invalid SSL version '%s' requested for tls_version '%s'" % (self.ssl_version, self.tls_level))
|
||||||
ssl_version = ssl.PROTOCOL_SSLv3
|
|
||||||
elif self.ssl_version == "ssl23" or self.ssl_version is None:
|
ssl_version = TLS_MAP[self.tls_level][self.ssl_version]
|
||||||
ssl_version = ssl.PROTOCOL_SSLv23
|
|
||||||
else:
|
|
||||||
raise socket.sslerror("Invalid SSL version requested: %s", self.ssl_version)
|
|
||||||
|
|
||||||
self.sock = ssl.wrap_socket(self.sock, self.keyfile, self.certfile, ca_certs=self.ca_certs, cert_reqs=cert_reqs, ssl_version=ssl_version)
|
self.sock = ssl.wrap_socket(self.sock, self.keyfile, self.certfile, ca_certs=self.ca_certs, cert_reqs=cert_reqs, ssl_version=ssl_version)
|
||||||
ssl_exc = ssl.SSLError
|
ssl_exc = ssl.SSLError
|
||||||
self.read_fd = self.sock.fileno()
|
self.read_fd = self.sock.fileno()
|
||||||
except ImportError:
|
except ImportError:
|
||||||
# No ssl module, and socket.ssl has no fileno(), and does not allow certificate verification
|
# No ssl module, and socket.ssl has no fileno(), and does not allow certificate verification
|
||||||
raise socket.sslerror("imaplib2 SSL mode does not work without ssl module")
|
raise socket.sslerror("imaplib SSL mode does not work without ssl module")
|
||||||
|
|
||||||
if self.cert_verify_cb is not None:
|
if self.cert_verify_cb is not None:
|
||||||
cert_err = self.cert_verify_cb(self.sock.getpeercert(), self.host)
|
cert_err = self.cert_verify_cb(self.sock.getpeercert(), self.host)
|
||||||
if cert_err:
|
if cert_err:
|
||||||
raise ssl_exc(cert_err)
|
raise ssl_exc(cert_err)
|
||||||
|
|
||||||
|
# Allow sending of keep-alive messages - seems to prevent some servers
|
||||||
|
# from closing SSL, leading to deadlocks.
|
||||||
|
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def start_compressing(self):
|
def start_compressing(self):
|
||||||
@ -534,16 +569,23 @@ class IMAP4(object):
|
|||||||
data += self.compressor.flush(zlib.Z_SYNC_FLUSH)
|
data += self.compressor.flush(zlib.Z_SYNC_FLUSH)
|
||||||
|
|
||||||
if bytes != str:
|
if bytes != str:
|
||||||
self.sock.sendall(bytes(data, 'utf8'))
|
data = bytes(data, 'ASCII')
|
||||||
else:
|
|
||||||
self.sock.sendall(data)
|
self.sock.sendall(data)
|
||||||
|
|
||||||
|
|
||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
"""shutdown()
|
"""shutdown()
|
||||||
Close I/O established in "open"."""
|
Close I/O established in "open"."""
|
||||||
|
|
||||||
self.sock.close()
|
try:
|
||||||
|
self.sock.shutdown(socket.SHUT_RDWR)
|
||||||
|
except Exception as e:
|
||||||
|
# The server might already have closed the connection
|
||||||
|
if e.errno != errno.ENOTCONN:
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
self.sock.close()
|
||||||
|
|
||||||
|
|
||||||
def socket(self):
|
def socket(self):
|
||||||
@ -881,7 +923,9 @@ class IMAP4(object):
|
|||||||
def _CRAM_MD5_AUTH(self, challenge):
|
def _CRAM_MD5_AUTH(self, challenge):
|
||||||
"""Authobject to use with CRAM-MD5 authentication."""
|
"""Authobject to use with CRAM-MD5 authentication."""
|
||||||
import hmac
|
import hmac
|
||||||
return self.user + " " + hmac.HMAC(self.password, challenge).hexdigest()
|
pwd = (self.password.encode('ASCII') if isinstance(self.password, str)
|
||||||
|
else self.password)
|
||||||
|
return self.user + " " + hmac.HMAC(pwd, challenge, 'md5').hexdigest()
|
||||||
|
|
||||||
|
|
||||||
def logout(self, **kw):
|
def logout(self, **kw):
|
||||||
@ -1065,8 +1109,8 @@ class IMAP4(object):
|
|||||||
return self._simple_command(name, sort_criteria, charset, *search_criteria, **kw)
|
return self._simple_command(name, sort_criteria, charset, *search_criteria, **kw)
|
||||||
|
|
||||||
|
|
||||||
def starttls(self, keyfile=None, certfile=None, ca_certs=None, cert_verify_cb=None, ssl_version="ssl23", **kw):
|
def starttls(self, keyfile=None, certfile=None, ca_certs=None, cert_verify_cb=None, ssl_version="ssl23", tls_level=TLS_COMPAT, **kw):
|
||||||
"""(typ, [data]) = starttls(keyfile=None, certfile=None, ca_certs=None, cert_verify_cb=None, ssl_version="ssl23")
|
"""(typ, [data]) = starttls(keyfile=None, certfile=None, ca_certs=None, cert_verify_cb=None, ssl_version="ssl23", tls_level="tls_compat")
|
||||||
Start TLS negotiation as per RFC 2595."""
|
Start TLS negotiation as per RFC 2595."""
|
||||||
|
|
||||||
name = 'STARTTLS'
|
name = 'STARTTLS'
|
||||||
@ -1074,7 +1118,7 @@ class IMAP4(object):
|
|||||||
if name not in self.capabilities:
|
if name not in self.capabilities:
|
||||||
raise self.abort('TLS not supported by server')
|
raise self.abort('TLS not supported by server')
|
||||||
|
|
||||||
if hasattr(self, '_tls_established') and self._tls_established:
|
if self._tls_established:
|
||||||
raise self.abort('TLS session already established')
|
raise self.abort('TLS session already established')
|
||||||
|
|
||||||
# Must now shutdown reader thread after next response, and restart after changing read_fd
|
# Must now shutdown reader thread after next response, and restart after changing read_fd
|
||||||
@ -1102,6 +1146,7 @@ class IMAP4(object):
|
|||||||
self.ca_certs = ca_certs
|
self.ca_certs = ca_certs
|
||||||
self.cert_verify_cb = cert_verify_cb
|
self.cert_verify_cb = cert_verify_cb
|
||||||
self.ssl_version = ssl_version
|
self.ssl_version = ssl_version
|
||||||
|
self.tls_level = tls_level
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.ssl_wrap_socket()
|
self.ssl_wrap_socket()
|
||||||
@ -1229,14 +1274,17 @@ class IMAP4(object):
|
|||||||
|
|
||||||
self.commands_lock.release()
|
self.commands_lock.release()
|
||||||
|
|
||||||
if __debug__: self._log(5, 'untagged_responses[%s] %s += ["%s"]' % (typ, len(urd)-1, dat))
|
if __debug__: self._log(5, 'untagged_responses[%s] %s += ["%.80s"]' % (typ, len(urd)-1, dat))
|
||||||
|
|
||||||
|
|
||||||
def _check_bye(self):
|
def _check_bye(self):
|
||||||
|
|
||||||
bye = self._get_untagged_response('BYE', leave=True)
|
bye = self._get_untagged_response('BYE', leave=True)
|
||||||
if bye:
|
if bye:
|
||||||
raise self.abort(bye[-1])
|
if str != bytes:
|
||||||
|
raise self.abort(bye[-1].decode('ASCII', 'replace'))
|
||||||
|
else:
|
||||||
|
raise self.abort(bye[-1])
|
||||||
|
|
||||||
|
|
||||||
def _checkquote(self, arg):
|
def _checkquote(self, arg):
|
||||||
@ -1297,13 +1345,13 @@ class IMAP4(object):
|
|||||||
self.commands_lock.release()
|
self.commands_lock.release()
|
||||||
if need_event:
|
if need_event:
|
||||||
if __debug__: self._log(3, 'sync command %s waiting for empty commands Q' % name)
|
if __debug__: self._log(3, 'sync command %s waiting for empty commands Q' % name)
|
||||||
self.state_change_free.wait()
|
self.state_change_free.wait(threading.TIMEOUT_MAX)
|
||||||
if __debug__: self._log(3, 'sync command %s proceeding' % name)
|
if __debug__: self._log(3, 'sync command %s proceeding' % name)
|
||||||
|
|
||||||
if self.state not in Commands[name][CMD_VAL_STATES]:
|
if self.state not in Commands[name][CMD_VAL_STATES]:
|
||||||
self.literal = None
|
self.literal = None
|
||||||
raise self.error('command %s illegal in state %s'
|
raise self.error('command %s illegal in state %s, only allowed in states %s'
|
||||||
% (name, self.state))
|
% (name, self.state, ', '.join(Commands[name][CMD_VAL_STATES])))
|
||||||
|
|
||||||
self._check_bye()
|
self._check_bye()
|
||||||
|
|
||||||
@ -1316,7 +1364,7 @@ class IMAP4(object):
|
|||||||
while self._get_untagged_response(typ):
|
while self._get_untagged_response(typ):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if self._get_untagged_response('READ-ONLY', leave=True) and not self.is_readonly:
|
if not self.is_readonly and self._get_untagged_response('READ-ONLY', leave=True):
|
||||||
self.literal = None
|
self.literal = None
|
||||||
raise self.readonly('mailbox status changed to READ-ONLY')
|
raise self.readonly('mailbox status changed to READ-ONLY')
|
||||||
|
|
||||||
@ -1348,7 +1396,7 @@ class IMAP4(object):
|
|||||||
return rqb
|
return rqb
|
||||||
|
|
||||||
# Must setup continuation expectancy *before* ouq.put
|
# Must setup continuation expectancy *before* ouq.put
|
||||||
crqb = self._request_push(tag='continuation')
|
crqb = self._request_push(name=name, tag='continuation')
|
||||||
|
|
||||||
self.ouq.put(rqb)
|
self.ouq.put(rqb)
|
||||||
|
|
||||||
@ -1373,7 +1421,7 @@ class IMAP4(object):
|
|||||||
|
|
||||||
if literator is not None:
|
if literator is not None:
|
||||||
# Need new request for next continuation response
|
# Need new request for next continuation response
|
||||||
crqb = self._request_push(tag='continuation')
|
crqb = self._request_push(name=name, tag='continuation')
|
||||||
|
|
||||||
if __debug__: self._log(4, 'write literal size %s' % len(literal))
|
if __debug__: self._log(4, 'write literal size %s' % len(literal))
|
||||||
crqb.data = '%s%s' % (literal, CRLF)
|
crqb.data = '%s%s' % (literal, CRLF)
|
||||||
@ -1402,7 +1450,7 @@ class IMAP4(object):
|
|||||||
def _command_completer(self, cb_arg_list):
|
def _command_completer(self, cb_arg_list):
|
||||||
|
|
||||||
# Called for callback commands
|
# Called for callback commands
|
||||||
(response, cb_arg, error) = cb_arg_list
|
response, cb_arg, error = cb_arg_list
|
||||||
rqb, kw = cb_arg
|
rqb, kw = cb_arg
|
||||||
rqb.callback = kw['callback']
|
rqb.callback = kw['callback']
|
||||||
rqb.callback_arg = kw.get('cb_arg')
|
rqb.callback_arg = kw.get('cb_arg')
|
||||||
@ -1413,13 +1461,17 @@ class IMAP4(object):
|
|||||||
return
|
return
|
||||||
bye = self._get_untagged_response('BYE', leave=True)
|
bye = self._get_untagged_response('BYE', leave=True)
|
||||||
if bye:
|
if bye:
|
||||||
rqb.abort(self.abort, bye[-1])
|
if str != bytes:
|
||||||
|
rqb.abort(self.abort, bye[-1].decode('ASCII', 'replace'))
|
||||||
|
else:
|
||||||
|
rqb.abort(self.abort, bye[-1])
|
||||||
return
|
return
|
||||||
typ, dat = response
|
typ, dat = response
|
||||||
if typ == 'BAD':
|
if typ == 'BAD':
|
||||||
if __debug__: self._print_log()
|
if __debug__: self._print_log()
|
||||||
rqb.abort(self.error, '%s command error: %s %s. Data: %.100s' % (rqb.name, typ, dat, rqb.data))
|
rqb.abort(self.error, '%s command error: %s %s. Data: %.100s' % (rqb.name, typ, dat, rqb.data))
|
||||||
return
|
return
|
||||||
|
if __debug__: self._log(4, '_command_completer(%s, %s, None) = %s' % (response, cb_arg, rqb.tag))
|
||||||
if 'untagged_response' in kw:
|
if 'untagged_response' in kw:
|
||||||
response = self._untagged_response(typ, dat, kw['untagged_response'])
|
response = self._untagged_response(typ, dat, kw['untagged_response'])
|
||||||
rqb.deliver(response)
|
rqb.deliver(response)
|
||||||
@ -1463,7 +1515,7 @@ class IMAP4(object):
|
|||||||
if not leave:
|
if not leave:
|
||||||
del self.untagged_responses[i]
|
del self.untagged_responses[i]
|
||||||
self.commands_lock.release()
|
self.commands_lock.release()
|
||||||
if __debug__: self._log(5, '_get_untagged_response(%s) => %s' % (name, dat))
|
if __debug__: self._log(5, '_get_untagged_response(%s) => %.80s' % (name, dat))
|
||||||
return dat
|
return dat
|
||||||
|
|
||||||
self.commands_lock.release()
|
self.commands_lock.release()
|
||||||
@ -1605,11 +1657,17 @@ class IMAP4(object):
|
|||||||
self.commands_lock.acquire()
|
self.commands_lock.acquire()
|
||||||
rqb = self.tagged_commands.pop(name)
|
rqb = self.tagged_commands.pop(name)
|
||||||
if not self.tagged_commands:
|
if not self.tagged_commands:
|
||||||
|
need_event = True
|
||||||
|
else:
|
||||||
|
need_event = False
|
||||||
|
self.commands_lock.release()
|
||||||
|
|
||||||
|
if __debug__: self._log(4, '_request_pop(%s, %s) [%d] = %s' % (name, data, len(self.tagged_commands), rqb.tag))
|
||||||
|
rqb.deliver(data)
|
||||||
|
|
||||||
|
if need_event:
|
||||||
if __debug__: self._log(3, 'state_change_free.set')
|
if __debug__: self._log(3, 'state_change_free.set')
|
||||||
self.state_change_free.set()
|
self.state_change_free.set()
|
||||||
self.commands_lock.release()
|
|
||||||
if __debug__: self._log(4, '_request_pop(%s, %s) = %s' % (name, data, rqb.tag))
|
|
||||||
rqb.deliver(data)
|
|
||||||
|
|
||||||
|
|
||||||
def _request_push(self, tag=None, name=None, **kw):
|
def _request_push(self, tag=None, name=None, **kw):
|
||||||
@ -1645,7 +1703,7 @@ class IMAP4(object):
|
|||||||
if not dat:
|
if not dat:
|
||||||
break
|
break
|
||||||
data += dat
|
data += dat
|
||||||
if __debug__: self._log(4, '_untagged_response(%s, ?, %s) => %s' % (typ, name, data))
|
if __debug__: self._log(4, '_untagged_response(%s, ?, %s) => %.80s' % (typ, name, data))
|
||||||
return typ, data
|
return typ, data
|
||||||
|
|
||||||
|
|
||||||
@ -1762,7 +1820,10 @@ class IMAP4(object):
|
|||||||
}
|
}
|
||||||
return ' '.join([PollErrors[s] for s in PollErrors.keys() if (s & state)])
|
return ' '.join([PollErrors[s] for s in PollErrors.keys() if (s & state)])
|
||||||
|
|
||||||
line_part = ''
|
if bytes != str:
|
||||||
|
line_part = b''
|
||||||
|
else:
|
||||||
|
line_part = ''
|
||||||
|
|
||||||
poll = select.poll()
|
poll = select.poll()
|
||||||
|
|
||||||
@ -1774,7 +1835,7 @@ class IMAP4(object):
|
|||||||
|
|
||||||
while not (terminate or self.Terminate):
|
while not (terminate or self.Terminate):
|
||||||
if self.state == LOGOUT:
|
if self.state == LOGOUT:
|
||||||
timeout = 1
|
timeout = 10
|
||||||
else:
|
else:
|
||||||
timeout = read_poll_timeout
|
timeout = read_poll_timeout
|
||||||
try:
|
try:
|
||||||
@ -1802,11 +1863,11 @@ class IMAP4(object):
|
|||||||
if bytes != str:
|
if bytes != str:
|
||||||
stop = data.find(b'\n', start)
|
stop = data.find(b'\n', start)
|
||||||
if stop < 0:
|
if stop < 0:
|
||||||
line_part += data[start:].decode()
|
line_part += data[start:]
|
||||||
break
|
break
|
||||||
stop += 1
|
stop += 1
|
||||||
line_part, start, line = \
|
line_part, start, line = \
|
||||||
'', stop, line_part + data[start:stop].decode()
|
b'', stop, (line_part + data[start:stop]).decode(errors='ignore')
|
||||||
else:
|
else:
|
||||||
stop = data.find('\n', start)
|
stop = data.find('\n', start)
|
||||||
if stop < 0:
|
if stop < 0:
|
||||||
@ -1846,7 +1907,10 @@ class IMAP4(object):
|
|||||||
|
|
||||||
if __debug__: self._log(1, 'starting using select')
|
if __debug__: self._log(1, 'starting using select')
|
||||||
|
|
||||||
line_part = ''
|
if bytes != str:
|
||||||
|
line_part = b''
|
||||||
|
else:
|
||||||
|
line_part = ''
|
||||||
|
|
||||||
rxzero = 0
|
rxzero = 0
|
||||||
terminate = False
|
terminate = False
|
||||||
@ -1878,11 +1942,11 @@ class IMAP4(object):
|
|||||||
if bytes != str:
|
if bytes != str:
|
||||||
stop = data.find(b'\n', start)
|
stop = data.find(b'\n', start)
|
||||||
if stop < 0:
|
if stop < 0:
|
||||||
line_part += data[start:].decode()
|
line_part += data[start:]
|
||||||
break
|
break
|
||||||
stop += 1
|
stop += 1
|
||||||
line_part, start, line = \
|
line_part, start, line = \
|
||||||
'', stop, line_part + data[start:stop].decode()
|
b'', stop, (line_part + data[start:stop]).decode(errors='ignore')
|
||||||
else:
|
else:
|
||||||
stop = data.find('\n', start)
|
stop = data.find('\n', start)
|
||||||
if stop < 0:
|
if stop < 0:
|
||||||
@ -2035,7 +2099,7 @@ class IMAP4_SSL(IMAP4):
|
|||||||
"""IMAP4 client class over SSL connection
|
"""IMAP4 client class over SSL connection
|
||||||
|
|
||||||
Instantiate with:
|
Instantiate with:
|
||||||
IMAP4_SSL(host=None, port=None, keyfile=None, certfile=None, ca_certs=None, cert_verify_cb=None, ssl_version="ssl23", debug=None, debug_file=None, identifier=None, timeout=None)
|
IMAP4_SSL(host=None, port=None, keyfile=None, certfile=None, ca_certs=None, cert_verify_cb=None, ssl_version="ssl23", debug=None, debug_file=None, identifier=None, timeout=None, debug_buf_lvl=None, tls_level="tls_compat")
|
||||||
|
|
||||||
host - host's name (default: localhost);
|
host - host's name (default: localhost);
|
||||||
port - port number (default: standard IMAP4 SSL port);
|
port - port number (default: standard IMAP4 SSL port);
|
||||||
@ -2043,23 +2107,30 @@ class IMAP4_SSL(IMAP4):
|
|||||||
certfile - PEM formatted certificate chain file (default: None);
|
certfile - PEM formatted certificate chain file (default: None);
|
||||||
ca_certs - PEM formatted certificate chain file used to validate server certificates (default: None);
|
ca_certs - PEM formatted certificate chain file used to validate server certificates (default: None);
|
||||||
cert_verify_cb - function to verify authenticity of server certificates (default: None);
|
cert_verify_cb - function to verify authenticity of server certificates (default: None);
|
||||||
ssl_version - SSL version to use (default: "ssl23", choose from: "tls1","ssl2","ssl3","ssl23");
|
ssl_version - SSL version to use (default: "ssl23", choose from: "tls1","ssl3","ssl23");
|
||||||
debug - debug level (default: 0 - no debug);
|
debug - debug level (default: 0 - no debug);
|
||||||
debug_file - debug stream (default: sys.stderr);
|
debug_file - debug stream (default: sys.stderr);
|
||||||
identifier - thread identifier prefix (default: host);
|
identifier - thread identifier prefix (default: host);
|
||||||
timeout - timeout in seconds when expecting a command response.
|
timeout - timeout in seconds when expecting a command response.
|
||||||
debug_buf_lvl - debug level at which buffering is turned off.
|
debug_buf_lvl - debug level at which buffering is turned off.
|
||||||
|
tls_level - TLS security level (default: "tls_compat").
|
||||||
|
|
||||||
|
The recognized values for tls_level are:
|
||||||
|
tls_secure: accept only TLS protocols recognized as "secure"
|
||||||
|
tls_no_ssl: disable SSLv2 and SSLv3 support
|
||||||
|
tls_compat: accept all SSL/TLS versions
|
||||||
|
|
||||||
For more documentation see the docstring of the parent class IMAP4.
|
For more documentation see the docstring of the parent class IMAP4.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def __init__(self, host=None, port=None, keyfile=None, certfile=None, ca_certs=None, cert_verify_cb=None, ssl_version="ssl23", debug=None, debug_file=None, identifier=None, timeout=None, debug_buf_lvl=None):
|
def __init__(self, host=None, port=None, keyfile=None, certfile=None, ca_certs=None, cert_verify_cb=None, ssl_version="ssl23", debug=None, debug_file=None, identifier=None, timeout=None, debug_buf_lvl=None, tls_level=TLS_COMPAT):
|
||||||
self.keyfile = keyfile
|
self.keyfile = keyfile
|
||||||
self.certfile = certfile
|
self.certfile = certfile
|
||||||
self.ca_certs = ca_certs
|
self.ca_certs = ca_certs
|
||||||
self.cert_verify_cb = cert_verify_cb
|
self.cert_verify_cb = cert_verify_cb
|
||||||
self.ssl_version = ssl_version
|
self.ssl_version = ssl_version
|
||||||
|
self.tls_level = tls_level
|
||||||
IMAP4.__init__(self, host, port, debug, debug_file, identifier, timeout, debug_buf_lvl)
|
IMAP4.__init__(self, host, port, debug, debug_file, identifier, timeout, debug_buf_lvl)
|
||||||
|
|
||||||
|
|
||||||
@ -2100,27 +2171,18 @@ class IMAP4_SSL(IMAP4):
|
|||||||
data += self.compressor.flush(zlib.Z_SYNC_FLUSH)
|
data += self.compressor.flush(zlib.Z_SYNC_FLUSH)
|
||||||
|
|
||||||
if bytes != str:
|
if bytes != str:
|
||||||
if hasattr(self.sock, "sendall"):
|
data = bytes(data, 'utf8')
|
||||||
self.sock.sendall(bytes(data, 'utf8'))
|
|
||||||
else:
|
if hasattr(self.sock, "sendall"):
|
||||||
dlen = len(data)
|
self.sock.sendall(data)
|
||||||
while dlen > 0:
|
|
||||||
sent = self.sock.write(bytes(data, 'utf8'))
|
|
||||||
if sent == dlen:
|
|
||||||
break # avoid copy
|
|
||||||
data = data[sent:]
|
|
||||||
dlen = dlen - sent
|
|
||||||
else:
|
else:
|
||||||
if hasattr(self.sock, "sendall"):
|
dlen = len(data)
|
||||||
self.sock.sendall(data)
|
while dlen > 0:
|
||||||
else:
|
sent = self.sock.write(data)
|
||||||
dlen = len(data)
|
if sent == dlen:
|
||||||
while dlen > 0:
|
break # avoid copy
|
||||||
sent = self.sock.write(data)
|
data = data[sent:]
|
||||||
if sent == dlen:
|
dlen = dlen - sent
|
||||||
break # avoid copy
|
|
||||||
data = data[sent:]
|
|
||||||
dlen = dlen - sent
|
|
||||||
|
|
||||||
|
|
||||||
def ssl(self):
|
def ssl(self):
|
||||||
@ -2195,9 +2257,9 @@ class IMAP4_stream(IMAP4):
|
|||||||
data += self.compressor.flush(zlib.Z_SYNC_FLUSH)
|
data += self.compressor.flush(zlib.Z_SYNC_FLUSH)
|
||||||
|
|
||||||
if bytes != str:
|
if bytes != str:
|
||||||
self.writefile.write(bytes(data, 'utf8'))
|
data = bytes(data, 'utf8')
|
||||||
else:
|
|
||||||
self.writefile.write(data)
|
self.writefile.write(data)
|
||||||
self.writefile.flush()
|
self.writefile.flush()
|
||||||
|
|
||||||
|
|
||||||
@ -2372,8 +2434,14 @@ if __name__ == '__main__':
|
|||||||
|
|
||||||
# To test: invoke either as 'python imaplib2.py [IMAP4_server_hostname]',
|
# To test: invoke either as 'python imaplib2.py [IMAP4_server_hostname]',
|
||||||
# or as 'python imaplib2.py -s "rsh IMAP4_server_hostname exec /etc/rimapd"'
|
# or as 'python imaplib2.py -s "rsh IMAP4_server_hostname exec /etc/rimapd"'
|
||||||
# or as 'python imaplib2.py -l "keyfile[:certfile]" [IMAP4_SSL_server_hostname]'
|
# or as 'python imaplib2.py -l keyfile[:certfile]|: [IMAP4_SSL_server_hostname]'
|
||||||
|
#
|
||||||
|
# Option "-d <level>" turns on debugging (use "-d 5" for everything)
|
||||||
# Option "-i" tests that IDLE is interruptible
|
# Option "-i" tests that IDLE is interruptible
|
||||||
|
# Option "-p <port>" allows alternate ports
|
||||||
|
|
||||||
|
if not __debug__:
|
||||||
|
raise ValueError('Please run without -O')
|
||||||
|
|
||||||
import getopt, getpass
|
import getopt, getpass
|
||||||
|
|
||||||
@ -2446,10 +2514,10 @@ if __name__ == '__main__':
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
AsyncError = None
|
AsyncError, M = None, None
|
||||||
|
|
||||||
def responder(cb_arg_list):
|
def responder(cb_arg_list):
|
||||||
(response, cb_arg, error) = cb_arg_list
|
response, cb_arg, error = cb_arg_list
|
||||||
global AsyncError
|
global AsyncError
|
||||||
cmd, args = cb_arg
|
cmd, args = cb_arg
|
||||||
if error is not None:
|
if error is not None:
|
||||||
@ -2491,7 +2559,7 @@ if __name__ == '__main__':
|
|||||||
if keyfile is not None:
|
if keyfile is not None:
|
||||||
if not keyfile: keyfile = None
|
if not keyfile: keyfile = None
|
||||||
if not certfile: certfile = None
|
if not certfile: certfile = None
|
||||||
M = IMAP4_SSL(host=host, port=port, keyfile=keyfile, certfile=certfile, debug=debug, identifier='', timeout=10, debug_buf_lvl=debug_buf_lvl)
|
M = IMAP4_SSL(host=host, port=port, keyfile=keyfile, certfile=certfile, ssl_version="tls1", debug=debug, identifier='', timeout=10, debug_buf_lvl=debug_buf_lvl, tls_level="tls_no_ssl")
|
||||||
elif stream_command:
|
elif stream_command:
|
||||||
M = IMAP4_stream(stream_command, debug=debug, identifier='', timeout=10, debug_buf_lvl=debug_buf_lvl)
|
M = IMAP4_stream(stream_command, debug=debug, identifier='', timeout=10, debug_buf_lvl=debug_buf_lvl)
|
||||||
else:
|
else:
|
||||||
@ -2569,7 +2637,7 @@ if __name__ == '__main__':
|
|||||||
print('All tests OK.')
|
print('All tests OK.')
|
||||||
|
|
||||||
except:
|
except:
|
||||||
if not idle_intr or not 'IDLE' in M.capabilities:
|
if not idle_intr or M is None or not 'IDLE' in M.capabilities:
|
||||||
print('Tests failed.')
|
print('Tests failed.')
|
||||||
|
|
||||||
if not debug:
|
if not debug:
|
||||||
|
@ -74,7 +74,7 @@ class UsefulIMAPMixIn(object):
|
|||||||
"""open_socket()
|
"""open_socket()
|
||||||
Open socket choosing first address family available."""
|
Open socket choosing first address family available."""
|
||||||
msg = (-1, 'could not open socket')
|
msg = (-1, 'could not open socket')
|
||||||
for res in socket.getaddrinfo(self.host, self.port, socket.AF_UNSPEC, socket.SOCK_STREAM):
|
for res in socket.getaddrinfo(self.host, self.port, self.af, socket.SOCK_STREAM):
|
||||||
af, socktype, proto, canonname, sa = res
|
af, socktype, proto, canonname, sa = res
|
||||||
try:
|
try:
|
||||||
# use socket of our own, possiblly socksified socket.
|
# use socket of our own, possiblly socksified socket.
|
||||||
@ -175,6 +175,9 @@ 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):
|
||||||
|
if "af" in kwargs:
|
||||||
|
self.af = kwargs['af']
|
||||||
|
del kwargs['af']
|
||||||
if "use_socket" in kwargs:
|
if "use_socket" in kwargs:
|
||||||
self.socket = kwargs['use_socket']
|
self.socket = kwargs['use_socket']
|
||||||
del kwargs['use_socket']
|
del kwargs['use_socket']
|
||||||
@ -209,6 +212,9 @@ class WrappedIMAP4(UsefulIMAPMixIn, IMAP4):
|
|||||||
"""Improved version of imaplib.IMAP4 overriding select()."""
|
"""Improved version of imaplib.IMAP4 overriding select()."""
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
if "af" in kwargs:
|
||||||
|
self.af = kwargs['af']
|
||||||
|
del kwargs['af']
|
||||||
if "use_socket" in kwargs:
|
if "use_socket" in kwargs:
|
||||||
self.socket = kwargs['use_socket']
|
self.socket = kwargs['use_socket']
|
||||||
del kwargs['use_socket']
|
del kwargs['use_socket']
|
||||||
|
@ -19,6 +19,11 @@ from threading import Lock, BoundedSemaphore, Thread, Event, currentThread
|
|||||||
import hmac
|
import hmac
|
||||||
import socket
|
import socket
|
||||||
import base64
|
import base64
|
||||||
|
|
||||||
|
import json
|
||||||
|
import urllib
|
||||||
|
|
||||||
|
import socket
|
||||||
import time
|
import time
|
||||||
import errno
|
import errno
|
||||||
from sys import exc_info
|
from sys import exc_info
|
||||||
@ -76,6 +81,13 @@ class IMAPServer:
|
|||||||
self.goodpassword = None
|
self.goodpassword = None
|
||||||
|
|
||||||
self.usessl = repos.getssl()
|
self.usessl = repos.getssl()
|
||||||
|
self.useipv6 = repos.getipv6()
|
||||||
|
if self.useipv6 == True:
|
||||||
|
self.af = socket.AF_INET6
|
||||||
|
elif self.useipv6 == False:
|
||||||
|
self.af = socket.AF_INET
|
||||||
|
else:
|
||||||
|
self.af = socket.AF_UNSPEC
|
||||||
self.hostname = \
|
self.hostname = \
|
||||||
None if self.preauth_tunnel else repos.gethost()
|
None if self.preauth_tunnel else repos.gethost()
|
||||||
self.port = repos.getport()
|
self.port = repos.getport()
|
||||||
@ -88,6 +100,13 @@ class IMAPServer:
|
|||||||
self.__verifycert = None # disable cert verification
|
self.__verifycert = None # disable cert verification
|
||||||
self.fingerprint = repos.get_ssl_fingerprint()
|
self.fingerprint = repos.get_ssl_fingerprint()
|
||||||
self.sslversion = repos.getsslversion()
|
self.sslversion = repos.getsslversion()
|
||||||
|
self.tlslevel = repos.gettlslevel()
|
||||||
|
|
||||||
|
self.oauth2_refresh_token = repos.getoauth2_refresh_token()
|
||||||
|
self.oauth2_access_token = repos.getoauth2_access_token()
|
||||||
|
self.oauth2_client_id = repos.getoauth2_client_id()
|
||||||
|
self.oauth2_client_secret = repos.getoauth2_client_secret()
|
||||||
|
self.oauth2_request_url = repos.getoauth2_request_url()
|
||||||
|
|
||||||
self.delim = None
|
self.delim = None
|
||||||
self.root = None
|
self.root = None
|
||||||
@ -195,11 +214,44 @@ class IMAPServer:
|
|||||||
authz = self.user_identity
|
authz = self.user_identity
|
||||||
NULL = u'\x00'
|
NULL = u'\x00'
|
||||||
retval = NULL.join((authz, authc, passwd)).encode('utf-8')
|
retval = NULL.join((authz, authc, passwd)).encode('utf-8')
|
||||||
self.ui.debug('imap', '__plainhandler: returning %s' % retval)
|
logsafe_retval = NULL.join((authz, authc, "(passwd hidden for log)")).encode('utf-8')
|
||||||
|
self.ui.debug('imap', '__plainhandler: returning %s' % logsafe_retval)
|
||||||
return retval
|
return retval
|
||||||
|
|
||||||
|
|
||||||
# XXX: describe function
|
def __xoauth2handler(self, response):
|
||||||
|
if self.oauth2_refresh_token is None and self.oauth2_access_token is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if self.oauth2_access_token is None:
|
||||||
|
# need to move these to config
|
||||||
|
# generate new access token
|
||||||
|
params = {}
|
||||||
|
params['client_id'] = self.oauth2_client_id
|
||||||
|
params['client_secret'] = self.oauth2_client_secret
|
||||||
|
params['refresh_token'] = self.oauth2_refresh_token
|
||||||
|
params['grant_type'] = 'refresh_token'
|
||||||
|
|
||||||
|
self.ui.debug('imap', 'xoauth2handler: url "%s"' % self.oauth2_request_url)
|
||||||
|
self.ui.debug('imap', 'xoauth2handler: params "%s"' % params)
|
||||||
|
|
||||||
|
original_socket = socket.socket
|
||||||
|
socket.socket = self.proxied_socket
|
||||||
|
try:
|
||||||
|
response = urllib.urlopen(self.oauth2_request_url, urllib.urlencode(params)).read()
|
||||||
|
finally:
|
||||||
|
socket.socket = original_socket
|
||||||
|
|
||||||
|
resp = json.loads(response)
|
||||||
|
self.ui.debug('imap', 'xoauth2handler: response "%s"' % resp)
|
||||||
|
self.oauth2_access_token = resp['access_token']
|
||||||
|
|
||||||
|
self.ui.debug('imap', 'xoauth2handler: access_token "%s"' % self.oauth2_access_token)
|
||||||
|
auth_string = 'user=%s\1auth=Bearer %s\1\1' % (self.username, self.oauth2_access_token)
|
||||||
|
#auth_string = base64.b64encode(auth_string)
|
||||||
|
self.ui.debug('imap', 'xoauth2handler: returning "%s"' % auth_string)
|
||||||
|
return auth_string
|
||||||
|
|
||||||
def __gssauth(self, response):
|
def __gssauth(self, response):
|
||||||
data = base64.b64encode(response)
|
data = base64.b64encode(response)
|
||||||
try:
|
try:
|
||||||
@ -283,6 +335,10 @@ class IMAPServer:
|
|||||||
imapobj.authenticate('PLAIN', self.__plainhandler)
|
imapobj.authenticate('PLAIN', self.__plainhandler)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def __authn_xoauth2(self, imapobj):
|
||||||
|
imapobj.authenticate('XOAUTH2', self.__xoauth2handler)
|
||||||
|
return True
|
||||||
|
|
||||||
def __authn_login(self, imapobj):
|
def __authn_login(self, imapobj):
|
||||||
# Use LOGIN command, unless LOGINDISABLED is advertized
|
# Use LOGIN command, unless LOGINDISABLED is advertized
|
||||||
# (per RFC 2595)
|
# (per RFC 2595)
|
||||||
@ -314,6 +370,7 @@ class IMAPServer:
|
|||||||
auth_methods = {
|
auth_methods = {
|
||||||
"GSSAPI": (self.__authn_gssapi, False, True),
|
"GSSAPI": (self.__authn_gssapi, False, True),
|
||||||
"CRAM-MD5": (self.__authn_cram_md5, True, True),
|
"CRAM-MD5": (self.__authn_cram_md5, True, True),
|
||||||
|
"XOAUTH2": (self.__authn_xoauth2, True, True),
|
||||||
"PLAIN": (self.__authn_plain, True, True),
|
"PLAIN": (self.__authn_plain, True, True),
|
||||||
"LOGIN": (self.__authn_login, True, False),
|
"LOGIN": (self.__authn_login, True, False),
|
||||||
}
|
}
|
||||||
@ -437,6 +494,8 @@ class IMAPServer:
|
|||||||
timeout=socket.getdefaulttimeout(),
|
timeout=socket.getdefaulttimeout(),
|
||||||
fingerprint=self.fingerprint,
|
fingerprint=self.fingerprint,
|
||||||
use_socket=self.proxied_socket,
|
use_socket=self.proxied_socket,
|
||||||
|
tls_level=self.tlslevel,
|
||||||
|
af=self.af,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.ui.connecting(self.hostname, self.port)
|
self.ui.connecting(self.hostname, self.port)
|
||||||
@ -444,6 +503,7 @@ class IMAPServer:
|
|||||||
self.hostname, self.port,
|
self.hostname, self.port,
|
||||||
timeout=socket.getdefaulttimeout(),
|
timeout=socket.getdefaulttimeout(),
|
||||||
use_socket=self.proxied_socket,
|
use_socket=self.proxied_socket,
|
||||||
|
af=self.af,
|
||||||
)
|
)
|
||||||
|
|
||||||
if not self.preauth_tunnel:
|
if not self.preauth_tunnel:
|
||||||
|
@ -25,6 +25,9 @@ from offlineimap.ui import getglobalui
|
|||||||
# Message headers that use space as the separator (for label storage)
|
# Message headers that use space as the separator (for label storage)
|
||||||
SPACE_SEPARATED_LABEL_HEADERS = ('X-Label', 'Keywords')
|
SPACE_SEPARATED_LABEL_HEADERS = ('X-Label', 'Keywords')
|
||||||
|
|
||||||
|
# Find the modified UTF-7 shifts of an international mailbox name.
|
||||||
|
MUTF7_SHIFT_RE = re.compile(r'&[^-]*-|\+')
|
||||||
|
|
||||||
|
|
||||||
def __debug(*args):
|
def __debug(*args):
|
||||||
msg = []
|
msg = []
|
||||||
@ -192,6 +195,14 @@ def flagsimap2maildir(flagstring):
|
|||||||
retval.add(maildirflag)
|
retval.add(maildirflag)
|
||||||
return retval
|
return retval
|
||||||
|
|
||||||
|
def flagsimap2keywords(flagstring):
|
||||||
|
"""Convert string '(\\Draft \\Deleted somekeyword otherkeyword)' into a
|
||||||
|
keyword set (somekeyword otherkeyword)."""
|
||||||
|
|
||||||
|
imapflagset = set(flagstring[1:-1].split())
|
||||||
|
serverflagset = set([flag for (flag, c) in flagmap])
|
||||||
|
return imapflagset - serverflagset
|
||||||
|
|
||||||
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)'."""
|
||||||
|
|
||||||
@ -328,3 +339,28 @@ def labels_from_header(header_name, header_value):
|
|||||||
|
|
||||||
return labels
|
return labels
|
||||||
|
|
||||||
|
|
||||||
|
def decode_mailbox_name(name):
|
||||||
|
"""Decodes a modified UTF-7 mailbox name.
|
||||||
|
|
||||||
|
If the string cannot be decoded, it is returned unmodified.
|
||||||
|
|
||||||
|
See RFC 3501, sec. 5.1.3.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
- name: string, possibly encoded with modified UTF-7
|
||||||
|
|
||||||
|
Returns: decoded UTF-8 string.
|
||||||
|
"""
|
||||||
|
def demodify(m):
|
||||||
|
s = m.group()
|
||||||
|
if s == '+':
|
||||||
|
return '+-'
|
||||||
|
return '+' + s[1:-1].replace(',', '/') + '-'
|
||||||
|
|
||||||
|
ret = MUTF7_SHIFT_RE.sub(demodify, name)
|
||||||
|
|
||||||
|
try:
|
||||||
|
return ret.decode('utf-7').encode('utf-8')
|
||||||
|
except UnicodeEncodeError:
|
||||||
|
return name
|
||||||
|
@ -25,11 +25,15 @@ import logging
|
|||||||
from optparse import OptionParser
|
from optparse import OptionParser
|
||||||
|
|
||||||
import offlineimap
|
import offlineimap
|
||||||
from offlineimap import accounts, threadutil, syncmaster
|
from offlineimap import accounts, threadutil, syncmaster, folder
|
||||||
from offlineimap import globals
|
from offlineimap import globals
|
||||||
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
|
||||||
|
from offlineimap.repository import Repository
|
||||||
|
|
||||||
|
import traceback
|
||||||
|
import collections
|
||||||
|
|
||||||
|
|
||||||
class OfflineImap:
|
class OfflineImap:
|
||||||
@ -47,11 +51,13 @@ class OfflineImap:
|
|||||||
options, args = self.__parse_cmd_options()
|
options, args = self.__parse_cmd_options()
|
||||||
if options.diagnostics:
|
if options.diagnostics:
|
||||||
self.__serverdiagnostics(options)
|
self.__serverdiagnostics(options)
|
||||||
|
elif options.migrate_fmd5:
|
||||||
|
self.__migratefmd5(options)
|
||||||
else:
|
else:
|
||||||
self.__sync(options)
|
return self.__sync(options)
|
||||||
|
|
||||||
def __parse_cmd_options(self):
|
def __parse_cmd_options(self):
|
||||||
parser = OptionParser(version=offlineimap.__bigversion__,
|
parser = OptionParser(version=offlineimap.__version__,
|
||||||
description="%s.\n\n%s" %
|
description="%s.\n\n%s" %
|
||||||
(offlineimap.__copyright__,
|
(offlineimap.__copyright__,
|
||||||
offlineimap.__license__))
|
offlineimap.__license__))
|
||||||
@ -89,6 +95,11 @@ class OfflineImap:
|
|||||||
parser.add_option("-l", dest="logfile", metavar="FILE",
|
parser.add_option("-l", dest="logfile", metavar="FILE",
|
||||||
help="log to FILE")
|
help="log to FILE")
|
||||||
|
|
||||||
|
parser.add_option("-s",
|
||||||
|
action="store_true", dest="syslog",
|
||||||
|
default=False,
|
||||||
|
help="log to syslog")
|
||||||
|
|
||||||
parser.add_option("-f", dest="folders",
|
parser.add_option("-f", dest="folders",
|
||||||
metavar="folder1[,folder2[,...]]",
|
metavar="folder1[,folder2[,...]]",
|
||||||
help="only sync the specified folders")
|
help="only sync the specified folders")
|
||||||
@ -110,7 +121,11 @@ class OfflineImap:
|
|||||||
|
|
||||||
parser.add_option("-u", dest="interface",
|
parser.add_option("-u", dest="interface",
|
||||||
help="specifies an alternative user interface"
|
help="specifies an alternative user interface"
|
||||||
" (quiet, basic, ttyui, blinkenlights, machineui)")
|
" (quiet, basic, syslog, ttyui, blinkenlights, machineui)")
|
||||||
|
|
||||||
|
parser.add_option("--migrate-fmd5-using-nametrans",
|
||||||
|
action="store_true", dest="migrate_fmd5", default=False,
|
||||||
|
help="migrate FMD5 hashes from versions prior to 6.3.5")
|
||||||
|
|
||||||
(options, args) = parser.parse_args()
|
(options, args) = parser.parse_args()
|
||||||
globals.set_options (options)
|
globals.set_options (options)
|
||||||
@ -196,6 +211,10 @@ class OfflineImap:
|
|||||||
if options.logfile:
|
if options.logfile:
|
||||||
self.ui.setlogfile(options.logfile)
|
self.ui.setlogfile(options.logfile)
|
||||||
|
|
||||||
|
#set up syslog
|
||||||
|
if options.syslog:
|
||||||
|
self.ui.setup_sysloghandler()
|
||||||
|
|
||||||
#welcome blurb
|
#welcome blurb
|
||||||
self.ui.init_banner()
|
self.ui.init_banner()
|
||||||
|
|
||||||
@ -217,6 +236,9 @@ class OfflineImap:
|
|||||||
imaplib.Debug = 5
|
imaplib.Debug = 5
|
||||||
|
|
||||||
if options.runonce:
|
if options.runonce:
|
||||||
|
# Must kill the possible default option
|
||||||
|
if config.has_option('DEFAULT', 'autorefresh'):
|
||||||
|
config.remove_option('DEFAULT', 'autorefresh')
|
||||||
# FIXME: spaghetti code alert!
|
# 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")
|
||||||
@ -260,6 +282,42 @@ class OfflineImap:
|
|||||||
self.config = config
|
self.config = config
|
||||||
return (options, args)
|
return (options, args)
|
||||||
|
|
||||||
|
def __dumpstacks(self, context=1, sighandler_deep=2):
|
||||||
|
""" Signal handler: dump a stack trace for each existing thread."""
|
||||||
|
|
||||||
|
currentThreadId = threading.currentThread().ident
|
||||||
|
|
||||||
|
def unique_count(l):
|
||||||
|
d = collections.defaultdict(lambda: 0)
|
||||||
|
for v in l:
|
||||||
|
d[tuple(v)] += 1
|
||||||
|
return list((k, v) for k, v in d.iteritems())
|
||||||
|
|
||||||
|
stack_displays = []
|
||||||
|
for threadId, stack in sys._current_frames().items():
|
||||||
|
stack_display = []
|
||||||
|
for filename, lineno, name, line in traceback.extract_stack(stack):
|
||||||
|
stack_display.append(' File: "%s", line %d, in %s'
|
||||||
|
% (filename, lineno, name))
|
||||||
|
if line:
|
||||||
|
stack_display.append(" %s" % (line.strip()))
|
||||||
|
if currentThreadId == threadId:
|
||||||
|
stack_display = stack_display[:- (sighandler_deep * 2)]
|
||||||
|
stack_display.append(' => Stopped to handle current signal. ')
|
||||||
|
stack_displays.append(stack_display)
|
||||||
|
stacks = unique_count(stack_displays)
|
||||||
|
self.ui.debug('thread', "** Thread List:\n")
|
||||||
|
for stack, times in stacks:
|
||||||
|
if times == 1:
|
||||||
|
msg = "%s Thread is at:\n%s\n"
|
||||||
|
else:
|
||||||
|
msg = "%s Threads are at:\n%s\n"
|
||||||
|
self.ui.debug('thread', msg % (times, '\n'.join(stack[- (context * 2):])))
|
||||||
|
|
||||||
|
self.ui.debug('thread', "Dumped a total of %d Threads." %
|
||||||
|
len(sys._current_frames().keys()))
|
||||||
|
|
||||||
|
|
||||||
def __sync(self, options):
|
def __sync(self, options):
|
||||||
"""Invoke the correct single/multithread syncing
|
"""Invoke the correct single/multithread syncing
|
||||||
|
|
||||||
@ -309,10 +367,19 @@ class OfflineImap:
|
|||||||
getglobalui().warn("Terminating NOW (this may "\
|
getglobalui().warn("Terminating NOW (this may "\
|
||||||
"take a few seconds)...")
|
"take a few seconds)...")
|
||||||
accounts.Account.set_abort_event(self.config, 3)
|
accounts.Account.set_abort_event(self.config, 3)
|
||||||
|
if 'thread' in self.ui.debuglist:
|
||||||
|
self.__dumpstacks(5)
|
||||||
|
|
||||||
|
# Abort after three Ctrl-C keystrokes
|
||||||
|
self.num_sigterm += 1
|
||||||
|
if self.num_sigterm >= 3:
|
||||||
|
getglobalui().warn("Signaled thrice. Aborting!")
|
||||||
|
sys.exit(1)
|
||||||
elif sig == signal.SIGQUIT:
|
elif sig == signal.SIGQUIT:
|
||||||
stacktrace.dump(sys.stderr)
|
stacktrace.dump(sys.stderr)
|
||||||
os.abort()
|
os.abort()
|
||||||
|
|
||||||
|
self.num_sigterm = 0
|
||||||
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)
|
||||||
@ -339,11 +406,13 @@ class OfflineImap:
|
|||||||
offlineimap.mbnames.write(True)
|
offlineimap.mbnames.write(True)
|
||||||
|
|
||||||
self.ui.terminate()
|
self.ui.terminate()
|
||||||
|
return 0
|
||||||
except (SystemExit):
|
except (SystemExit):
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.ui.error(e)
|
self.ui.error(e)
|
||||||
self.ui.terminate()
|
self.ui.terminate()
|
||||||
|
return 1
|
||||||
|
|
||||||
def __sync_singlethreaded(self, accs):
|
def __sync_singlethreaded(self, accs):
|
||||||
"""Executed if we do not want a separate syncmaster thread
|
"""Executed if we do not want a separate syncmaster thread
|
||||||
@ -365,3 +434,21 @@ class OfflineImap:
|
|||||||
for account in allaccounts:
|
for account in allaccounts:
|
||||||
if account.name not in activeaccounts: continue
|
if account.name not in activeaccounts: continue
|
||||||
account.serverdiagnostics()
|
account.serverdiagnostics()
|
||||||
|
|
||||||
|
def __migratefmd5(self, options):
|
||||||
|
activeaccounts = self.config.get("general", "accounts")
|
||||||
|
if options.accounts:
|
||||||
|
activeaccounts = options.accounts
|
||||||
|
activeaccounts = activeaccounts.replace(" ", "")
|
||||||
|
activeaccounts = activeaccounts.split(",")
|
||||||
|
allaccounts = accounts.AccountListGenerator(self.config)
|
||||||
|
|
||||||
|
for account in allaccounts:
|
||||||
|
if account.name not in activeaccounts:
|
||||||
|
continue
|
||||||
|
localrepo = Repository(account, 'local')
|
||||||
|
if localrepo.getfoldertype() != folder.Maildir.MaildirFolder:
|
||||||
|
continue
|
||||||
|
folders = localrepo.getfolders()
|
||||||
|
for f in folders:
|
||||||
|
f.migratefmd5(options.dryrun)
|
||||||
|
@ -48,6 +48,7 @@ class BaseRepository(CustomConfig.ConfigHelperMixin, object):
|
|||||||
self.folderfilter = lambda foldername: 1
|
self.folderfilter = lambda foldername: 1
|
||||||
self.folderincludes = []
|
self.folderincludes = []
|
||||||
self.foldersort = None
|
self.foldersort = None
|
||||||
|
self.newmail_hook = None
|
||||||
if self.config.has_option(self.getsection(), 'nametrans'):
|
if self.config.has_option(self.getsection(), 'nametrans'):
|
||||||
self.nametrans = self.localeval.eval(
|
self.nametrans = self.localeval.eval(
|
||||||
self.getconf('nametrans'), {'re': re})
|
self.getconf('nametrans'), {'re': re})
|
||||||
@ -132,6 +133,9 @@ class BaseRepository(CustomConfig.ConfigHelperMixin, object):
|
|||||||
def getsep(self):
|
def getsep(self):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def getkeywordmap(self):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
def should_sync_folder(self, fname):
|
def should_sync_folder(self, fname):
|
||||||
"""Should this folder be synced?"""
|
"""Should this folder be synced?"""
|
||||||
|
|
||||||
|
@ -29,6 +29,8 @@ class GmailRepository(IMAPRepository):
|
|||||||
# Gmail IMAP server port
|
# Gmail IMAP server port
|
||||||
PORT = 993
|
PORT = 993
|
||||||
|
|
||||||
|
OAUTH2_URL = 'https://accounts.google.com/o/oauth2/token'
|
||||||
|
|
||||||
def __init__(self, reposname, account):
|
def __init__(self, reposname, account):
|
||||||
"""Initialize a GmailRepository object."""
|
"""Initialize a GmailRepository object."""
|
||||||
# Enforce SSL usage
|
# Enforce SSL usage
|
||||||
@ -49,6 +51,20 @@ class GmailRepository(IMAPRepository):
|
|||||||
self._host = GmailRepository.HOSTNAME
|
self._host = GmailRepository.HOSTNAME
|
||||||
return self._host
|
return self._host
|
||||||
|
|
||||||
|
def getoauth2_request_url(self):
|
||||||
|
"""Return the server name to connect to.
|
||||||
|
|
||||||
|
Gmail implementation first checks for the usual IMAP settings
|
||||||
|
and falls back to imap.gmail.com if not specified."""
|
||||||
|
|
||||||
|
url = super(GmailRepository, self).getoauth2_request_url()
|
||||||
|
if url is None:
|
||||||
|
# Nothing was configured, cache and return hardcoded one.
|
||||||
|
self._oauth2_request_url = GmailRepository.OAUTH2_URL
|
||||||
|
else:
|
||||||
|
self._oauth2_request_url = url
|
||||||
|
return self._oauth2_request_url
|
||||||
|
|
||||||
def getport(self):
|
def getport(self):
|
||||||
return GmailRepository.PORT
|
return GmailRepository.PORT
|
||||||
|
|
||||||
|
@ -34,8 +34,14 @@ class IMAPRepository(BaseRepository):
|
|||||||
BaseRepository.__init__(self, reposname, account)
|
BaseRepository.__init__(self, reposname, account)
|
||||||
# self.ui is being set by the BaseRepository
|
# self.ui is being set by the BaseRepository
|
||||||
self._host = None
|
self._host = None
|
||||||
|
self._oauth2_request_url = None
|
||||||
self.imapserver = imapserver.IMAPServer(self)
|
self.imapserver = imapserver.IMAPServer(self)
|
||||||
self.folders = None
|
self.folders = None
|
||||||
|
# Only set the newmail_hook in an IMAP repository.
|
||||||
|
if self.config.has_option(self.getsection(), 'newmail_hook'):
|
||||||
|
self.newmail_hook = self.localeval.eval(
|
||||||
|
self.getconf('newmail_hook'))
|
||||||
|
|
||||||
if self.getconf('sep', None):
|
if self.getconf('sep', None):
|
||||||
self.ui.info("The 'sep' setting is being ignored for IMAP "
|
self.ui.info("The 'sep' setting is being ignored for IMAP "
|
||||||
"repository '%s' (it's autodetected)"% self)
|
"repository '%s' (it's autodetected)"% self)
|
||||||
@ -125,12 +131,12 @@ class IMAPRepository(BaseRepository):
|
|||||||
return self.getconf('remote_identity', default=None)
|
return self.getconf('remote_identity', default=None)
|
||||||
|
|
||||||
def get_auth_mechanisms(self):
|
def get_auth_mechanisms(self):
|
||||||
supported = ["GSSAPI", "CRAM-MD5", "PLAIN", "LOGIN"]
|
supported = ["GSSAPI", "XOAUTH2", "CRAM-MD5", "PLAIN", "LOGIN"]
|
||||||
# Mechanisms are ranged from the strongest to the
|
# Mechanisms are ranged from the strongest to the
|
||||||
# weakest ones.
|
# weakest ones.
|
||||||
# TODO: we need DIGEST-MD5, it must come before CRAM-MD5
|
# TODO: we need DIGEST-MD5, it must come before CRAM-MD5
|
||||||
# TODO: due to the chosen-plaintext resistance.
|
# TODO: due to the chosen-plaintext resistance.
|
||||||
default = ["GSSAPI", "CRAM-MD5", "PLAIN", "LOGIN"]
|
default = ["GSSAPI", "XOAUTH2", "CRAM-MD5", "PLAIN", "LOGIN"]
|
||||||
|
|
||||||
mechs = self.getconflist('auth_mechanisms', r',\s*',
|
mechs = self.getconflist('auth_mechanisms', r',\s*',
|
||||||
default)
|
default)
|
||||||
@ -188,8 +194,11 @@ class IMAPRepository(BaseRepository):
|
|||||||
|
|
||||||
return self.getconfint('remoteport', None)
|
return self.getconfint('remoteport', None)
|
||||||
|
|
||||||
|
def getipv6(self):
|
||||||
|
return self.getconfboolean('ipv6', None)
|
||||||
|
|
||||||
def getssl(self):
|
def getssl(self):
|
||||||
return self.getconfboolean('ssl', 0)
|
return self.getconfboolean('ssl', 1)
|
||||||
|
|
||||||
def getsslclientcert(self):
|
def getsslclientcert(self):
|
||||||
xforms = [os.path.expanduser, os.path.expandvars, os.path.abspath]
|
xforms = [os.path.expanduser, os.path.expandvars, os.path.abspath]
|
||||||
@ -240,6 +249,9 @@ class IMAPRepository(BaseRepository):
|
|||||||
raise OfflineImapError(reason, OfflineImapError.ERROR.REPO)
|
raise OfflineImapError(reason, OfflineImapError.ERROR.REPO)
|
||||||
return cacertfile
|
return cacertfile
|
||||||
|
|
||||||
|
def gettlslevel(self):
|
||||||
|
return self.getconf('tls_level', 'tls_compat')
|
||||||
|
|
||||||
def getsslversion(self):
|
def getsslversion(self):
|
||||||
return self.getconf('ssl_version', None)
|
return self.getconf('ssl_version', None)
|
||||||
|
|
||||||
@ -252,6 +264,30 @@ class IMAPRepository(BaseRepository):
|
|||||||
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]
|
||||||
|
|
||||||
|
def getoauth2_request_url(self):
|
||||||
|
if self._oauth2_request_url: # Use cached value if possible.
|
||||||
|
return self._oauth2_request_url
|
||||||
|
|
||||||
|
oauth2_request_url = self.getconf('oauth2_request_url', None)
|
||||||
|
if oauth2_request_url != None:
|
||||||
|
self._oauth2_request_url = oauth2_request_url
|
||||||
|
return self._oauth2_request_url
|
||||||
|
|
||||||
|
#raise OfflineImapError("No remote oauth2_request_url for repository "
|
||||||
|
#"'%s' specified."% self, OfflineImapError.ERROR.REPO)
|
||||||
|
|
||||||
|
def getoauth2_refresh_token(self):
|
||||||
|
return self.getconf('oauth2_refresh_token', None)
|
||||||
|
|
||||||
|
def getoauth2_access_token(self):
|
||||||
|
return self.getconf('oauth2_access_token', None)
|
||||||
|
|
||||||
|
def getoauth2_client_id(self):
|
||||||
|
return self.getconf('oauth2_client_id', None)
|
||||||
|
|
||||||
|
def getoauth2_client_secret(self):
|
||||||
|
return self.getconf('oauth2_client_secret', None)
|
||||||
|
|
||||||
def getpreauthtunnel(self):
|
def getpreauthtunnel(self):
|
||||||
return self.getconf('preauthtunnel', None)
|
return self.getconf('preauthtunnel', None)
|
||||||
|
|
||||||
@ -261,6 +297,9 @@ class IMAPRepository(BaseRepository):
|
|||||||
def getreference(self):
|
def getreference(self):
|
||||||
return self.getconf('reference', '')
|
return self.getconf('reference', '')
|
||||||
|
|
||||||
|
def getdecodefoldernames(self):
|
||||||
|
return self.getconfboolean('decodefoldernames', 0)
|
||||||
|
|
||||||
def getidlefolders(self):
|
def getidlefolders(self):
|
||||||
localeval = self.localeval
|
localeval = self.localeval
|
||||||
return localeval.eval(self.getconf('idlefolders', '[]'))
|
return localeval.eval(self.getconf('idlefolders', '[]'))
|
||||||
|
@ -39,6 +39,14 @@ class MaildirRepository(BaseRepository):
|
|||||||
if not os.path.isdir(self.root):
|
if not os.path.isdir(self.root):
|
||||||
os.mkdir(self.root, 0o700)
|
os.mkdir(self.root, 0o700)
|
||||||
|
|
||||||
|
# Create the keyword->char mapping
|
||||||
|
self.keyword2char = dict()
|
||||||
|
for c in 'abcdefghijklmnopqrstuvwxyz':
|
||||||
|
confkey = 'customflag_' + c
|
||||||
|
keyword = self.getconf(confkey, None)
|
||||||
|
if keyword is not None:
|
||||||
|
self.keyword2char[keyword] = c
|
||||||
|
|
||||||
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"""
|
||||||
|
|
||||||
@ -72,6 +80,9 @@ class MaildirRepository(BaseRepository):
|
|||||||
def getsep(self):
|
def getsep(self):
|
||||||
return self.getconf('sep', '.').strip()
|
return self.getconf('sep', '.').strip()
|
||||||
|
|
||||||
|
def getkeywordmap(self):
|
||||||
|
return self.keyword2char if len(self.keyword2char) > 0 else None
|
||||||
|
|
||||||
def makefolder(self, foldername):
|
def makefolder(self, foldername):
|
||||||
"""Create new Maildir folder if necessary
|
"""Create new Maildir folder if necessary
|
||||||
|
|
||||||
|
@ -603,7 +603,7 @@ class Blinkenlights(UIBase, CursesUtil):
|
|||||||
self.bannerwin.clear() # Delete old content (eg before resizes)
|
self.bannerwin.clear() # Delete old content (eg before resizes)
|
||||||
self.bannerwin.bkgd(' ', color) # Fill background with that color
|
self.bannerwin.bkgd(' ', color) # Fill background with that color
|
||||||
string = "%s %s"% (offlineimap.__productname__,
|
string = "%s %s"% (offlineimap.__productname__,
|
||||||
offlineimap.__bigversion__)
|
offlineimap.__version__)
|
||||||
self.bannerwin.addstr(0, 0, string, color)
|
self.bannerwin.addstr(0, 0, string, color)
|
||||||
self.bannerwin.addstr(0, self.width -len(offlineimap.__copyright__) -1,
|
self.bannerwin.addstr(0, self.width -len(offlineimap.__copyright__) -1,
|
||||||
offlineimap.__copyright__, color)
|
offlineimap.__copyright__, color)
|
||||||
|
@ -17,9 +17,10 @@
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
from offlineimap.ui.UIBase import UIBase
|
from offlineimap.ui.UIBase import UIBase
|
||||||
|
import offlineimap
|
||||||
|
|
||||||
class Basic(UIBase):
|
class Basic(UIBase):
|
||||||
"""'Quiet' simply sets log level to INFO"""
|
"""'Basic' simply sets log level to INFO"""
|
||||||
def __init__(self, config, loglevel = logging.INFO):
|
def __init__(self, config, loglevel = logging.INFO):
|
||||||
return super(Basic, self).__init__(config, loglevel)
|
return super(Basic, self).__init__(config, loglevel)
|
||||||
|
|
||||||
@ -27,3 +28,22 @@ class Quiet(UIBase):
|
|||||||
"""'Quiet' simply sets log level to WARNING"""
|
"""'Quiet' simply sets log level to WARNING"""
|
||||||
def __init__(self, config, loglevel = logging.WARNING):
|
def __init__(self, config, loglevel = logging.WARNING):
|
||||||
return super(Quiet, self).__init__(config, loglevel)
|
return super(Quiet, self).__init__(config, loglevel)
|
||||||
|
|
||||||
|
class Syslog(UIBase):
|
||||||
|
"""'Syslog' sets log level to INFO and outputs to syslog instead of stdout"""
|
||||||
|
def __init__(self, config, loglevel = logging.INFO):
|
||||||
|
return super(Syslog, self).__init__(config, loglevel)
|
||||||
|
|
||||||
|
def setup_consolehandler(self):
|
||||||
|
# create syslog handler
|
||||||
|
ch = logging.handlers.SysLogHandler('/dev/log')
|
||||||
|
# create formatter and add it to the handlers
|
||||||
|
self.formatter = logging.Formatter("%(message)s")
|
||||||
|
ch.setFormatter(self.formatter)
|
||||||
|
# add the handlers to the logger
|
||||||
|
self.logger.addHandler(ch)
|
||||||
|
self.logger.info(offlineimap.banner)
|
||||||
|
return ch
|
||||||
|
|
||||||
|
def setup_sysloghandler(self):
|
||||||
|
pass # Do not honor -s (log to syslog) CLI option.
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
# 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 logging
|
import logging
|
||||||
|
import logging.handlers
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
import sys
|
import sys
|
||||||
@ -91,10 +92,22 @@ class UIBase(object):
|
|||||||
self.logger.info(offlineimap.banner)
|
self.logger.info(offlineimap.banner)
|
||||||
return ch
|
return ch
|
||||||
|
|
||||||
|
def setup_sysloghandler(self):
|
||||||
|
"""Backend specific syslog handler."""
|
||||||
|
|
||||||
|
# create syslog handler
|
||||||
|
ch = logging.handlers.SysLogHandler('/dev/log')
|
||||||
|
# create formatter and add it to the handlers
|
||||||
|
self.formatter = logging.Formatter("%(message)s")
|
||||||
|
ch.setFormatter(self.formatter)
|
||||||
|
# add the handlers to the logger
|
||||||
|
self.logger.addHandler(ch)
|
||||||
|
|
||||||
def setlogfile(self, logfile):
|
def setlogfile(self, logfile):
|
||||||
"""Create file handler which logs to file."""
|
"""Create file handler which logs to file."""
|
||||||
|
|
||||||
fh = logging.FileHandler(logfile, 'at')
|
fh = logging.FileHandler(logfile, 'at')
|
||||||
|
#fh.setLevel(logging.DEBUG)
|
||||||
file_formatter = logging.Formatter("%(asctime)s %(levelname)s: "
|
file_formatter = logging.Formatter("%(asctime)s %(levelname)s: "
|
||||||
"%(message)s", '%Y-%m-%d %H:%M:%S')
|
"%(message)s", '%Y-%m-%d %H:%M:%S')
|
||||||
fh.setFormatter(file_formatter)
|
fh.setFormatter(file_formatter)
|
||||||
@ -102,9 +115,11 @@ 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.__version__, p_ver, sys.platform,
|
||||||
" ".join(sys.argv))
|
" ".join(sys.argv))
|
||||||
self.logger.info(msg)
|
record = logging.LogRecord('OfflineImap', logging.INFO, __file__,
|
||||||
|
None, msg, None, None)
|
||||||
|
fh.emit(record)
|
||||||
|
|
||||||
def _msg(self, msg):
|
def _msg(self, msg):
|
||||||
"""Display a message."""
|
"""Display a message."""
|
||||||
@ -430,7 +445,7 @@ class UIBase(object):
|
|||||||
#TODO: Debug and make below working, it hangs Gmail
|
#TODO: Debug and make below working, it hangs Gmail
|
||||||
#res_type, response = conn.id((
|
#res_type, response = conn.id((
|
||||||
# 'name', offlineimap.__productname__,
|
# 'name', offlineimap.__productname__,
|
||||||
# 'version', offlineimap.__bigversion__))
|
# 'version', offlineimap.__version__))
|
||||||
#self._msg("Server ID: %s %s" % (res_type, response[0]))
|
#self._msg("Server ID: %s %s" % (res_type, response[0]))
|
||||||
self._msg("Server welcome string: %s" % str(conn.welcome))
|
self._msg("Server welcome string: %s" % str(conn.welcome))
|
||||||
self._msg("Server capabilities: %s\n" % str(conn.capabilities))
|
self._msg("Server capabilities: %s\n" % str(conn.capabilities))
|
||||||
|
@ -21,6 +21,7 @@ from offlineimap.ui import TTY, Noninteractive, Machine
|
|||||||
UI_LIST = {'ttyui': TTY.TTYUI,
|
UI_LIST = {'ttyui': TTY.TTYUI,
|
||||||
'basic': Noninteractive.Basic,
|
'basic': Noninteractive.Basic,
|
||||||
'quiet': Noninteractive.Quiet,
|
'quiet': Noninteractive.Quiet,
|
||||||
|
'syslog': Noninteractive.Syslog,
|
||||||
'machineui': Machine.MachineUI}
|
'machineui': Machine.MachineUI}
|
||||||
|
|
||||||
#add Blinkenlights UI if it imports correctly (curses installed)
|
#add Blinkenlights UI if it imports correctly (curses installed)
|
||||||
|
@ -24,7 +24,7 @@ __author__ = 'Sebastian Spaeth'
|
|||||||
__author_email__= 'Sebastian@SSpaeth.de'
|
__author_email__= 'Sebastian@SSpaeth.de'
|
||||||
__description__ = 'Moo'
|
__description__ = 'Moo'
|
||||||
__license__ = "Licensed under the GNU GPL v2+ (v2 or any later version)"
|
__license__ = "Licensed under the GNU GPL v2+ (v2 or any later version)"
|
||||||
__homepage__ = "http://offlineimap.org"
|
__homepage__ = "http://www.offlineimap.org"
|
||||||
banner = """%(__productname__)s %(__version__)s
|
banner = """%(__productname__)s %(__version__)s
|
||||||
%(__license__)s""" % locals()
|
%(__license__)s""" % locals()
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user