Merge tag 'v6.7.0' into maint

v6.7.0
This commit is contained in:
Nicolas Sebrecht 2016-06-08 01:56:21 +02:00
commit cb8678a5b5
38 changed files with 1357 additions and 252 deletions

28
.github/ISSUE_TEMPLATE.md vendored Normal file
View 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
View 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
View 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
-->

View File

@ -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
========= =========

View File

@ -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 Hackers 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)

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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
``` ```

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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
--------- ---------

View File

@ -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)

View File

@ -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__

View File

@ -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

View File

@ -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....')]

View File

@ -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
@ -273,11 +282,7 @@ class IMAPFolder(BaseFolder):
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."""
imapobj = self.imapserver.acquireconnection()
try:
query = "(%s)"% (" ".join(self.imap_query)) query = "(%s)"% (" ".join(self.imap_query))
fails_left = retry_num # retry on dropped connection fails_left = retry_num ## retry on dropped connection
while fails_left: while fails_left:
try: try:
imapobj.select(self.getfullname(), readonly = True) imapobj.select(self.getfullname(), readonly = True)
res_type, data = imapobj.uid('fetch', uids, query) res_type, data = imapobj.uid('fetch', uids, query)
fails_left = 0 break
except imapobj.abort as e: except imapobj.abort as e:
fails_left -= 1
# self.ui.error() will show the original traceback
if fails_left <= 0:
message = ("%s, while fetching msg %r in folder %r."
" Max retry reached (%d)"%
(e, uids, self.name, retry_num))
severity = OfflineImapError.ERROR.MESSAGE
raise OfflineImapError(message,
OfflineImapError.ERROR.MESSAGE)
# Release dropped connection, and get a new one # Release dropped connection, and get a new one
self.imapserver.releaseconnection(imapobj, True) self.imapserver.releaseconnection(imapobj, True)
imapobj = self.imapserver.acquireconnection() imapobj = self.imapserver.acquireconnection()
self.ui.error(e, exc_info()[2]) self.ui.error("%s. While fetching msg %r in folder %r."
fails_left -= 1 " Retrying (%d/%d)"%
# self.ui.error() will show the original traceback (e, uids, self.name, retry_num - fails_left, retry_num))
if not fails_left: finally:
raise e # 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

View File

@ -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:
try:
date = emailutil.get_message_date(content, 'Date') date = emailutil.get_message_date(content, 'Date')
if date != None: if date is not None:
os.utime(os.path.join(self.getfullname(), tmpname), (date, date)) 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))

214
offlineimap/imaplib2.py Normal file → Executable file
View 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,8 +569,8 @@ 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)
@ -543,6 +578,13 @@ class IMAP4(object):
"""shutdown() """shutdown()
Close I/O established in "open".""" Close I/O established in "open"."""
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() self.sock.close()
@ -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,13 +1274,16 @@ 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:
if str != bytes:
raise self.abort(bye[-1].decode('ASCII', 'replace'))
else:
raise self.abort(bye[-1]) raise self.abort(bye[-1])
@ -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,6 +1461,9 @@ 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:
if str != bytes:
rqb.abort(self.abort, bye[-1].decode('ASCII', 'replace'))
else:
rqb.abort(self.abort, bye[-1]) rqb.abort(self.abort, bye[-1])
return return
typ, dat = response typ, dat = response
@ -1420,6 +1471,7 @@ class IMAP4(object):
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,6 +1820,9 @@ 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)])
if bytes != str:
line_part = b''
else:
line_part = '' 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,6 +1907,9 @@ class IMAP4(object):
if __debug__: self._log(1, 'starting using select') if __debug__: self._log(1, 'starting using select')
if bytes != str:
line_part = b''
else:
line_part = '' line_part = ''
rxzero = 0 rxzero = 0
@ -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,17 +2171,8 @@ 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:
dlen = len(data)
while dlen > 0:
sent = self.sock.write(bytes(data, 'utf8'))
if sent == dlen:
break # avoid copy
data = data[sent:]
dlen = dlen - sent
else:
if hasattr(self.sock, "sendall"): if hasattr(self.sock, "sendall"):
self.sock.sendall(data) self.sock.sendall(data)
else: else:
@ -2195,8 +2257,8 @@ 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:

View File

@ -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']

View File

@ -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:

View File

@ -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

View File

@ -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)

View File

@ -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?"""

View File

@ -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

View File

@ -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', '[]'))

View File

@ -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

View File

@ -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)

View File

@ -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.

View File

@ -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))

View File

@ -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)

3
setup.cfg Normal file
View File

@ -0,0 +1,3 @@
[metadata]
description-file = README.md

View File

@ -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()