Compare commits
224 Commits
Author | SHA1 | Date |
---|---|---|
Chris Coleman | ffbace51a2 | |
Chris Coleman | 7eacce1f58 | |
Chris Coleman | 87b04f0f54 | |
Chris Coleman | 8bd94ee8bc | |
Nicolas Sebrecht | e70d3992a0 | |
Nicolas Sebrecht | 99a7f74805 | |
Nicolas Sebrecht | 6e837c614d | |
Nicolas Sebrecht | 1953001196 | |
Nicolas Sebrecht | a4ee6675ce | |
Nicolas Sebrecht | a2a92e7433 | |
Nicolas Sebrecht | feec770809 | |
Cathal Garvey | 069c05a0c2 | |
Nicolas Sebrecht | 2d0d07cd6a | |
Rodolfo García Peñas (kix) | 595b814074 | |
Rodolfo García Peñas (kix) | 2722234d16 | |
Rodolfo García Peñas (kix) | 3d925b006a | |
Rodolfo García Peñas (kix) | c7cb68ad7f | |
Reto Schnyder | f6935342c2 | |
Nicolas Sebrecht | 21332efb08 | |
Nicolas Sebrecht | fdb9974ab7 | |
Martin Di Paola | c13e0135a7 | |
Martin Di Paola | 8599cab2ab | |
Nicolas Sebrecht | 7531ac4640 | |
Herton R. Krzesinski | 6a25f1930c | |
Frank LENORMAND | da69fd81ed | |
martin f. krafft | 4562b1c5d1 | |
Chris Coleman | 84efb45270 | |
Nicolas Sebrecht | 564930725e | |
Nicolas Sebrecht | 1e4990524e | |
Chris Coleman | 50cfab5ba2 | |
Jaroslav Lichtblau | 3c80607907 | |
Nicolas Sebrecht | f6e08b8609 | |
Chris Coleman | 85c7692284 | |
Chris Coleman | c35e9257fd | |
Jess | c391141b56 | |
Nicolas Sebrecht | c53664544e | |
Chris Coutinho | 403ddf9c1c | |
Nicolas Sebrecht | 4208fd4a15 | |
Nicolas Sebrecht | 0b18cb5e73 | |
Nicolas Sebrecht | 3d918ff7fe | |
Nicolas Sebrecht | 4464195326 | |
Nicolas Sebrecht | ba4ecea9e4 | |
Dario Maiocchi | 805a1c156c | |
Nicolas Sebrecht | 9c6a1760b4 | |
Ilias Tsitsimpis | beaaf77a1a | |
Julien Cristau | 0a831b3cab | |
Nicolas Sebrecht | 741b33b455 | |
Ben Cotterell | 8d5b22bd09 | |
Nicolas Sebrecht | 8406ba3bb4 | |
Nicolas Sebrecht | c97fe498e6 | |
Olivier Mehani | 0d5496ba0a | |
Nicolas Sebrecht | 06ed00a211 | |
Nicolas Sebrecht | 801893cc73 | |
kimim | 93bc8d11ac | |
kimim | 2806f40071 | |
Jelmer Vernooij | 8a7946e338 | |
Nicolas Sebrecht | 64c763c7f5 | |
Nicolas Sebrecht | 0d865c2e36 | |
Kyle Altendorf | 57a43f5bba | |
Nicolas Sebrecht | f9aefed704 | |
Nicolas Sebrecht | 5c735fd327 | |
Mart Lubbers | c9f71b0c64 | |
Nicolas Sebrecht | 137130fbd0 | |
Robbie Harwood | 096aa07650 | |
Nicolas Sebrecht | a51064e83a | |
Nicolas Sebrecht | 698ec64319 | |
Frode Aannevik | 8692799e65 | |
Philippe Loctaux | af3a35ae30 | |
Philippe Loctaux | 761e10e8b1 | |
Nicolas Sebrecht | d9301254ff | |
Philippe Loctaux | 2890dec37f | |
Philippe Loctaux | 7af4728d62 | |
Benedikt Heine | ce9a1981c1 | |
Benedikt Heine | d3ba837900 | |
Nicolas Sebrecht | 6ef5937a5c | |
Nicolas Sebrecht | 4544bb1305 | |
Carnë Draug | c9005cd4ff | |
Carnë Draug | 5f9474e10d | |
Nicolas Sebrecht | 11313a9b9c | |
Lorenzo | c865dcc03c | |
Nicolas Sebrecht | b5ffa1c163 | |
Robbie Harwood | 17cfb63db6 | |
Nicolas Sebrecht | fc61c6fd30 | |
Nicolas Sebrecht | 356bd0b78f | |
velleto | cf95a1b341 | |
velleto | c126b4286d | |
velleto | c5f8603ff2 | |
velleto | b150daaa49 | |
velleto | 5180b964d8 | |
velleto | ac9ed47262 | |
Michael Billington | db4a5e149f | |
Edgar HIPP | 89b40ec3af | |
Nicolas Sebrecht | 33384ec6fb | |
Nicolas Sebrecht | 487c671d88 | |
Nicolas Sebrecht | e4fa84ba1f | |
Chris Coleman | 88197a7e90 | |
Eygene Ryabinkin | 3a807d0f2b | |
Nicolas Sebrecht | 0ad8bb25ad | |
Nicolas Sebrecht | 57b2794bfb | |
Nicolas Sebrecht | dd0be5e166 | |
Nicolas Sebrecht | da86c2f564 | |
Nicolas Sebrecht | 04932f18e4 | |
Nicolas Sebrecht | a6de848a23 | |
Nicolas Sebrecht | 2b64e100b0 | |
Nicolas Sebrecht | 08ce510cfb | |
Nicolas Sebrecht | 52e8483bbe | |
Nicolas Sebrecht | cf3a6ecde2 | |
Nicolas Sebrecht | 36dae1aefe | |
chris001 | fc52034ea8 | |
Nicolas Sebrecht | 610308f97c | |
Nicolas Sebrecht | 0f65469ce1 | |
Nicolas Sebrecht | ea5093bc5e | |
Robbie Harwood | 88724949fa | |
Musashi69 | c8847ccff9 | |
Nicolas Sebrecht | f732d6b2b6 | |
Nicolas Sebrecht | 67d35dbbcb | |
Nicolas Sebrecht | 0d6a9a44da | |
Nicolas Sebrecht | e802f5fbd5 | |
Evan Dandrea | e8b31af4c2 | |
Nicolas Sebrecht | a4b89f1034 | |
John Ferlito | 3c6b07b25f | |
Nicolas Sebrecht | 9805d3e7af | |
Nicolas Sebrecht | 681e271fc0 | |
Nicolas Sebrecht | 5836970d51 | |
Nicolas Sebrecht | a79263bb31 | |
Thomas Merkel | ee17e3dc3c | |
Nicolas Sebrecht | 8c9cd5b7e3 | |
Nicolas Sebrecht | 832c704443 | |
Nicolas Sebrecht | 0d65762168 | |
Nicolas Sebrecht | e1a6feb2d5 | |
Nicolas Sebrecht | 392e64c3b3 | |
Nicolas Sebrecht | a9514c2b8a | |
Nicolas Sebrecht | 6b64f87462 | |
Nicolas Sebrecht | 12de158ca2 | |
Urs Liska | f5198794e5 | |
Urs Liska | 8b398f3aa8 | |
Urs Liska | ef3299b7ce | |
Urs Liska | 36d726763d | |
Urs Liska | 14d83dbf48 | |
Urs Liska | dca5f1846d | |
Urs Liska | 24b3f27e5f | |
Urs Liska | 032376efad | |
Nicolas Sebrecht | 31eee55672 | |
Thomas Merkel | 1ce596d713 | |
Nicolas Sebrecht | 4b18ffd5e3 | |
Nicolas Sebrecht | 172b4279ca | |
Nicolas Sebrecht | 19442d0010 | |
Nicolas Sebrecht | a5d9edc560 | |
Nicolas Sebrecht | 6079755b20 | |
Nicolas Sebrecht | df7dd3e3b7 | |
Nicolas Sebrecht | ec0460a668 | |
Nicolas Sebrecht | e4bee74dad | |
Nicolas Sebrecht | ceb69c7033 | |
Nicolas Sebrecht | 7d1a540598 | |
Nicolas Sebrecht | 0903d5f33c | |
Nicolas Sebrecht | 4b103d6d3c | |
Nicolas Sebrecht | 212ed87509 | |
Nicolas Sebrecht | fe2e3249ac | |
Nicolas Sebrecht | 481f44c784 | |
Nicolas Sebrecht | 90244b7fa6 | |
Nicolas Sebrecht | cf01d8e0eb | |
Alvaro Pereyra | ca21b2bd31 | |
Nicolas Sebrecht | 67b4b0d9b4 | |
Nicolas Sebrecht | ba47138616 | |
Nicolas Sebrecht | ce83efc3c7 | |
Nicolas Sebrecht | 05ff68c7e1 | |
Nicolas Sebrecht | fc77de5af6 | |
Nicolas Sebrecht | 47a7bdc883 | |
Nicolas Sebrecht | 6e917bf0e5 | |
Hugo Osvaldo Barrera | 71f5a7759b | |
Hugo Osvaldo Barrera | e7a3fd55ac | |
Hugo Osvaldo Barrera | 87d92badfc | |
Hugo Osvaldo Barrera | a242a07582 | |
Hugo Osvaldo Barrera | 8bbfca9016 | |
benutzer193 | efd2548f67 | |
Nicolas Sebrecht | f3b9963a48 | |
Nicolas Sebrecht | f9960f9293 | |
Nicolas Sebrecht | 176fc19c55 | |
Nicolas Sebrecht | 33889c04bb | |
Nicolas Sebrecht | 6b51d4ed49 | |
Nicolas Sebrecht | d0d832e14c | |
Nicolas Sebrecht | ac5cfb0e29 | |
Nicolas Sebrecht | f37b97c61b | |
benutzer193 | d1e770fdcb | |
Nicolas Sebrecht | 5d5ad62fa7 | |
Ævar Arnfjörð Bjarmason | ddf2df1d8d | |
Chris Coleman | 99b06ef47e | |
Nicolas Sebrecht | ffeefd9459 | |
Nicolas Sebrecht | 237d1ce5e7 | |
Nicolas Sebrecht | 22a163a768 | |
Nicolas Sebrecht | 8c04684f3b | |
Nicolas Sebrecht | 39b657194a | |
Maximilian Kaul | 4ac44071b6 | |
Nicolas Sebrecht | e8f0e82f6c | |
Nicolas Sebrecht | e9d8e87a71 | |
Ilias Tsitsimpis | 7bc54d241c | |
Nicolas Sebrecht | 2b1c842a7d | |
Nicolas Sebrecht | 76ca65e0b2 | |
Nicolas Sebrecht | 33a7f3c14c | |
Nicolas Sebrecht | 874106d3bd | |
Nicolas Sebrecht | a4c887e5b1 | |
Nicolas Sebrecht | a92b116a4f | |
Nicolas Sebrecht | f669b3ea2f | |
Nicolas Sebrecht | 2c6fac6449 | |
benutzer193 | e88a6bcf03 | |
benutzer193 | 11c6995ac2 | |
benutzer193 | 1462d0994a | |
benutzer193 | bf85746923 | |
Nicolas Sebrecht | c8206f24e1 | |
Nicolas Sebrecht | 49c6f14ce4 | |
Nicolas Sebrecht | 1670505780 | |
Ilias Tsitsimpis | eb7651898e | |
Nicolas Sebrecht | ebf5fd527e | |
Nicolas Sebrecht | 5fa0340f9d | |
Nicolas Sebrecht | c883160814 | |
Nicolas Sebrecht | f9360ba2d7 | |
Nicolas Sebrecht | fc079499e9 | |
Nicolas Sebrecht | 04cb32bf9e | |
Stéphane Graber | e3bbf75feb | |
Nicolas Sebrecht | 4df06d57c3 | |
Nicolas Sebrecht | 23b497d191 | |
Jens Heinrich | 7c7d693bd5 | |
927589452 | 2c77e00946 | |
mailinglists@927589452.de | b143f63031 |
|
@ -0,0 +1,14 @@
|
|||
[run]
|
||||
branch = True
|
||||
source = offlineimap
|
||||
|
||||
[report]
|
||||
exclude_lines =
|
||||
if self.debug:
|
||||
pragma: no cover
|
||||
raise NotImplementedError
|
||||
if __name__ == .__main__.:
|
||||
ignore_errors = True
|
||||
omit =
|
||||
tests/*
|
||||
test/*
|
|
@ -0,0 +1,19 @@
|
|||
# This is a comment.
|
||||
# Each line is a file pattern followed by one or more owners.
|
||||
|
||||
# These owners will be the default owners for everything in the repo.
|
||||
# Unless a later match takes precedence, @global-owner1 and @global-owner2
|
||||
# will be requested for review when someone opens a pull request.
|
||||
#* @global-owner1 @global-owner2
|
||||
|
||||
# Order is important; the last matching pattern takes the most precedence.
|
||||
# When someone opens a pull request that only modifies JS files, only @js-owner
|
||||
# and not the global owner(s) will be requested for a review.
|
||||
#*.js @js-owner
|
||||
|
||||
# You can also use email addresses if you prefer. They'll be used to look up
|
||||
# users just like we do for commit author emails.
|
||||
#docs/* docs@example.com
|
||||
|
||||
|
||||
* @chris001
|
|
@ -0,0 +1,3 @@
|
|||
# These are supported funding model platforms
|
||||
|
||||
open_collective: offlineimap-organization
|
|
@ -1,24 +1,14 @@
|
|||
> 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 v1.1 template stands in `.github/`.
|
||||
|
||||
### 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).
|
||||
- [ ] 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
|
||||
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
# Backups.
|
||||
.*.swp
|
||||
.*.swo
|
||||
*.html
|
||||
*~
|
||||
# websites.
|
||||
/website/
|
||||
/wiki/
|
||||
# Generated files.
|
||||
*.html
|
||||
*.css
|
||||
/docs/dev-doc/
|
||||
/build/
|
||||
*.pyc
|
||||
|
@ -14,3 +15,5 @@ offlineimap.1
|
|||
offlineimapui.7
|
||||
# Editors/IDEs
|
||||
tags
|
||||
# Generated conf files for Travis-CI tests.
|
||||
oli-travis.conf
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
language: python
|
||||
python:
|
||||
- '2.7'
|
||||
notifications:
|
||||
webhooks:
|
||||
urls:
|
||||
- https://webhooks.gitter.im/e/975e807e0314c9fa189c
|
||||
on_success: always # options: [always|never|change] default: always
|
||||
on_failure: always # options: [always|never|change] default: always
|
||||
on_start: never
|
||||
os:
|
||||
- linux
|
||||
env:
|
||||
global:
|
||||
- secure: jehlvkFxQbkvr73A0z3HGNC/knZQPKcaXLf6nByGpNE0ZTQKF7Y5KkNfeTcw4st7L7KuRZ1S/1bFtpMXTaplE6G0OtIEC4//SM+z+Dnadn2OY6wHiaapwZmmqDC5qVvcXPdmz/wTRsdrJSGLb2l6kEb91vRGbCCfHHf6Z2cF71U=
|
||||
- secure: kWdmWAFK4qrA73ONz1X8CJdHSER3bCBXjLfYHYEEMPCZep21bTITUXIfZBlSNN1888SQtYksuloRJmvj7xiY/hf/4lyWiqM3RgWQ+YptJMVOQX+Gara6vm4nGntKQwaXgZF2YHSh+NYwQm1VY6m0n1ye/vfOIJnYfgGTk5qAZYU=
|
||||
- secure: MzytYRX6HxgBj6Q3efkACTtDed8ZYO+P6UJrDA9IDtvffi8fAFb+wkQtKJrdcvMXNOap6fPe4c0EVGjgL5hFxmgC8yAh5t2YK7OhstAtq0ptKFlOcU24/drrkqoq040sAM/4Lc0nQCvYpz7bH370jzZl69rpbQWttwQR0i1e3Gw=
|
||||
- secure: RWvIOHSiv2kt6cfZR7MEueiAmC61bWMXAtgsC6gKq1u3BfENfqSBTA/heIy+nlu7AXK1b6hPMZDCHWK09Zz6Klkd9xZ1gkE/AARWseoo9UWgGjmfvqng1S6qpESeX2GnZGR9CuBXTPGhtbYLgtNlxAo+6uZLolz2utW2XNk3Z/Y=
|
||||
- secure: spivQv+vSJhE+ttn/Z6tANaINqiMSaJSucRqtoXR7PtioVDTOTmmL01Ja6dXuo8Ua5iVFtpZPDzqVpntQLKtjcywSK2zWnC9qbZYDfENr1/yIvfbSRjGeseq0eoY+fFp67FGZV4mIasdC3LOB0lRGOyrsX787fNKVQ8ZH0CRz0o=
|
||||
- secure: ZcY0TvTQnRCdoFkdbJPfDJJNx91tViwbpiOBkxNEa3u0RN48xkZkii35kNVBaEcVZHcT9C81ctHk4QX+plBkCsoj5GDf25scgcv1j9R9UoN/rIkmyTu1Znmc+3UQ2J+EnGLWVn5xJ7yT/l9NZeLfNbULQRjttwT4j2MBGxezgdM=
|
||||
matrix:
|
||||
- OUTLOOK_AUTH=PLAIN GMAIL_AUTH=XOAUTH2
|
||||
- OUTLOOK_AUTH=LOGIN GMAIL_AUTH=XOAUTH2
|
||||
matrix:
|
||||
include:
|
||||
- os: osx
|
||||
language: generic
|
||||
env: PYTHON=2.7.14 OUTLOOK_AUTH=PLAIN GMAIL_AUTH=XOAUTH2
|
||||
- os: osx
|
||||
language: generic
|
||||
env: PYTHON=2.7.14 OUTLOOK_AUTH=LOGIN GMAIL_AUTH=XOAUTH2
|
||||
allow_failures:
|
||||
- os: osx
|
||||
cache: pip
|
||||
before_install:
|
||||
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew update && brew install openssl readline;
|
||||
fi
|
||||
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then export OSX_BREW_SSLCACERTFILE="/usr/local/etc/openssl/cert.pem";
|
||||
fi
|
||||
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew outdated pyenv || brew upgrade pyenv;
|
||||
fi
|
||||
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew install pyenv-virtualenv; fi
|
||||
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then pyenv install $PYTHON; fi
|
||||
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then export PYENV_VERSION="${PYTHON}"; fi
|
||||
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then export PATH="/Users/travis/.pyenv/shims:${PATH}";
|
||||
fi
|
||||
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then pyenv virtualenv $PYTHON myvenv; fi
|
||||
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then pyenv versions; fi
|
||||
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then python --version; fi
|
||||
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then pyenv version; fi
|
||||
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then python --version; fi
|
||||
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then python -m pip install -U pip; fi
|
||||
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then python -m easy_install -U setuptools;
|
||||
fi
|
||||
install:
|
||||
- pip install -r requirements.txt
|
||||
- pip install -r tests/requirements.txt
|
||||
- export PATH=$PATH:.
|
||||
- python tests/create_conf_file.py
|
||||
script:
|
||||
- "./offlineimap.py -c ./oli-travis.conf"
|
||||
- codecov
|
|
@ -0,0 +1,24 @@
|
|||
### Many thanks to our wonderful Sponsors! Your generous support helps us maintain OfflineIMAP.
|
||||
|
||||
|
||||
<!-- PLATINUM sponsors: in the README.md and on the front page of website -->
|
||||
<!-- https://shields.io/opencollective/tier/offlineimap-organization/12940 -->
|
||||
|
||||
<!-- GOLD sponsors: in the README.md and on the front page of website -->
|
||||
<!-- https://shields.io/opencollective/tier/offlineimap-organization/12941 -->
|
||||
|
||||
# SILVER sponsors <!-- here and on sponsors page of website -->
|
||||
<!-- https://shields.io/opencollective/tier/offlineimap-organization/12942 -->
|
||||
[![silver-zeronet-logo]][silver-zeronet-link]
|
||||
|
||||
![Silver Sponsors][silver]
|
||||
|
||||
# BRONZE sponsors <!-- here only -->
|
||||
<!-- https://shields.io/opencollective/tier/offlineimap-organization/12873 -->
|
||||
![Bronze Sponsors][bronze]
|
||||
|
||||
<!-- links / references -->
|
||||
[silver]: https://opencollective.com/offlineimap-organization/tiers/silver-sponsor.svg "Our Silver Sponsors"
|
||||
[silver-zeronet-logo]: https://github.com/OfflineIMAP/offlineimap.github.io/raw/master/assets/img/sponsors/zeronet.svg
|
||||
[silver-zeronet-link]: https://zeronet.co.nz/ "Zeronet - naturally fast internet"
|
||||
[bronze]: https://opencollective.com/offlineimap-organization/tiers/bronze-sponsor.svg "Our Bronze Sponsors"
|
768
Changelog.md
768
Changelog.md
|
@ -15,6 +15,774 @@ Note to mainainers:
|
|||
* The following excerpt is only usefull when rendered in the website.
|
||||
{:toc}
|
||||
|
||||
### OfflineIMAP v7.3.4 (2021-08-03)
|
||||
|
||||
#### Notes
|
||||
|
||||
The release is likely the last minor release. This project will be maintained
|
||||
for minor bug fixes only.
|
||||
|
||||
In this version we've backported patches from the offlineimap3 project. There's
|
||||
no new features. Users should try this fork. The official project is there and
|
||||
is maintained by Rodolfo:
|
||||
|
||||
https://github.com/OfflineIMAP/offlineimap3
|
||||
|
||||
This release was tested by:
|
||||
|
||||
- Nicolas Sebrecht
|
||||
|
||||
|
||||
#### Authors
|
||||
|
||||
- Nicolas Sebrecht (5)
|
||||
- Rodolfo García Peñas (kix) (4)
|
||||
- Martin Di Paola (2)
|
||||
- Reto Schnyder (1)
|
||||
|
||||
|
||||
#### Fixes
|
||||
|
||||
- folder: IMAP: fix issue when the response of searchforheaders is the same UID multiple times. [Nicolas Sebrecht]
|
||||
- Fix hooks for IDLE sync. [Reto Schnyder]
|
||||
- Changed wrong comparison equal. [Rodolfo García Peñas (kix)]
|
||||
- Comparison error. [Rodolfo García Peñas (kix)]
|
||||
- remove outdated links to travis. [Nicolas Sebrecht]
|
||||
- ui init is lintian clean. [Rodolfo García Peñas (kix)]
|
||||
- Require the minimal dependencies in python package. [Martin Di Paola]
|
||||
|
||||
#### Changes
|
||||
|
||||
- README: update regarding the offlineimap3 fork. [Nicolas Sebrecht]
|
||||
- redirect the users to offlineimap3. [Nicolas Sebrecht]
|
||||
- threadutil imports not used. [Rodolfo García Peñas (kix)]
|
||||
- Move out pkg attributes from __init__.py. [Martin Di Paola]
|
||||
|
||||
|
||||
### OfflineIMAP v7.3.3 (2020-04-11)
|
||||
|
||||
#### Notes
|
||||
|
||||
Here is a small release after nearly 4 months of slow moves. Still, the patches
|
||||
are very usefull for some use cases. Thanks to the contributors!
|
||||
|
||||
This release was tested by:
|
||||
|
||||
- Nicolas Sebrecht
|
||||
|
||||
|
||||
#### Authors
|
||||
|
||||
- Chris Coleman (1)
|
||||
- Frank LENORMAND (1)
|
||||
- Herton R. Krzesinski (1)
|
||||
- martin f. krafft (1)
|
||||
|
||||
|
||||
#### Features
|
||||
|
||||
- export env. variables when running account hooks. [Frank LENORMAND]
|
||||
|
||||
#### Fixes
|
||||
|
||||
- Fix stale gss api authentication security context. [Herton R. Krzesinski]
|
||||
- Handle [ALREADYEXISTS] and Mailbox already exists!. [Chris Coleman]
|
||||
|
||||
#### Changes
|
||||
|
||||
- exec() the tunnel command. [martin f. krafft]
|
||||
|
||||
|
||||
|
||||
|
||||
### OfflineIMAP v7.3.2 (2019-12-17)
|
||||
|
||||
#### Notes
|
||||
|
||||
This is a very small bug fix release.
|
||||
|
||||
|
||||
This release was tested by:
|
||||
|
||||
- Nicolas Sebrecht
|
||||
|
||||
|
||||
#### Authors
|
||||
|
||||
- Jaroslav Lichtblau (1)
|
||||
- Nicolas Sebrecht (1)
|
||||
|
||||
|
||||
#### Fixes
|
||||
|
||||
- Revert "fix check for unsupported sep character". [Nicolas Sebrecht]
|
||||
- Fixing the Arch Linux name. [Jaroslav Lichtblau]
|
||||
|
||||
|
||||
### OfflineIMAP v7.3.1 (2019-12-15)
|
||||
|
||||
#### Notes
|
||||
|
||||
|
||||
This release was tested by:
|
||||
|
||||
- Nicolas Sebrecht
|
||||
|
||||
|
||||
#### Authors
|
||||
|
||||
- Nicolas Sebrecht (5)
|
||||
- Chris Coleman (1)
|
||||
- Chris Coutinho (1)
|
||||
- Jess (1)
|
||||
|
||||
|
||||
#### Features
|
||||
|
||||
- Additional address for sysloghandler to handle mac. [Chris Coutinho]
|
||||
- Added financial contributors to the README. [Jess]
|
||||
- Introduce FUNDING.yml for opencollective. [Nicolas Sebrecht]
|
||||
|
||||
|
||||
#### Fixes
|
||||
|
||||
- Fix check for unsupported sep character. [Nicolas Sebrecht]
|
||||
- Contrib: use yaml.safe_load() instead of load(). [Nicolas Sebrecht]
|
||||
- Ensure python2 in the release workflow. [Nicolas Sebrecht]
|
||||
- Make docs: ensure py2 when running sphinx. [Nicolas Sebrecht]
|
||||
|
||||
|
||||
#### Changes
|
||||
|
||||
- Update README.md. [Chris Coleman]
|
||||
|
||||
|
||||
### OfflineIMAP v7.3.0 (2019-08-19)
|
||||
|
||||
#### Notes
|
||||
|
||||
Here comes a new release.
|
||||
|
||||
The upstream imaplib2 project is discontinued. That's why I've decided to take
|
||||
over the maintenance of imaplib2 for offlineimap.
|
||||
|
||||
For the use of offlineimap I've applied the pending PRs from imaplib2. I have
|
||||
applied another change sent to offlineimap (see OfflineIMAP/offlineimap#623).
|
||||
|
||||
However, there are 3 important limitations:
|
||||
|
||||
- I intend to maintain imaplib2 for offlineimap only. Everything will take place
|
||||
in the offlineimap project. I'll neither package imaplib2 nor maintain any
|
||||
"official" repository dedicated to imaplib2. If you want imaplib2 but not
|
||||
offlineimap, you should extract the file `offlineimap/bundled_imaplib2.py` from
|
||||
the offlineimap repository. Please, send your patches for imaplib2.py to the
|
||||
offlineimap project directly.
|
||||
|
||||
- Starting from imaplib2 v2.100 (tagged: imaplib2-v2.100) I'm taking the patches
|
||||
in the lazy mode. This means that I won't make deep checks/tests of the
|
||||
changes. Hence, the quality and the stability might become a bit more
|
||||
fluctuating. For more stability, you might like to only consider the imaplib2
|
||||
versions released with the stable versions of offlineimap. Don't expect
|
||||
changelogs dedicated to imaplib2. They will be part of the offlineimap
|
||||
changelogs.
|
||||
|
||||
- All of this only applies to the py2 version of imaplib2. Sadly, offlineimap
|
||||
has few chances to be ported on py3 so I don't aim to maintain the py3 version
|
||||
of imaplib2.
|
||||
|
||||
|
||||
In this release, offlineimap is learning Happy Eyeballs.
|
||||
|
||||
|
||||
This release was tested by:
|
||||
|
||||
- Nicolas Sebrecht
|
||||
|
||||
|
||||
#### Authors
|
||||
|
||||
- Nicolas Sebrecht (4)
|
||||
- Ben Cotterell (1)
|
||||
- Dario Maiocchi (1)
|
||||
- Ilias Tsitsimpis (1)
|
||||
- Julien Cristau (1)
|
||||
- Olivier Mehani (1)
|
||||
|
||||
|
||||
#### Features
|
||||
|
||||
- Implement Happy Eyeballs. [Olivier Mehani]
|
||||
- imaplib2 v2.101. [Nicolas Sebrecht]
|
||||
- imaplib2 v2.100. [Nicolas Sebrecht]
|
||||
|
||||
#### Changes
|
||||
|
||||
- Update readme to give an hint about Linux distros. [Dario Maiocchi]
|
||||
- travis: remove python3.6. [Nicolas Sebrecht]
|
||||
- README: add required dependency to rfc6555. [Nicolas Sebrecht]
|
||||
|
||||
#### imaplib2
|
||||
|
||||
- Do not use TIMEOUT_MAX for Condition.wait(). [Ilias Tsitsimpis]
|
||||
- Use SSLContext if available so we send SNI. [Julien Cristau]
|
||||
- Don't expect trailing space on command completion. [Ben Cotterell]
|
||||
|
||||
|
||||
### OfflineIMAP v7.2.4 (2019-06-08)
|
||||
|
||||
#### Notes
|
||||
|
||||
This release introduces mkdir -p alike folder creation and fixes cygwin support
|
||||
in Windows.
|
||||
|
||||
This release was tested by:
|
||||
|
||||
- Nicolas Sebrecht
|
||||
|
||||
|
||||
#### Authors
|
||||
|
||||
- Nicolas Sebrecht (4)
|
||||
- kimim (2)
|
||||
- Jelmer Vernooij (1)
|
||||
- Kyle Altendorf (1)
|
||||
|
||||
|
||||
#### Features
|
||||
|
||||
- mkdir -p alike folder creation. [Kyle Altendorf]
|
||||
|
||||
#### Fixes
|
||||
|
||||
- Use portable locker to support cygwin in Windows. [kimim]
|
||||
- contrib/release.py: don't break if sphinx-build is missing. [Nicolas Sebrecht]
|
||||
|
||||
#### Changes
|
||||
|
||||
- Update FSF postal address. [Jelmer Vernooij]
|
||||
- repository/IMAP: update copyright header date. [Nicolas Sebrecht]
|
||||
- PULL_REQUEST_TEMPLATE: add space between brackets to enable the edition in the gui. [Nicolas Sebrecht]
|
||||
|
||||
|
||||
### OfflineIMAP v7.2.3 (2019-02-17)
|
||||
|
||||
#### Notes
|
||||
|
||||
A tiny release for one minor bug fix.
|
||||
|
||||
This release was tested by:
|
||||
|
||||
- Nicolas Sebrecht
|
||||
|
||||
|
||||
#### Authors
|
||||
|
||||
- Mart Lubbers (1)
|
||||
|
||||
|
||||
#### Fixes
|
||||
|
||||
- add checks in curses ui for small windows. [Mart Lubbers]
|
||||
|
||||
|
||||
### OfflineIMAP v7.2.2 (2018-12-22)
|
||||
|
||||
#### Notes
|
||||
|
||||
With this release offlineimap can renew the token for OAUTH2. There is better
|
||||
integration for ArchLinux and OSX. SSL configuration options are more
|
||||
consistent.
|
||||
|
||||
There are bug fixes about maxage and GSSAPI.
|
||||
|
||||
The imaplib2 library looks discontinued. I wonder we'll have no other choice
|
||||
than maintaining our own fork.
|
||||
|
||||
This release was tested by:
|
||||
|
||||
- Nicolas Sebrecht
|
||||
|
||||
|
||||
#### Authors
|
||||
|
||||
- Nicolas Sebrecht (5)
|
||||
- Philippe Loctaux (4)
|
||||
- Benedikt Heine (2)
|
||||
- Carnë Draug (2)
|
||||
- Frode Aannevik (1)
|
||||
- Robbie Harwood (1)
|
||||
|
||||
|
||||
#### Features
|
||||
|
||||
- 2890dec Added ssl certfile on osx for openssl pacakge on homebrew. [Philippe Loctaux]
|
||||
- 761e10e Add Archlinux to list of supported distros. [Philippe Loctaux]
|
||||
|
||||
#### Fixes
|
||||
|
||||
- 8692799 Fix expired oauth2_access_token. [Frode Aannevik]
|
||||
- 096aa07 Handle empty token with complete GSSAPI context. [Robbie Harwood]
|
||||
- a51064e maxage: always compute the remote cache list for min_uid. [Nicolas Sebrecht]
|
||||
- 698ec64 offlineimap.conf: minor fixes. [Nicolas Sebrecht]
|
||||
- af3a35a offlineimap/utilis/distro.py: indentation fix. [Philippe Loctaux]
|
||||
- d3ba837 Fix typo in exception message. [Benedikt Heine]
|
||||
- c9005cd Check if username is provided before trying plain authentication.. [Carnë Draug]
|
||||
|
||||
#### Changes
|
||||
|
||||
- 5f9474e Print username instead of accountname when asking for password. [Carnë Draug]
|
||||
- ce9a198 Chain tls_level and ssl_version only if ssl is enabled. [Benedikt Heine]
|
||||
- 6ef5937 docs/website-doc.sh: minor improvements in comments of versions.yml. [Nicolas Sebrecht]
|
||||
- 4544bb1 contrib/release.py: minor UI improvement. [Nicolas Sebrecht]
|
||||
- d930125 fix dates in copyright lines. [Nicolas Sebrecht]
|
||||
|
||||
|
||||
### OfflineIMAP v7.2.1 (2018-06-16)
|
||||
|
||||
#### Notes
|
||||
|
||||
This new version introduces interesting features. The fingerprints now accepts
|
||||
hashes in sha224, sha256, sha384 and sha512 to improve the compatibility with
|
||||
IMAP servers.
|
||||
|
||||
There's a new script in ./contrib to store passwords with GPG.
|
||||
|
||||
The new GSSAPI library for kerberos gets a fix about authentication. Gmail
|
||||
labels can now have parenthesis and the hostname can have path separators in
|
||||
theirs names.
|
||||
|
||||
There's a lot of other minors improvements to make offlineimap better
|
||||
(in the documentation, UI, configuration file and the code).
|
||||
|
||||
This release was tested by:
|
||||
|
||||
- Nicolas Sebrecht
|
||||
|
||||
Thanks to all the contributors. A lot of patches are first time contributions to
|
||||
this project. This is very pleasant.
|
||||
|
||||
Special thanks to Ilias Tsitsimpis, Eygene Ryabinkin, Chris Coleman our long
|
||||
time contributors involved in this release and Sebastian Spaeth who is still
|
||||
paying for the domain name!
|
||||
|
||||
|
||||
#### Authors
|
||||
|
||||
- Nicolas Sebrecht (9)
|
||||
- velleto (6)
|
||||
- Chris Coleman (1)
|
||||
- Edgar HIPP (1)
|
||||
- Eygene Ryabinkin (1)
|
||||
- Lorenzo (1)
|
||||
- Michael Billington (1)
|
||||
- Robbie Harwood (1)
|
||||
|
||||
|
||||
#### Features
|
||||
|
||||
- Script to store passwords in a file with GPG or using OSX's secure keychain. [Lorenzo]
|
||||
- Added support for sha512, sha384, sha256, sha224 hashing algorithms to calculate server certificate fingerprints.. [velleto]
|
||||
|
||||
#### Fixes
|
||||
|
||||
- Pass username through in GSSAPI connections. [Robbie Harwood]
|
||||
- Gmail: allow parenthesis in labels. [Nicolas Sebrecht]
|
||||
- Correct typographical errors in offlineimap.conf. [Michael Billington]
|
||||
- Create filenames with no path separators in them. [Eygene Ryabinkin]
|
||||
|
||||
#### Changes
|
||||
|
||||
- imapserver: fix copyright line. [Nicolas Sebrecht]
|
||||
- Available hashes added to documentation.. [velleto]
|
||||
- Documented the now allowed use of colon separated fingerprints with examples.. [velleto]
|
||||
- Allow users to keep colons between each hex pair of server certificate fingerprint in configuration file.. [velleto]
|
||||
- Removed uneccessary call of list() on zip() object.. [velleto]
|
||||
- Changed the 'exception raised' message, to be more understandable.. [velleto]
|
||||
- Make CTRL-C message more clear. [Edgar HIPP]
|
||||
- setup: add long_description. [Nicolas Sebrecht]
|
||||
- offlineimap.conf: fix comment about gssapi. [Nicolas Sebrecht]
|
||||
- Add self to maintainers. Update email address.. [Chris Coleman]
|
||||
- Makefile: targz: don't set the abbrev in the archive directory name. [Nicolas Sebrecht]
|
||||
- contrib: learn to build website/_uploads. [Nicolas Sebrecht]
|
||||
- docs/website-doc.sh: limit the number of exported versions in _data/announces.yml. [Nicolas Sebrecht]
|
||||
- Makefile: targz: update files. [Nicolas Sebrecht]
|
||||
- Makefile: clean: remove __pycache__ directories. [Nicolas Sebrecht]
|
||||
|
||||
|
||||
### OfflineIMAP v7.2.0 (2018-04-07)
|
||||
|
||||
#### Notes
|
||||
|
||||
The biggest change with this release is the introduction of automated tests;
|
||||
thanks to Chris from http://www.espacenetworks.com.
|
||||
|
||||
Robbie Hardwood from RedHat switched the GSSAPI dependency from pykerberos to
|
||||
python-gssapi because it's more active and has more pleasant interface.
|
||||
|
||||
The shebang is fixed back to python2 to fix issues on some environments.
|
||||
|
||||
The UI was improved to show both the local and remote foldernames (usefull when
|
||||
nametrans is enabled).
|
||||
|
||||
Thanks to all the contributors.
|
||||
|
||||
This release was tested by:
|
||||
|
||||
- Nicolas Sebrecht
|
||||
- Remi Locherer
|
||||
|
||||
|
||||
#### Authors
|
||||
|
||||
- Nicolas Sebrecht (9)
|
||||
- Musashi69 (1)
|
||||
- Robbie Harwood (1)
|
||||
- chris001 (1)
|
||||
|
||||
|
||||
#### Features
|
||||
|
||||
- Autmomated testing using Travis and CodeCov.io!. [chris001]
|
||||
- README: travis: add badge for the next branch. [Nicolas Sebrecht]
|
||||
- travis: add notification to gitter room OfflineIMAP/offlineimap. [Nicolas Sebrecht]
|
||||
|
||||
#### Fixes
|
||||
|
||||
- offlineimap.py: fix shebang to python2. [Nicolas Sebrecht]
|
||||
- bin/offlineimap: fix shebang to env python2. [Nicolas Sebrecht]
|
||||
|
||||
#### Changes
|
||||
|
||||
- Port to python-gssapi from pykerberos. [Robbie Harwood]
|
||||
- requirements: add gssapi as optional dependency. [Nicolas Sebrecht]
|
||||
- make UI output show local AND remote dirs involved. [Musashi69]
|
||||
- maxsyncaccounts: improve documentation. [Nicolas Sebrecht]
|
||||
|
||||
|
||||
|
||||
|
||||
### OfflineIMAP v7.1.5 (2018-01-13)
|
||||
|
||||
#### Notes
|
||||
|
||||
This minor release fixes a bug about maxage failing to upload some emails. Also,
|
||||
this introduces the snapcraft.yaml to package offlineimap with this packaging
|
||||
system.
|
||||
|
||||
This release was tested by:
|
||||
|
||||
- Nicolas Sebrecht
|
||||
- Remi Locherer
|
||||
|
||||
|
||||
#### Authors
|
||||
|
||||
- Nicolas Sebrecht (4)
|
||||
- Evan Dandrea (1)
|
||||
- John Ferlito (1)
|
||||
|
||||
|
||||
#### Features
|
||||
|
||||
- Initial commit of snapcraft.yaml. [Evan Dandrea]
|
||||
|
||||
#### Fixes
|
||||
|
||||
- maxage: don't consider negative UIDs when computing min UID. [Nicolas Sebrecht]
|
||||
- Add missing space to output string. [John Ferlito]
|
||||
|
||||
#### Changes
|
||||
|
||||
- folder: IMAP: improve search logging. [Nicolas Sebrecht]
|
||||
- no UIDPLUS: improve logging on failures. [Nicolas Sebrecht]
|
||||
- github: remove the trick to download the PR. [Nicolas Sebrecht]
|
||||
|
||||
|
||||
### OfflineIMAP v7.1.4 (2017-10-29)
|
||||
|
||||
#### Notes
|
||||
|
||||
Here is a bugfix release for v7.1.3. Two regressions got fixes and the
|
||||
--delete-folder CLI option now expects an UTF-8 folder name when utf8foldernames
|
||||
is enabled.
|
||||
|
||||
This release was tested by:
|
||||
|
||||
- Nicolas Sebrecht
|
||||
|
||||
#### Authors
|
||||
|
||||
- Nicolas Sebrecht (5)
|
||||
- Thomas Merkel (1)
|
||||
|
||||
#### Fixes
|
||||
|
||||
- utf8foldernames: fix missing decode argument. [Nicolas Sebrecht]
|
||||
- Fix: if any tunnel (preauth_tunnel or transport_tunnel) the hostname should not be required. [Thomas Merkel]
|
||||
|
||||
#### Changes
|
||||
|
||||
- utf8foldernames: support --delete-folder with UTF-8 folder name. [Nicolas Sebrecht]
|
||||
- contrib/release.py improvements
|
||||
|
||||
|
||||
### OfflineIMAP v7.1.3 (2017-10-08)
|
||||
|
||||
#### Notes
|
||||
|
||||
This release introduces a new experimental utf8foldernames configuration option.
|
||||
|
||||
We already had the "tricky" decodefoldernames which is now deprecated. The new
|
||||
code is the correct implementation for this feature. The changes are neat and
|
||||
rather small. All the users having decodefoldernames are requested to move to
|
||||
utf8foldernames. This requires to update almost all the functions like
|
||||
nametrans, folderfilter, etc, because they work on the UTF-8 encoding. See the
|
||||
documentation for more. Thank you Urs Liska for this contribution!
|
||||
|
||||
In the long run, the idea is to:
|
||||
|
||||
1. Remove decodefoldernames in favour of utf8foldernames.
|
||||
2. Promote utf8foldernames up to stable.
|
||||
3. Turn utf8foldernames on by default.
|
||||
|
||||
Currently, folders with non-ASCII characters in their name have to be fully
|
||||
re-downloaded. So, there's a bit more work to be done to have (3) and maybe (2).
|
||||
|
||||
Also, this release includes a fix about remotehost and transporttunnel that
|
||||
would require some testing. Thanks Thomas Merkel!
|
||||
|
||||
There are documentation improvements, improved errors and minor code cleanups,
|
||||
too.
|
||||
|
||||
This release was tested by:
|
||||
|
||||
- Nicolas Sebrecht
|
||||
- Remi Locherer
|
||||
|
||||
|
||||
#### Authors
|
||||
|
||||
- Nicolas Sebrecht (11)
|
||||
- Urs Liska (8)
|
||||
- Thomas Merkel (1)
|
||||
|
||||
#### Features
|
||||
|
||||
- utf8: implement utf8foldernames option. [Urs Liska]
|
||||
- utf8: document new feature, deprecate old one. [Urs Liska]
|
||||
|
||||
#### Fixes
|
||||
|
||||
- remotehost should not be required if transporttunnel is used. [Thomas Merkel]
|
||||
- accounts: error out when no folder to sync. [Nicolas Sebrecht]
|
||||
- sqlite: provide better message error for insert. [Nicolas Sebrecht]
|
||||
- folder: Gmail: fix copyright header. [Nicolas Sebrecht]
|
||||
|
||||
#### Changes
|
||||
|
||||
- man: remove mention of experimental support for python 3. [Nicolas Sebrecht]
|
||||
- man: mention the supported directions of the syncs. [Nicolas Sebrecht]
|
||||
- folder: Gmail: remove dead code. [Nicolas Sebrecht]
|
||||
- upcoming.py: get header template from external file. [Nicolas Sebrecht]
|
||||
- upcoming.py: display a message with the filename once written. [Nicolas Sebrecht]
|
||||
- contrib/helpers: sort testers by name. [Nicolas Sebrecht]
|
||||
- Remove some unnecessary whitespace (in existing code). [Urs Liska]
|
||||
- MAINTAINERS: Rainer is not currently active. [Nicolas Sebrecht]
|
||||
|
||||
|
||||
### OfflineIMAP v7.1.2 (2017-07-10)
|
||||
|
||||
#### Notes
|
||||
|
||||
This release introduces better Davmail support, better reliability when in
|
||||
IMAP/IMAP mode, better output on some errors, and minor fixes. The provided
|
||||
systemd files are improved.
|
||||
|
||||
The imaplib2 requirement is now v2.57.
|
||||
|
||||
Remi Locherer is joining our tester team. Great!
|
||||
|
||||
Starting with this release, the feedbacks from the testers are recorded in the
|
||||
release notes, the git logs and the Changelog. Thanks to all of them for
|
||||
improving the releases.
|
||||
|
||||
This release was tested by:
|
||||
|
||||
- benutzer193
|
||||
- Nicolas Sebrecht
|
||||
- Remi Locherer
|
||||
|
||||
#### Authors
|
||||
|
||||
- Nicolas Sebrecht (20)
|
||||
- Hugo Osvaldo Barrera (5)
|
||||
- Alvaro Pereyra (1)
|
||||
- benutzer193 (1)
|
||||
|
||||
#### Features
|
||||
|
||||
- contrib/release.py: consider positive feedbacks from testers. [Nicolas Sebrecht]
|
||||
- Introduce the github CODEOWNERS file. [Nicolas Sebrecht]
|
||||
- IMAP/IMAP: continue to sync if the local side does not return a valid UID on upload. [Nicolas Sebrecht]
|
||||
|
||||
#### Fixes
|
||||
|
||||
- folder/IMAP: introduce dedicated parsing for davmail (not supporting UIDPLUS). [Nicolas Sebrecht]
|
||||
- offlineimap.conf: minor typo fix. [Alvaro Pereyra]
|
||||
- Respect systemd conventions for timers. [Hugo Osvaldo Barrera]
|
||||
- Use a pre-existing target for systemd services. [Hugo Osvaldo Barrera]
|
||||
- Remove invalid systemd setting. [Hugo Osvaldo Barrera]
|
||||
- systemd: remove unused watchdog functionality. [benutzer193]
|
||||
- gitignore generated css file. [Nicolas Sebrecht]
|
||||
- Changelog: fix syntax. [Nicolas Sebrecht]
|
||||
|
||||
#### Changes
|
||||
|
||||
- Increase imaplib2 requirement from v2.55 to v2.57. [Nicolas Sebrecht]
|
||||
- folder/IMAP: improve the warning when we can't parse the returned UID. [Nicolas Sebrecht]
|
||||
- Provide more details in error message when SSL fails on non-standard port. [Nicolas Sebrecht]
|
||||
- Use basic logger (since systemd picks up stdout). [Hugo Osvaldo Barrera]
|
||||
- Explain how to override systemd values. [Hugo Osvaldo Barrera]
|
||||
- systemd: add documentation entry in configuration files. [Nicolas Sebrecht]
|
||||
- offlineimap.conf: ssl must be disabled to force STARTTLS in some cases. [Nicolas Sebrecht]
|
||||
- Advise singlethreadperfolder when offlineimap hangs. [Nicolas Sebrecht]
|
||||
- offlineimap.conf: minor improvements. [Nicolas Sebrecht]
|
||||
- contrib: more release automation. [Nicolas Sebrecht]
|
||||
- MAINTAINERS: Remi Locherer joins the team of testers. [Nicolas Sebrecht]
|
||||
- systemd: README: credit Hugo as contributor. [Nicolas Sebrecht]
|
||||
|
||||
|
||||
### OfflineIMAP v7.1.1 (2017-05-28)
|
||||
|
||||
#### Notes
|
||||
|
||||
This release has some interesting fixes, including one for the Blinkenlights UI.
|
||||
|
||||
Otherwise, there is no big change since the previous version.
|
||||
|
||||
Furthermore, this release was tested by:
|
||||
|
||||
- Remi Locherer
|
||||
|
||||
#### Authors
|
||||
|
||||
- Nicolas Sebrecht (17)
|
||||
- Chris Coleman (1)
|
||||
- Ilias Tsitsimpis (1)
|
||||
- Maximilian Kaul (1)
|
||||
- benutzer193 (1)
|
||||
- Ævar Arnfjörð Bjarmason (1)
|
||||
|
||||
#### Features
|
||||
|
||||
- contrib: introduce a tool to produce the "upcoming notes". [Nicolas Sebrecht]
|
||||
- contrib: secure HTTPS test internet is connected.. [Chris Coleman]
|
||||
- Env info (used by -V and banner): add openssl version. [Nicolas Sebrecht]
|
||||
- docs: learn to build html files for the manual pages. [Nicolas Sebrecht]
|
||||
|
||||
#### Fixes
|
||||
|
||||
- Acquire lock before updating the CursesLogHandler window. [Ilias Tsitsimpis]
|
||||
- maxage: use the remote folder first to compute min_uid. [Nicolas Sebrecht]
|
||||
- Fix systemd.timer: initialize timer after boot. [benutzer193]
|
||||
- XOAUTH2: don't try this authentication method when not configured. [Nicolas Sebrecht]
|
||||
- mbnames: don't duplicate entries in autorefresh mode. [Nicolas Sebrecht]
|
||||
- docs: update the instructions for creating OAuth projects for GMail. [Ævar Arnfjörð Bjarmason]
|
||||
- Fixed typo in doc: tls_1_2 => tls1_2. [Maximilian Kaul]
|
||||
- IMAP: UIDPLUS: correctly warn about weird responses from some servers. [Nicolas Sebrecht]
|
||||
- website-doc: force copy of the new HTML generated man pages. [Nicolas Sebrecht]
|
||||
- Makefile: fix clean target. [Nicolas Sebrecht]
|
||||
|
||||
#### Changes
|
||||
|
||||
- MAINTAINERS: benutzer193 joins the testers team. [Nicolas Sebrecht]
|
||||
- IMAP: UIDPLUS: improve error message on response error for new UID. [Nicolas Sebrecht]
|
||||
- Display the imaplib and python versions for each normal run. [Nicolas Sebrecht]
|
||||
- imapserver: provide some SSL info while in imap debug mode. [Nicolas Sebrecht]
|
||||
- manual: improve the documentation about sqlite migration. [Nicolas Sebrecht]
|
||||
- documentation: add entry for faulting folders with Microsoft servers. [Nicolas Sebrecht]
|
||||
- website-doc.sh: add hint on API removal. [Nicolas Sebrecht]
|
||||
- README: refactorize sections. [Nicolas Sebrecht]
|
||||
|
||||
|
||||
|
||||
### OfflineIMAP v7.1.0 (2017-04-16)
|
||||
|
||||
#### Notes
|
||||
|
||||
The most important change is the removal of the status_backend configuration
|
||||
option and that's why we're moving to v7.1.0.
|
||||
|
||||
There are other small bug fixes and improvements. However, the codebase didn't
|
||||
change much since v7.0.14.
|
||||
|
||||
#### Authors
|
||||
|
||||
- Nicolas Sebrecht (6)
|
||||
- benutzer193 (4)
|
||||
- Ilias Tsitsimpis (1)
|
||||
|
||||
#### Fixes
|
||||
|
||||
- doc: Fix typo in offlineimap.1 man page. [Ilias Tsitsimpis]
|
||||
- README: we moved to imaplib2 v2.57. [Nicolas Sebrecht]
|
||||
- README: mark porting to py3 as stalled. [Nicolas Sebrecht]
|
||||
- folder: UIDMaps: ignore KeyError failure while removing keys. [Nicolas Sebrecht]
|
||||
|
||||
#### Changes
|
||||
|
||||
- Remove support for the status_backend configuration option. [Nicolas Sebrecht]
|
||||
- folder/IMAP: improve handling of "matchinguids" error while searching headers. [Nicolas Sebrecht]
|
||||
- Adjust README to systemd service file changes. [benutzer193]
|
||||
- Remove oneshot switch from systemd services. [benutzer193]
|
||||
- Use oneshot services for systemd timers. [benutzer193]
|
||||
- Create systemd oneshot services. [benutzer193]
|
||||
- website-doc.sh: versions.yml: set versions in order. [Nicolas Sebrecht]
|
||||
|
||||
|
||||
|
||||
### OfflineIMAP v7.0.14 (2017-03-11)
|
||||
|
||||
#### Notes
|
||||
|
||||
Here is a new small fixup release for the v7.0 series. The first v7.0.0 release
|
||||
is near to 8 months old. This v7.0.14 release is more reliable than v6.7.0.3.
|
||||
Hence, I'm deprecating the v6.7 series.
|
||||
|
||||
Now, you are all enjoined to migrate to v7.0.14. Migrating back to v6.7 is not
|
||||
supported so you might like to backup your local maildirs and metadata first.
|
||||
|
||||
We will fully remove the legacy text backend driver in near future. The SQLite
|
||||
driver proved to be better for both performance and reliability.
|
||||
|
||||
With this release we use imaplib2 v2.57 to support some faulting IMAP servers,
|
||||
fix a little bug about the backend migration to SQLite and serialize the sync
|
||||
processes to prevent from issues when both IDLE and autorefresh are enabled.
|
||||
|
||||
Happy sync'ing!
|
||||
|
||||
#### Authors
|
||||
|
||||
- Nicolas Sebrecht (5)
|
||||
- 927589452 (2)
|
||||
- Jens Heinrich (1)
|
||||
- Stéphane Graber (1)
|
||||
|
||||
#### Fixes
|
||||
|
||||
- SQLite: avoid concurrent writes on backend migration. [Nicolas Sebrecht]
|
||||
- Fix ipv6 configuration handling. [Stéphane Graber]
|
||||
- Prevent synchronization of identical folders from multiple threads. [Nicolas Sebrecht]
|
||||
|
||||
#### Changes
|
||||
|
||||
- Bump from imaplib2 v2.55 to v2.57. [Nicolas Sebrecht]
|
||||
- scripts/get-repository.sh: use portable /bin/sh. [Jens Heinrich]
|
||||
- MAINTAINERS: add new tester. [Nicolas Sebrecht]
|
||||
- scripts/get-repository.sh: use env to call bash. [mailinglists@927589452.de]
|
||||
|
||||
|
||||
|
||||
### OfflineIMAP v7.0.13 (2017-01-27)
|
||||
|
||||
#### Notes
|
||||
|
|
|
@ -4,53 +4,61 @@ Contacts
|
|||
========
|
||||
|
||||
- Abdó Roig-Maranges
|
||||
- email: abdo.roig at gmail.com
|
||||
- github: aroig
|
||||
- email: abdo.roig at gmail.com
|
||||
- github: aroig
|
||||
|
||||
- Ben Boeckel
|
||||
- email: mathstuf at gmail.com
|
||||
- github: mathstuf
|
||||
- email: mathstuf at gmail.com
|
||||
- github: mathstuf
|
||||
|
||||
- benutzer193
|
||||
- email: registerbn at gmail.com
|
||||
- github: benutzer193
|
||||
|
||||
- Chris Coleman
|
||||
- email: chris at espacenetworks.com
|
||||
- github: chris001
|
||||
|
||||
- Darshit Shah
|
||||
- email: darnir at gmail.com
|
||||
- github: darnir
|
||||
- email: darnir at gmail.com
|
||||
- github: darnir
|
||||
|
||||
- Eygene Ryabinkin
|
||||
- email: rea at freebsd.org
|
||||
- github: konvpalto
|
||||
- other: FreeBSD maintainer
|
||||
- email: rea at freebsd.org
|
||||
- github: konvpalto
|
||||
- other: FreeBSD maintainer
|
||||
|
||||
- Igor Almeida
|
||||
- email: igor.contato at gmail.com
|
||||
- github: igoralmeida
|
||||
- email: igor.contato at gmail.com
|
||||
- github: igoralmeida
|
||||
|
||||
- Ilias Tsitsimpis
|
||||
- email: i.tsitsimpis at gmail.com
|
||||
- github: iliastsi
|
||||
- other: Debian maintainer
|
||||
- email: i.tsitsimpis at gmail.com
|
||||
- github: iliastsi
|
||||
- other: Debian maintainer
|
||||
|
||||
- "J"
|
||||
- email: offlineimap at 927589452.de
|
||||
- other: FreeBSD user
|
||||
- email: offlineimap at 927589452.de
|
||||
- github: 927589452
|
||||
- other: FreeBSD user
|
||||
|
||||
- Łukasz Żarnowiecki
|
||||
- email: dolohow at outlook.com
|
||||
- github: dolohow
|
||||
- email: dolohow at outlook.com
|
||||
- github: dolohow
|
||||
|
||||
- Nicolas Sebrecht
|
||||
- email: nicolas.s-dev at laposte.net
|
||||
- github: nicolas33
|
||||
- system: Linux
|
||||
- email: nicolas.s-dev at laposte.net
|
||||
- github: nicolas33
|
||||
- system: Linux
|
||||
|
||||
- Rainer M Krug
|
||||
- email: Rainer at krugs.de
|
||||
- github: rkrug
|
||||
- system: OSX
|
||||
- Remi Locherer
|
||||
- email: remi.locherer at relo.ch
|
||||
- system: OpenBSD maintainer
|
||||
|
||||
- Sebastian Spaeth
|
||||
- email: sebastian at sspaeth.de
|
||||
- github: spaetz
|
||||
- other: left the project but still responding
|
||||
- email: sebastian at sspaeth.de
|
||||
- github: spaetz
|
||||
- other: left the project but still responding
|
||||
|
||||
|
||||
Testers
|
||||
|
@ -58,6 +66,7 @@ Testers
|
|||
|
||||
- Abdó Roig-Maranges
|
||||
- Ben Boeckel
|
||||
- Chris Coleman
|
||||
- Darshit Shah
|
||||
- Eygene Ryabinkin
|
||||
- Igor Almeida
|
||||
|
@ -65,7 +74,7 @@ Testers
|
|||
- "J"
|
||||
- Łukasz Żarnowiecki
|
||||
- Nicolas Sebrecht
|
||||
- Rainer M Krug
|
||||
- Remi Locherer
|
||||
|
||||
|
||||
Maintainers
|
||||
|
@ -74,6 +83,7 @@ Maintainers
|
|||
- Eygene Ryabinkin
|
||||
- Sebastian Spaeth
|
||||
- Nicolas Sebrecht
|
||||
- Chris Coleman
|
||||
|
||||
|
||||
Github
|
||||
|
|
19
Makefile
19
Makefile
|
@ -1,5 +1,4 @@
|
|||
# Copyright (C) 2002 - 2006 John Goerzen
|
||||
# <jgoerzen@complete.org>
|
||||
# Copyright (C) 2002 - 2018 John Goerzen & contributors.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -15,30 +14,32 @@
|
|||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
|
||||
# Warning: VERSION, ABBREV and TARGZ are used in docs/build-uploads.sh.
|
||||
VERSION=$(shell ./offlineimap.py --version)
|
||||
ABBREV=$(shell git log --format='%h' HEAD~1..)
|
||||
TARGZ=offlineimap-$(VERSION)-$(ABBREV)
|
||||
TARGZ=offlineimap-v$(VERSION)-$(ABBREV)
|
||||
SHELL=/bin/bash
|
||||
RST2HTML=`type rst2html >/dev/null 2>&1 && echo rst2html || echo rst2html.py`
|
||||
|
||||
all: build
|
||||
|
||||
build:
|
||||
python setup.py build
|
||||
python2 setup.py build
|
||||
@echo
|
||||
@echo "Build process finished, run 'python setup.py install' to install" \
|
||||
"or 'python setup.py --help' for more information".
|
||||
@echo "Build process finished, run 'python2 setup.py install' to install" \
|
||||
"or 'python2 setup.py --help' for more information".
|
||||
|
||||
clean:
|
||||
-python setup.py clean --all
|
||||
-python2 setup.py clean --all
|
||||
-rm -f bin/offlineimapc 2>/dev/null
|
||||
-find . -name '*.pyc' -exec rm -f {} \;
|
||||
-find . -name '*.pygc' -exec rm -f {} \;
|
||||
-find . -name '*.class' -exec rm -f {} \;
|
||||
-find . -name '.cache*' -exec rm -f {} \;
|
||||
-find . -type d -name '__pycache__' -exec rm -rf {} \;
|
||||
-rm -f manpage.links manpage.refs 2>/dev/null
|
||||
-find . -name auth -exec rm -vf {}/password {}/username \;
|
||||
@$(MAKE) -C clean
|
||||
-$(MAKE) -C docs clean
|
||||
|
||||
.PHONY: docs
|
||||
docs:
|
||||
|
@ -49,7 +50,7 @@ websitedoc:
|
|||
|
||||
targz: ../$(TARGZ)
|
||||
../$(TARGZ):
|
||||
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}
|
||||
cd .. && tar -zhcv --transform s,^offlineimap,offlineimap-v$(VERSION), -f $(TARGZ).tar.gz --exclude '.*.swp' --exclude '.*.swo' --exclude '*.pyc' --exclude '__pycache__' offlineimap/{bin,Changelog.md,Changelog.maint.md,contrib,CONTRIBUTING.rst,COPYING,docs,MAINTAINERS.rst,Makefile,MANIFEST.in,offlineimap,offlineimap.conf,offlineimap.conf.minimal,offlineimap.py,README.md,requirements.txt,scripts,setup.cfg,setup.py,snapcraft.yaml,test,tests,TODO.rst}
|
||||
|
||||
rpm: targz
|
||||
cd .. && sudo rpmbuild -ta $(TARGZ)
|
||||
|
|
93
README.md
93
README.md
|
@ -1,13 +1,30 @@
|
|||
|
||||
Financial contributors: [![Financial Contributors on Open Collective](https://opencollective.com/offlineimap-organization/all/badge.svg?label=financial+contributors)](https://opencollective.com/offlineimap-organization)
|
||||
|
||||
[offlineimap]: http://github.com/OfflineIMAP/offlineimap
|
||||
[offlineimap3]: http://github.com/OfflineIMAP/offlineimap3
|
||||
[website]: http://www.offlineimap.org
|
||||
[wiki]: http://github.com/OfflineIMAP/offlineimap/wiki
|
||||
[blog]: http://www.offlineimap.org/posts.html
|
||||
|
||||
Links:
|
||||
* Official github code repository: [offlineimap]
|
||||
* Website: [website]
|
||||
* Wiki: [wiki]
|
||||
* Blog: [blog]
|
||||
|
||||
# OfflineIMAP
|
||||
|
||||
***"Get the emails where you need them."***
|
||||
|
||||
[Official offlineimap][offlineimap].
|
||||
> IMPORTANT NOTE: This repository is for python2 only. The support for offlineimap3
|
||||
> is happening in [Official offlineimap for python3][offlineimap3].
|
||||
>
|
||||
> I'll still lazily maintain this legacy offlineimap but users should definitely go with
|
||||
> offlineimap3.
|
||||
|
||||
- [Official offlineimap for python3][offlineimap3].
|
||||
- [Official offlineimap for python2][offlineimap].
|
||||
|
||||
|
||||
## Description
|
||||
|
@ -15,6 +32,8 @@
|
|||
OfflineIMAP is software that downloads your email mailbox(es) as **local
|
||||
Maildirs**. OfflineIMAP will synchronize both sides via *IMAP*.
|
||||
|
||||
## Why should I use OfflineIMAP?
|
||||
|
||||
IMAP's main downside is that you have to **trust** your email provider to
|
||||
not lose your email. While certainly unlikely, it's not impossible.
|
||||
With OfflineIMAP, you can download your Mailboxes and make you own backups of
|
||||
|
@ -27,42 +46,56 @@ message without internet connection? No problem, 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 software in
|
||||
> development that I intend to replace OfflineIMAP with in the long term.
|
||||
>
|
||||
> That's why I'm not going to continue OfflineIMAP development. I'll continue
|
||||
> to maintain OfflineIMAP (fixing small bugs, reviewing patches and merging,
|
||||
> and rolling out new releases), but that's all.
|
||||
>
|
||||
> While I keep tracking issues for OfflineIMAP, you should not expect future support.
|
||||
>
|
||||
> 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. ,-)
|
||||
The [offlineimap][offlineimap] project was forked to
|
||||
[offlineimap3][offlineimap3] to support python3. Contributions are welcome to
|
||||
this project.
|
||||
|
||||
|
||||
## Contributors
|
||||
|
||||
### Code Contributors
|
||||
|
||||
This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)].
|
||||
<a href="https://github.com/OfflineIMAP/offlineimap/graphs/contributors"><img src="https://opencollective.com/offlineimap-organization/contributors.svg?width=890&button=false" /></a>
|
||||
|
||||
### Financial Contributors
|
||||
|
||||
Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/offlineimap-organization/contribute)]
|
||||
|
||||
#### Individuals
|
||||
|
||||
<a href="https://opencollective.com/offlineimap-organization"><img src="https://opencollective.com/offlineimap-organization/individuals.svg?width=890"></a>
|
||||
|
||||
#### Organizations
|
||||
|
||||
Support this project with your organization. Your logo will show up here with a link to your website. [[Contribute](https://opencollective.com/offlineimap-organization/contribute)]
|
||||
|
||||
<a href="https://opencollective.com/offlineimap-organization/organization/0/website"><img src="https://opencollective.com/offlineimap-organization/organization/0/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/offlineimap-organization/organization/1/website"><img src="https://opencollective.com/offlineimap-organization/organization/1/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/offlineimap-organization/organization/2/website"><img src="https://opencollective.com/offlineimap-organization/organization/2/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/offlineimap-organization/organization/3/website"><img src="https://opencollective.com/offlineimap-organization/organization/3/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/offlineimap-organization/organization/4/website"><img src="https://opencollective.com/offlineimap-organization/organization/4/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/offlineimap-organization/organization/5/website"><img src="https://opencollective.com/offlineimap-organization/organization/5/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/offlineimap-organization/organization/6/website"><img src="https://opencollective.com/offlineimap-organization/organization/6/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/offlineimap-organization/organization/7/website"><img src="https://opencollective.com/offlineimap-organization/organization/7/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/offlineimap-organization/organization/8/website"><img src="https://opencollective.com/offlineimap-organization/organization/8/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/offlineimap-organization/organization/9/website"><img src="https://opencollective.com/offlineimap-organization/organization/9/avatar.svg"></a>
|
||||
|
||||
## License
|
||||
|
||||
GNU General Public License v2.
|
||||
|
||||
|
||||
## Why should I use OfflineIMAP?
|
||||
|
||||
* It is **fast**.
|
||||
* It is **reliable**.
|
||||
* It is **flexible**.
|
||||
* It is **safe**.
|
||||
|
||||
|
||||
## Downloads
|
||||
|
||||
You should first check if your distribution already packages OfflineIMAP for you.
|
||||
Downloads releases as [tarball or zipball](https://github.com/OfflineIMAP/offlineimap/tags).
|
||||
|
||||
If you are running Linux Os, you can install offlineimap with:
|
||||
|
||||
- openSUSE `zypper in offlineimap`
|
||||
- Arch Linux `pacman -S offlineimap`
|
||||
- fedora `dnf install offlineimap`
|
||||
|
||||
## Feedbacks and contributions
|
||||
|
||||
|
@ -92,11 +125,15 @@ Bugs, issues and contributions can be requested to both the mailing list or the
|
|||
|
||||
## Requirements & dependencies
|
||||
|
||||
* Python v2.7+
|
||||
* Python v3.4+ ***(experimental: [see known issues](https://github.com/OfflineIMAP/offlineimap/issues?q=is%3Aissue+is%3Aopen+label%3APy3))***
|
||||
* Python v2.7.x
|
||||
* six (required)
|
||||
* imaplib2 >= 2.55 (optional)
|
||||
* rfc6555 (required)
|
||||
* imaplib2 >= 2.57 (optional)
|
||||
* gssapi (optional), for Kerberos authentication
|
||||
* portalocker (optional), if you need to run offlineimap in Cygwin for Windows
|
||||
|
||||
* Python v3: See the [offlineimap3][offlineimap3] fork of
|
||||
[offlineimap][offlineimap].
|
||||
|
||||
## Documentation
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
#!/usr/bin/env python
|
||||
#!/usr/bin/env python2
|
||||
# Startup from system-wide installation
|
||||
# Copyright (C) 2002 - 2009 John Goerzen
|
||||
# <jgoerzen@complete.org>
|
||||
# Copyright (C) 2002-2018 John Goerzen & contributors
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
|
|
@ -0,0 +1,327 @@
|
|||
"""
|
||||
|
||||
Put into Public Domain, by Nicolas Sebrecht.
|
||||
|
||||
Helpers for maintenance scripts.
|
||||
|
||||
"""
|
||||
|
||||
from os import chdir, makedirs, system, getcwd
|
||||
from os.path import expanduser
|
||||
import shlex
|
||||
from subprocess import check_output, check_call, CalledProcessError
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
FS_ENCODING = 'UTF-8'
|
||||
ENCODING = 'UTF-8'
|
||||
|
||||
MAILING_LIST = 'offlineimap-project@lists.alioth.debian.org'
|
||||
CACHEDIR = '.git/offlineimap-release'
|
||||
EDITOR = 'vim'
|
||||
MAILALIASES_FILE = expanduser('~/.mutt/mail_aliases')
|
||||
TESTERS_FILE = "{}/testers.yml".format(CACHEDIR)
|
||||
ME = "Nicolas Sebrecht <nicolas.s-dev@laposte.net>"
|
||||
|
||||
|
||||
def run(cmd):
|
||||
return check_output(cmd, timeout=5).rstrip()
|
||||
|
||||
def goTo(path):
|
||||
try:
|
||||
chdir(path)
|
||||
return True
|
||||
except FileNotFoundError:
|
||||
print("Could not find the '{}' directory in '{}'...".format(
|
||||
path, getcwd())
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
class Author(object):
|
||||
def __init__(self, name, count, email):
|
||||
self.name = name
|
||||
self.count = count
|
||||
self.email = email
|
||||
|
||||
def getName(self):
|
||||
return self.name
|
||||
|
||||
def getCount(self):
|
||||
return self.count
|
||||
|
||||
def getEmail(self):
|
||||
return self.email
|
||||
|
||||
|
||||
class Git(object):
|
||||
@staticmethod
|
||||
def getShortlog(ref):
|
||||
shortlog = ""
|
||||
|
||||
cmd = shlex.split("git shortlog --no-merges -n v{}..".format(ref))
|
||||
output = run(cmd).decode(ENCODING)
|
||||
|
||||
for line in output.split("\n"):
|
||||
if len(line) > 0:
|
||||
if line[0] != " ":
|
||||
line = " {}\n".format(line)
|
||||
else:
|
||||
line = " {}\n".format(line.lstrip())
|
||||
else:
|
||||
line = "\n"
|
||||
|
||||
shortlog += line
|
||||
|
||||
return shortlog
|
||||
|
||||
@staticmethod
|
||||
def add(files):
|
||||
cmd = shlex.split("git add -- {}".format(files))
|
||||
return run(cmd).decode(ENCODING)
|
||||
|
||||
@staticmethod
|
||||
def commit(msg):
|
||||
cmd = shlex.split("git commit -s -m '{}'".format(msg))
|
||||
return run(cmd).decode(ENCODING)
|
||||
|
||||
@staticmethod
|
||||
def tag(version):
|
||||
cmd = shlex.split("git tag -a 'v{}' -m 'v{}'".format(version, version))
|
||||
return run(cmd).decode(ENCODING)
|
||||
|
||||
@staticmethod
|
||||
def stash(msg):
|
||||
cmd = shlex.split("git stash create '{}'".format(msg))
|
||||
return run(cmd).decode(ENCODING)
|
||||
|
||||
@staticmethod
|
||||
def mergeFF(ref):
|
||||
cmd = shlex.split("git merge --ff '{}'".format(ref))
|
||||
return run(cmd).decode(ENCODING)
|
||||
|
||||
@staticmethod
|
||||
def getDiffstat(ref):
|
||||
cmd = shlex.split("git diff --stat v{}..".format(ref))
|
||||
return run(cmd).decode(ENCODING)
|
||||
|
||||
@staticmethod
|
||||
def isClean():
|
||||
try:
|
||||
check_call(shlex.split("git diff --quiet"))
|
||||
check_call(shlex.split("git diff --cached --quiet"))
|
||||
except CalledProcessError:
|
||||
return False
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def buildMessageId():
|
||||
cmd = shlex.split(
|
||||
"git log HEAD~1.. --oneline --pretty='%H.%t.upcoming.%ce'")
|
||||
return run(cmd).decode(ENCODING)
|
||||
|
||||
@staticmethod
|
||||
def resetKeep(ref):
|
||||
return run(shlex.split("git reset --keep {}".format(ref)))
|
||||
|
||||
@staticmethod
|
||||
def getRef(ref):
|
||||
return run(shlex.split("git rev-parse {}".format(ref))).rstrip()
|
||||
|
||||
@staticmethod
|
||||
def rmTag(tag):
|
||||
return run(shlex.split("git tag -d {}".format(tag)))
|
||||
|
||||
@staticmethod
|
||||
def checkout(ref, create=False):
|
||||
if create:
|
||||
create = "-b"
|
||||
else:
|
||||
create = ""
|
||||
|
||||
cmd = shlex.split("git checkout {} {}".format(create, ref))
|
||||
run(cmd)
|
||||
head = shlex.split("git rev-parse HEAD")
|
||||
revparseRef = shlex.split("git rev-parse {}".format(ref))
|
||||
if run(head) != run(revparseRef):
|
||||
raise Exception("checkout to '{}' did not work".format(ref))
|
||||
|
||||
@staticmethod
|
||||
def makeCacheDir():
|
||||
try:
|
||||
makedirs(CACHEDIR)
|
||||
except FileExistsError:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def getLocalUser():
|
||||
cmd = shlex.split("git config --get user.name")
|
||||
name = run(cmd).decode(ENCODING)
|
||||
cmd = shlex.split("git config --get user.email")
|
||||
email = run(cmd).decode(ENCODING)
|
||||
return name, email
|
||||
|
||||
@staticmethod
|
||||
def buildDate():
|
||||
cmd = shlex.split("git log HEAD~1.. --oneline --pretty='%cD'")
|
||||
return run(cmd).decode(ENCODING)
|
||||
|
||||
@staticmethod
|
||||
def getAuthorsList(sinceRef):
|
||||
authors = []
|
||||
|
||||
cmd = shlex.split("git shortlog --no-merges -sne v{}..".format(sinceRef))
|
||||
output = run(cmd).decode(ENCODING)
|
||||
|
||||
for line in output.split("\n"):
|
||||
count, full = line.strip().split("\t")
|
||||
full = full.split(' ')
|
||||
name = ' '.join(full[:-1])
|
||||
email = full[-1]
|
||||
|
||||
authors.append(Author(name, count, email))
|
||||
|
||||
return authors
|
||||
|
||||
@staticmethod
|
||||
def getCommitsList(sinceRef):
|
||||
cmd = shlex.split(
|
||||
"git log --no-merges --format='- %h %s. [%aN]' v{}..".format(sinceRef)
|
||||
)
|
||||
return run(cmd).decode(ENCODING)
|
||||
|
||||
@staticmethod
|
||||
def chdirToRepositoryTopLevel():
|
||||
cmd = shlex.split("git rev-parse --show-toplevel")
|
||||
topLevel = run(cmd)
|
||||
|
||||
chdir(topLevel)
|
||||
|
||||
|
||||
class OfflineimapInfo(object):
|
||||
def getVersion(self):
|
||||
cmd = shlex.split("./offlineimap.py --version")
|
||||
return run(cmd).rstrip().decode(FS_ENCODING)
|
||||
|
||||
def editVersion(self):
|
||||
return system("{} ./offlineimap/version.py".format(EDITOR))
|
||||
|
||||
|
||||
class User(object):
|
||||
"""Interact with the user."""
|
||||
|
||||
@staticmethod
|
||||
def request(msg, prompt='--> '):
|
||||
print(msg)
|
||||
return input(prompt)
|
||||
|
||||
@staticmethod
|
||||
def pause(msg=False):
|
||||
return User.request(msg, prompt="Press Enter to continue..")
|
||||
|
||||
@staticmethod
|
||||
def yesNo(msg, defaultToYes=False, prompt='--> '):
|
||||
endMsg = " [y/N]: No"
|
||||
if defaultToYes:
|
||||
endMsg = " [Y/n]: Yes"
|
||||
msg += endMsg
|
||||
answer = User.request(msg, prompt).lower()
|
||||
if answer in ['y', 'yes']:
|
||||
return True
|
||||
if defaultToYes is not False and answer not in ['n', 'no']:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class Tester(object):
|
||||
def __init__(self, name, email, feedback):
|
||||
self.name = name
|
||||
self.email = email
|
||||
self.feedback = feedback
|
||||
|
||||
def __str__(self):
|
||||
return "{} {}".format(self.name, self.email)
|
||||
|
||||
def getName(self):
|
||||
return self.name
|
||||
|
||||
def getEmail(self):
|
||||
return self.email
|
||||
|
||||
def getFeedback(self):
|
||||
return self.feedback
|
||||
|
||||
def positiveFeedback(self):
|
||||
return self.feedback is True
|
||||
|
||||
def setFeedback(self, feedback):
|
||||
assert feedback in [True, False, None]
|
||||
self.feedback = feedback
|
||||
|
||||
def switchFeedback(self):
|
||||
self.feedback = not self.feedback
|
||||
|
||||
|
||||
class Testers(object):
|
||||
def __init__(self):
|
||||
self.testers = None
|
||||
self._read()
|
||||
|
||||
def _read(self):
|
||||
self.testers = []
|
||||
with open(TESTERS_FILE, 'r') as fd:
|
||||
testers = yaml.safe_load(fd)
|
||||
for tester in testers:
|
||||
name = tester['name']
|
||||
email = tester['email']
|
||||
feedback = tester['feedback']
|
||||
self.testers.append(Tester(name, email, feedback))
|
||||
self.testers.sort(key=lambda x: x.getName().lower())
|
||||
|
||||
@staticmethod
|
||||
def listTestersInTeam():
|
||||
"""Returns a list of emails extracted from my mailaliases file."""
|
||||
|
||||
cmd = shlex.split("grep offlineimap-testers {}".format(MAILALIASES_FILE))
|
||||
output = run(cmd).decode(ENCODING)
|
||||
emails = output.lstrip("alias offlineimap-testers ").split(', ')
|
||||
return emails
|
||||
|
||||
def add(self, name, email, feedback=None):
|
||||
self.testers.append(Tester(name, email, feedback))
|
||||
|
||||
def remove(self, tester):
|
||||
self.testers.remove(tester)
|
||||
|
||||
def get(self):
|
||||
return self.testers
|
||||
|
||||
def getList(self):
|
||||
testersList = ""
|
||||
for tester in self.testers:
|
||||
testersList += "- {}\n".format(tester.getName())
|
||||
return testersList
|
||||
|
||||
def getListOk(self):
|
||||
testersOk = []
|
||||
for tester in self.testers:
|
||||
if tester.positiveFeedback():
|
||||
testersOk.append(tester)
|
||||
return testersOk
|
||||
|
||||
def reset(self):
|
||||
for tester in self.testers:
|
||||
tester.setFeedback(None)
|
||||
|
||||
def write(self):
|
||||
testers = []
|
||||
for tester in self.testers:
|
||||
testers.append({
|
||||
'name': tester.getName(),
|
||||
'email': tester.getEmail(),
|
||||
'feedback': tester.getFeedback(),
|
||||
})
|
||||
with open(TESTERS_FILE, 'w') as fd:
|
||||
fd.write(yaml.dump(testers))
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import urllib3
|
||||
import certifi
|
||||
|
||||
def isInternetConnected(url="www.ietf.org"):
|
||||
result = False
|
||||
http = urllib3.PoolManager(
|
||||
cert_reqs='CERT_REQUIRED', # Force certificate check.
|
||||
ca_certs=certifi.where(), # Path to the Certifi bundle.
|
||||
)
|
||||
try:
|
||||
r = http.request('HEAD', 'https://' + url)
|
||||
result = True
|
||||
except Exception as e: # urllib3.exceptions.SSLError
|
||||
result = False
|
||||
return result
|
||||
|
||||
print isInternetConnected()
|
|
@ -0,0 +1,477 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
"""
|
||||
|
||||
Put into Public Domain, by Nicolas Sebrecht.
|
||||
|
||||
Make a new release.
|
||||
|
||||
"""
|
||||
|
||||
#TODO: announce: cc list on announce includes all testers
|
||||
#TODO: announce: remove empty sections
|
||||
#TODO: websitedoc up
|
||||
#TODO: website branch not including all changes!
|
||||
|
||||
|
||||
from os import system, path, rename
|
||||
from datetime import datetime
|
||||
from subprocess import check_call
|
||||
import shlex
|
||||
import time
|
||||
from email import utils
|
||||
|
||||
from helpers import (
|
||||
MAILING_LIST, CACHEDIR, EDITOR, Git, OfflineimapInfo, Testers, User, run, goTo
|
||||
)
|
||||
|
||||
|
||||
__VERSION__ = "0.2"
|
||||
|
||||
SPHINXBUILD = 'sphinx-build'
|
||||
DOCSDIR = 'docs'
|
||||
CHANGELOG_MAGIC = '{:toc}'
|
||||
WEBSITE_LATEST = "website/_data/latest.yml"
|
||||
|
||||
CHANGELOG_EXCERPT = "{}/changelog.excerpt.md".format(CACHEDIR)
|
||||
CHANGELOG_EXCERPT_OLD = "{}.old".format(CHANGELOG_EXCERPT)
|
||||
CHANGELOG = "Changelog.md"
|
||||
ANNOUNCE_FILE = "{}/announce.txt".format(CACHEDIR)
|
||||
|
||||
WEBSITE_LATEST_SKEL = """# DO NOT EDIT MANUALLY: it is generated by the release script.
|
||||
stable: v{stable}
|
||||
"""
|
||||
|
||||
CHANGELOG_SKEL = """
|
||||
### OfflineIMAP v{version} ({date})
|
||||
|
||||
#### Notes
|
||||
|
||||
|
||||
This release was tested by:
|
||||
|
||||
{testersList}
|
||||
|
||||
#### Authors
|
||||
|
||||
{authorsList}
|
||||
|
||||
#### Features
|
||||
|
||||
|
||||
#### Fixes
|
||||
|
||||
|
||||
#### Changes
|
||||
|
||||
|
||||
|
||||
{commitsList}
|
||||
|
||||
"""
|
||||
|
||||
END_MESSAGE = """
|
||||
Release is ready!
|
||||
Make your checks and push the changes for both offlineimap and the website.
|
||||
Announce template stands in '{announce}'.
|
||||
Command samples to do manually:
|
||||
|
||||
- git push <remote> master next {new_version}
|
||||
- python2 setup.py sdist && twine upload dist/* && rm -rf dist MANIFEST
|
||||
- cd website
|
||||
- git checkout master
|
||||
- git merge {website_branch}
|
||||
- git push <remote> master
|
||||
- cd ..
|
||||
- git send-email {announce}
|
||||
|
||||
...and write a Twitter message.
|
||||
Have fun! ,-)
|
||||
"""
|
||||
|
||||
|
||||
class State(object):
|
||||
def __init__(self):
|
||||
self.master = None
|
||||
self.next = None
|
||||
self.website = None
|
||||
self.tag = None
|
||||
|
||||
def setTag(self, tag):
|
||||
self.tag = tag
|
||||
|
||||
def save(self):
|
||||
self.master = Git.getRef('master')
|
||||
self.next = Git.getRef('next')
|
||||
|
||||
def saveWebsite(self):
|
||||
Git.chdirToRepositoryTopLevel()
|
||||
goTo('website')
|
||||
self.website = Git.getRef('master')
|
||||
goTo('..')
|
||||
|
||||
def restore(self):
|
||||
Git.chdirToRepositoryTopLevel()
|
||||
try:
|
||||
Git.checkout('-f')
|
||||
except:
|
||||
pass
|
||||
# Git.checkout('master')
|
||||
# Git.resetKeep(self.master)
|
||||
# Git.checkout('next')
|
||||
# Git.resetKeep(self.next)
|
||||
|
||||
if self.tag is not None:
|
||||
Git.rmTag(self.tag)
|
||||
|
||||
if self.website is not None:
|
||||
if goTo('website'):
|
||||
Git.checkout(self.website)
|
||||
goTo('..')
|
||||
|
||||
|
||||
class Changelog(object):
|
||||
def __init__(self):
|
||||
self.shouldUsePrevious = False
|
||||
|
||||
def edit(self):
|
||||
return system("{} {}".format(EDITOR, CHANGELOG_EXCERPT))
|
||||
|
||||
def update(self):
|
||||
# Insert excerpt to CHANGELOG.
|
||||
system("sed -i -e '/{}/ r {}' '{}'".format(
|
||||
CHANGELOG_MAGIC, CHANGELOG_EXCERPT, CHANGELOG
|
||||
)
|
||||
)
|
||||
# Remove trailing whitespaces.
|
||||
system("sed -i -r -e 's, +$,,' '{}'".format(CHANGELOG))
|
||||
|
||||
def savePrevious(self):
|
||||
rename(CHANGELOG_EXCERPT, CHANGELOG_EXCERPT_OLD)
|
||||
|
||||
def isPrevious(self):
|
||||
if path.isfile(CHANGELOG_EXCERPT_OLD):
|
||||
return True
|
||||
return False
|
||||
|
||||
def showPrevious(self):
|
||||
output = run(shlex.split("cat '{}'".format(CHANGELOG_EXCERPT_OLD)))
|
||||
for line in output.splitlines():
|
||||
print(line.decode('utf-8')) # Weird to have to decode bytes here.
|
||||
|
||||
def usePrevious(self):
|
||||
rename(CHANGELOG_EXCERPT_OLD, CHANGELOG_EXCERPT)
|
||||
self.shouldUsePrevious = True
|
||||
|
||||
def usingPrevious(self):
|
||||
return self.shouldUsePrevious
|
||||
|
||||
def writeExcerpt(self, version, date,
|
||||
testersList, authorsList, commitsList):
|
||||
|
||||
with open(CHANGELOG_EXCERPT, 'w+') as fd:
|
||||
fd.write(CHANGELOG_SKEL.format(
|
||||
version=version,
|
||||
date=date,
|
||||
testersList=testersList,
|
||||
authorsList=authorsList,
|
||||
commitsList=commitsList,
|
||||
))
|
||||
|
||||
def getSectionsContent(self):
|
||||
dict_Content = {}
|
||||
|
||||
with open(CHANGELOG_EXCERPT, 'r') as fd:
|
||||
currentSection = None
|
||||
for line in fd:
|
||||
line = line.rstrip()
|
||||
if line == "#### Notes":
|
||||
currentSection = 'Notes'
|
||||
dict_Content['Notes'] = ""
|
||||
continue # Don't keep this title.
|
||||
elif line == "#### Authors":
|
||||
currentSection = 'Authors'
|
||||
dict_Content['Authors'] = ""
|
||||
continue # Don't keep this title.
|
||||
elif line == "#### Features":
|
||||
currentSection = 'Features'
|
||||
dict_Content['Features'] = ""
|
||||
continue # Don't keep this title.
|
||||
elif line == "#### Fixes":
|
||||
currentSection = 'Fixes'
|
||||
dict_Content['Fixes'] = ""
|
||||
continue # Don't keep this title.
|
||||
elif line == "#### Changes":
|
||||
currentSection = 'Changes'
|
||||
dict_Content['Changes'] = ""
|
||||
continue # Don't keep this title.
|
||||
elif line == "-- ":
|
||||
break # Stop extraction.
|
||||
|
||||
if currentSection is not None:
|
||||
dict_Content[currentSection] += "{}\n".format(line)
|
||||
|
||||
#TODO: cleanup empty sections.
|
||||
return dict_Content
|
||||
|
||||
|
||||
class Announce(object):
|
||||
def __init__(self, version):
|
||||
self.fd = open(ANNOUNCE_FILE, 'w')
|
||||
self.version = version
|
||||
|
||||
def setHeaders(self, messageId, date):
|
||||
self.fd.write("Message-Id: {}\n".format(messageId))
|
||||
self.fd.write("Date: {}\n".format(date))
|
||||
self.fd.write("From: Nicolas Sebrecht <nicolas.s-dev@laposte.net>\n")
|
||||
self.fd.write("To: {}\n".format(MAILING_LIST))
|
||||
self.fd.write(
|
||||
"Subject: [ANNOUNCE] OfflineIMAP v{} released\n".format(self.version))
|
||||
self.fd.write("\n")
|
||||
|
||||
self.fd.write("""
|
||||
OfflineIMAP v{version} is out.
|
||||
|
||||
Downloads:
|
||||
http://github.com/OfflineIMAP/offlineimap/archive/v{version}.tar.gz
|
||||
http://github.com/OfflineIMAP/offlineimap/archive/v{version}.zip
|
||||
|
||||
Pip:
|
||||
wget "https://raw.githubusercontent.com/OfflineIMAP/offlineimap/v{version}/requirements.txt" -O requirements.txt
|
||||
pip install -r ./requirements.txt --user git+https://github.com/OfflineIMAP/offlineimap.git@v{version}
|
||||
|
||||
""".format(version=self.version)
|
||||
)
|
||||
|
||||
def setContent(self, dict_Content):
|
||||
self.fd.write("\n")
|
||||
for section in ['Notes', 'Authors', 'Features', 'Fixes', 'Changes']:
|
||||
if section in dict_Content:
|
||||
if section != "Notes":
|
||||
self.fd.write("# {}\n".format(section))
|
||||
self.fd.write(dict_Content[section])
|
||||
self.fd.write("\n")
|
||||
# Signature.
|
||||
self.fd.write("-- \n")
|
||||
self.fd.write("Nicolas Sebrecht\n")
|
||||
|
||||
def close(self):
|
||||
self.fd.close()
|
||||
|
||||
|
||||
class Website(object):
|
||||
def updateUploads(self):
|
||||
req = ("add new archive to uploads/ on the website? "
|
||||
"(warning: checksums will change if it already exists)")
|
||||
if User.yesNo(req, defaultToYes=True) is False:
|
||||
return False
|
||||
if check_call(shlex.split("./docs/build-uploads.sh")) != 0:
|
||||
return exit(5)
|
||||
return True
|
||||
|
||||
def updateAPI(self):
|
||||
req = "update API of the website? (requires {})".format(SPHINXBUILD)
|
||||
if User.yesNo(req, defaultToYes=True) is False:
|
||||
return False
|
||||
|
||||
try:
|
||||
if check_call(shlex.split("{} --version".format(SPHINXBUILD))) != 0:
|
||||
raise RuntimeError("{} not found".format(SPHINXBUILD))
|
||||
except:
|
||||
print("""
|
||||
Oops! you don't have {} installed?"
|
||||
Cannot update the webite documentation..."
|
||||
You should install it and manually run:"
|
||||
$ cd {}"
|
||||
$ make websitedoc"
|
||||
Then, commit and push changes of the website.""".format(SPHINXBUILD, DOCSDIR))
|
||||
User.pause()
|
||||
return False
|
||||
|
||||
Git.chdirToRepositoryTopLevel()
|
||||
if not goTo('website'):
|
||||
User.pause()
|
||||
return False
|
||||
if not Git.isClean:
|
||||
print("There is WIP in the website repository: stashing")
|
||||
Git.stash('WIP during offlineimap API import')
|
||||
|
||||
goTo('..')
|
||||
return True
|
||||
|
||||
def buildLatest(self, version):
|
||||
Git.chdirToRepositoryTopLevel()
|
||||
with open(WEBSITE_LATEST, 'w') as fd:
|
||||
fd.write(WEBSITE_LATEST_SKEL.format(stable=version))
|
||||
|
||||
def exportDocs(self):
|
||||
if not goTo(DOCSDIR):
|
||||
User.pause()
|
||||
return
|
||||
|
||||
if check_call(shlex.split("make websitedoc")) != 0:
|
||||
print("error while calling 'make websitedoc'")
|
||||
exit(3)
|
||||
|
||||
def createImportBranch(self, version):
|
||||
branchName = "import-v{}".format(version)
|
||||
|
||||
Git.chdirToRepositoryTopLevel()
|
||||
if not goTo("website"):
|
||||
User.pause()
|
||||
return
|
||||
|
||||
Git.checkout(branchName, create=True)
|
||||
Git.add('.')
|
||||
Git.commit("update for offlineimap v{}".format(version))
|
||||
|
||||
User.pause(
|
||||
"website: branch '{}' is ready for a merge in master!".format(
|
||||
branchName
|
||||
)
|
||||
)
|
||||
goTo('..')
|
||||
return branchName
|
||||
|
||||
|
||||
class Release(object):
|
||||
def __init__(self):
|
||||
self.state = State()
|
||||
self.offlineimapInfo = OfflineimapInfo()
|
||||
self.testers = Testers()
|
||||
self.changelog = Changelog()
|
||||
self.websiteBranch = "NO_BRANCH_NAME_ERROR"
|
||||
|
||||
|
||||
def getVersion(self):
|
||||
return self.offlineimapInfo.getVersion()
|
||||
|
||||
def prepare(self):
|
||||
if not Git.isClean():
|
||||
print("The git repository is not clean; aborting")
|
||||
exit(1)
|
||||
Git.makeCacheDir()
|
||||
Git.checkout('next')
|
||||
|
||||
def requestVersion(self, currentVersion):
|
||||
User.request("going to make a new release after {}".format(currentVersion))
|
||||
|
||||
def updateVersion(self):
|
||||
self.offlineimapInfo.editVersion()
|
||||
|
||||
def checkVersions(self, current, new):
|
||||
if new == current:
|
||||
print("version was not changed; stopping.")
|
||||
exit(1)
|
||||
|
||||
def updateChangelog(self):
|
||||
if self.changelog.isPrevious():
|
||||
self.changelog.showPrevious()
|
||||
if User.yesNo("A previous Changelog excerpt was found. Use it?"):
|
||||
self.changelog.usePrevious()
|
||||
|
||||
if not self.changelog.usingPrevious():
|
||||
date = datetime.now().strftime('%Y-%m-%d')
|
||||
testersList = ""
|
||||
testers = self.testers.getListOk()
|
||||
authorsList = ""
|
||||
authors = Git.getAuthorsList(currentVersion)
|
||||
|
||||
for tester in testers:
|
||||
testersList += "- {}\n".format(tester.getName())
|
||||
for author in authors:
|
||||
authorsList += "- {} ({})\n".format(
|
||||
author.getName(), author.getCount()
|
||||
)
|
||||
commitsList = Git.getCommitsList(currentVersion)
|
||||
date = datetime.now().strftime('%Y-%m-%d')
|
||||
self.changelog.writeExcerpt(
|
||||
newVersion, date, testersList, authorsList, commitsList
|
||||
)
|
||||
|
||||
self.changelog.edit()
|
||||
self.changelog.update()
|
||||
|
||||
def writeAnnounce(self):
|
||||
announce = Announce(newVersion)
|
||||
|
||||
messageId = utils.make_msgid('release.py', 'laposte.net')
|
||||
nowtuple = datetime.now().timetuple()
|
||||
nowtimestamp = time.mktime(nowtuple)
|
||||
date = utils.formatdate(nowtimestamp)
|
||||
|
||||
announce.setHeaders(messageId, date)
|
||||
announce.setContent(self.changelog.getSectionsContent())
|
||||
announce.close()
|
||||
|
||||
def make(self):
|
||||
Git.add('offlineimap/version.py')
|
||||
Git.add('Changelog.md')
|
||||
commitMsg = "v{}\n".format(newVersion)
|
||||
for tester in self.testers.getListOk():
|
||||
commitMsg = "{}\nTested-by: {} {}".format(
|
||||
commitMsg, tester.getName(), tester.getEmail()
|
||||
)
|
||||
Git.commit(commitMsg)
|
||||
self.state.setTag(newVersion)
|
||||
Git.tag(newVersion)
|
||||
Git.checkout('master')
|
||||
Git.mergeFF('next')
|
||||
Git.checkout('next')
|
||||
|
||||
def updateWebsite(self, newVersion):
|
||||
self.state.saveWebsite()
|
||||
website = Website()
|
||||
website.buildLatest(newVersion)
|
||||
res_upload = website.updateUploads()
|
||||
res_api = website.updateAPI()
|
||||
if res_api:
|
||||
res_export = website.exportDocs()
|
||||
if True in [res_upload, res_api, res_export]:
|
||||
self.websiteBranch = website.createImportBranch(newVersion)
|
||||
|
||||
def getWebsiteBranch(self):
|
||||
return self.websiteBranch
|
||||
|
||||
def after(self):
|
||||
for protectedRun in [self.testers.reset, self.changelog.savePrevious]:
|
||||
try:
|
||||
protectedRun()
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
def restore(self):
|
||||
self.state.restore()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
release = Release()
|
||||
Git.chdirToRepositoryTopLevel()
|
||||
|
||||
try:
|
||||
release.prepare()
|
||||
currentVersion = release.getVersion()
|
||||
|
||||
release.requestVersion(currentVersion)
|
||||
release.updateVersion()
|
||||
newVersion = release.getVersion()
|
||||
|
||||
release.checkVersions(currentVersion, newVersion)
|
||||
release.updateChangelog()
|
||||
|
||||
release.writeAnnounce()
|
||||
User.pause()
|
||||
|
||||
release.make()
|
||||
release.updateWebsite(newVersion)
|
||||
release.after()
|
||||
|
||||
websiteBranch = release.getWebsiteBranch()
|
||||
print(END_MESSAGE.format(
|
||||
announce=ANNOUNCE_FILE,
|
||||
new_version=newVersion,
|
||||
website_branch=websiteBranch)
|
||||
)
|
||||
except Exception as e:
|
||||
release.restore()
|
||||
raise
|
|
@ -0,0 +1,37 @@
|
|||
# gpg-offlineimap
|
||||
|
||||
Python bindings for offlineimap to use gpg instead of storing cleartext passwords
|
||||
|
||||
Author: Lorenzo G.
|
||||
[GitHub](https://github.com/lorenzog/gpg-offlineimap)
|
||||
|
||||
## Quickstart
|
||||
|
||||
Requirements: a working GPG set-up. Ideally with gpg-agent. Should work
|
||||
out of the box on most modern Linux desktop environments.
|
||||
|
||||
1. Enable IMAP in gmail (if you have two factor authentication, you
|
||||
need to create an app-specific password)
|
||||
|
||||
2. Create a directory `~/Mail`
|
||||
|
||||
3. In `~/Mail`, create a password file `passwords-gmail.txt`. Format:
|
||||
`account@gmail.com password`. Look at the example file in this
|
||||
directory.
|
||||
|
||||
4. **ENCRYPT** the file: `gpg -e passwords-gmail.txt`. It should create
|
||||
a file `passwords-gmail.txt.gpg`. Check you can decrypt it: `gpg -d
|
||||
passwords-gmail.txt.gpg`: it will ask you for your GPG password and
|
||||
show it to you.
|
||||
|
||||
5. Use the file `offlineimaprc.sample` as a sample for your own
|
||||
`.offlineimaprc`; edit it by following the comments. Minimal items
|
||||
to configure: the `remoteuser` field and the `pythonfile` parameter
|
||||
pointing at the `offlineimap.py` file in this directory.
|
||||
|
||||
6. Run it: `offlineimap`. It should ask you for your GPG passphrase to
|
||||
decrypt the password file.
|
||||
|
||||
7. If all works well, delete the cleartext password file.
|
||||
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
#!/usr/bin/python
|
||||
# Originally taken from: http://stevelosh.com/blog/2012/10/the-homely-mutt/
|
||||
# by Steve Losh
|
||||
# Modified by Lorenzo Grespan on Jan, 2014
|
||||
|
||||
import re
|
||||
import subprocess
|
||||
from sys import argv
|
||||
import logging
|
||||
from os.path import expanduser
|
||||
import unittest
|
||||
import os
|
||||
import sys
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
|
||||
DEFAULT_PASSWORDS_FILE = os.path.join(
|
||||
os.path.expanduser('~/Mail'),
|
||||
'passwords.gpg')
|
||||
|
||||
|
||||
def get_keychain_pass(account=None, server=None):
|
||||
'''Mac OSX keychain password extraction'''
|
||||
params = {
|
||||
'security': '/usr/bin/security',
|
||||
'command': 'find-internet-password',
|
||||
'account': account,
|
||||
'server': server,
|
||||
'keychain': expanduser('~') + '/Library/Keychains/login.keychain',
|
||||
}
|
||||
command = ("%(security)s -v %(command)s"
|
||||
" -g -a %(account)s -s %(server)s %(keychain)s" % params)
|
||||
output = subprocess.check_output(
|
||||
command, shell=True, stderr=subprocess.STDOUT)
|
||||
outtext = [l for l in output.splitlines()
|
||||
if l.startswith('password: ')][0]
|
||||
return find_password(outtext)
|
||||
|
||||
|
||||
def find_password(text):
|
||||
'''Helper method for osx password extraction'''
|
||||
# a non-capturing group
|
||||
r = re.match(r'password: (?:0x[A-F0-9]+ )?"(.*)"', text)
|
||||
if r:
|
||||
return r.group(1)
|
||||
else:
|
||||
logging.warn("Not found")
|
||||
return None
|
||||
|
||||
|
||||
def get_gpg_pass(account, storage):
|
||||
'''GPG method'''
|
||||
command = ("gpg", "-d", storage)
|
||||
# get attention
|
||||
print '\a' # BEL
|
||||
output = subprocess.check_output(command)
|
||||
# p = subprocess.Popen(command, stdout=subprocess.PIPE)
|
||||
# output, err = p.communicate()
|
||||
for line in output.split('\n'):
|
||||
r = re.match(r'{} ([a-zA-Z0-9]+)'.format(account), line)
|
||||
if r:
|
||||
return r.group(1)
|
||||
return None
|
||||
|
||||
|
||||
def get_pass(account=None, server=None, passwd_file=None):
|
||||
'''Main method'''
|
||||
if not passwd_file:
|
||||
storage = DEFAULT_PASSWORDS_FILE
|
||||
else:
|
||||
storage = os.path.join(
|
||||
os.path.expanduser('~/Mail'),
|
||||
passwd_file)
|
||||
if os.path.exists('/usr/bin/security'):
|
||||
return get_keychain_pass(account, server)
|
||||
if os.path.exists(storage):
|
||||
logging.info("Using {}".format(storage))
|
||||
return get_gpg_pass(account, storage)
|
||||
else:
|
||||
logging.warn("No password file found")
|
||||
sys.exit(1)
|
||||
return None
|
||||
|
||||
|
||||
# test with: python -m unittest <this module name>
|
||||
# really basic tests.. nothing to see. move along
|
||||
class Tester(unittest.TestCase):
|
||||
def testMatchSimple(self):
|
||||
text = 'password: "exampleonetimepass "'
|
||||
self.assertTrue(find_password(text))
|
||||
|
||||
def testMatchComplex(self):
|
||||
text = r'password: 0x74676D62646D736B646970766C66696B0A "anotherexamplepass\012"'
|
||||
self.assertTrue(find_password(text))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print get_pass(argv[1], argv[2], argv[3])
|
|
@ -0,0 +1,63 @@
|
|||
[general]
|
||||
# GPG quirks, leave unconfigured
|
||||
ui = ttyui
|
||||
# you can use any name as long as it matches the 'account1, 'account2' in the rest
|
||||
# of the file
|
||||
accounts = account1, account2
|
||||
# this is where the `gpg-pw.py` file is on disk
|
||||
pythonfile=~/where/is/the/file/gpg-pw.py
|
||||
fsync = False
|
||||
|
||||
# you can call this any way you like
|
||||
[Account account1]
|
||||
localrepository = account1-local
|
||||
remoterepository = account1-remote
|
||||
# no need to touch this
|
||||
status_backend = sqlite
|
||||
|
||||
[Account account2]
|
||||
localrepository = account2-local
|
||||
remoterepository = account2-remote
|
||||
status_backend = sqlite
|
||||
|
||||
# thi sis a gmail account
|
||||
[Repository account1-local]
|
||||
type = Maildir
|
||||
# create with maildirmake or by hand by creating cur, new, tmp
|
||||
localfolders = ~/Mail/Mailboxes/account1
|
||||
# standard Gmail stuff
|
||||
nametrans = lambda folder: { 'drafts': '[Gmail]/Drafts',
|
||||
'sent': '[Gmail]/Sent mail',
|
||||
'flagged': '[Gmail]/Starred',
|
||||
'trash': '[Gmail]/Trash',
|
||||
'archive': '[Gmail]/All Mail'
|
||||
}.get(folder, folder)
|
||||
|
||||
[Repository account1-remote]
|
||||
maxconnections = 1
|
||||
type = Gmail
|
||||
ssl=yes
|
||||
# for osx, you might need to download the certs by hand
|
||||
#sslcacertfile=~/Mail/certs.pem
|
||||
#sslcacertfile=~/Mail/imap.gmail.com.pem
|
||||
# sslcacertfile=/etc/ssl/cert.pem
|
||||
|
||||
# or use Linux's standard certs
|
||||
sslcacertfile=/etc/ssl/certs/ca-certificates.crt
|
||||
# your account
|
||||
remoteuser = account1@gmail.com
|
||||
remotepasseval = get_pass(account="account1@gmail.com", server="imap.gmail.com", passwd_file="passwords-gmail.txt.gpg")
|
||||
realdelete = no
|
||||
createfolders = no
|
||||
nametrans = lambda folder: {'[Gmail]/Drafts': 'drafts',
|
||||
'[Gmail]/Sent Mail': 'sent',
|
||||
'[Gmail]/Starred': 'star',
|
||||
'[Gmail]/Trash': 'trash',
|
||||
'[Gmail]/All Mail': 'archive',
|
||||
}.get(folder, folder)
|
||||
folderfilter = lambda folder: folder not in ['[Gmail]/Trash',
|
||||
'[Gmail]/Spam',
|
||||
]
|
||||
|
||||
[Repository account2-remote]
|
||||
# copy the stanza above, change the 'account' parameter of get_pass, etc.
|
|
@ -0,0 +1,2 @@
|
|||
account1@gmail.com password1
|
||||
account2@gmail.com password2
|
|
@ -3,8 +3,8 @@ layout: page
|
|||
title: Integrating OfflineIMAP into systemd
|
||||
author: Ben Boeckel
|
||||
date: 2015-03-22
|
||||
contributors: Abdo Roig-Maranges
|
||||
updated: 2015-03-25
|
||||
contributors: Abdo Roig-Maranges, benutzer193, Hugo Osvaldo Barrera
|
||||
updated: 2017-06-01
|
||||
---
|
||||
|
||||
<!-- This file is copied to the website by script. -->
|
||||
|
@ -18,32 +18,10 @@ into `/etc/systemd/user` or `${XDG_DATA_HOME}/systemd/user` followed by
|
|||
|
||||
These files are meant to be triggered either manually using `systemctl --user
|
||||
start offlineimap.service` or by enabling the timer unit using `systemctl --user
|
||||
enable offlineimap.timer`. Additionally, specific accounts may be triggered by
|
||||
using `offlineimap@myaccount.timer` or `offlineimap@myaccount.service`.
|
||||
|
||||
These unit files are installed as being enabled via a `mail.target` unit which
|
||||
is intended to be a catch-all for mail-related unit files. A simple
|
||||
`mail.target` file is also provided.
|
||||
|
||||
## Signals
|
||||
|
||||
Systemd supports a watchdog (via the WatchdogSec service file option) which
|
||||
will send the program a SIGABRT when the timer expires.
|
||||
|
||||
Offlineimap handles it in the same manner as SIGUSR2, so that the current
|
||||
synchronisation is completed before the program exits safely.
|
||||
|
||||
This makes offlineimap more flexible and robust for persistent setups that make
|
||||
use of holdconnectionopen and autorefresh options.
|
||||
|
||||
For example, it may be useful in assisting with the occasional situation where
|
||||
offlineimap may not return successfully after a suspend and resume.
|
||||
|
||||
To make use of this, users could add the following to the [Service] section of
|
||||
their systemd offlineimap service file (restart every 5 minutes):
|
||||
|
||||
``` conf
|
||||
Restart=on-watchdog
|
||||
WatchdogSec=300
|
||||
```
|
||||
enable offlineimap-oneshot.timer`. Additionally, specific accounts may be
|
||||
triggered by using `offlineimap@myaccount.timer` or
|
||||
`offlineimap-oneshot@myaccount.service`.
|
||||
|
||||
If the defaults provided by these units doesn't suit your setup, any of the
|
||||
values may be overridden by using `systemctl --user edit offlineimap.service`.
|
||||
This'll prevent having to copy-and-edit the original file.
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
[Unit]
|
||||
Description=Mail Target
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
|
@ -0,0 +1,12 @@
|
|||
[Unit]
|
||||
Description=Offlineimap Service (oneshot)
|
||||
Documentation=man:offlineimap(1)
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/bin/offlineimap -o -u basic
|
||||
# Give 120 seconds for offlineimap to gracefully stop before hard killing it:
|
||||
TimeoutStopSec=120
|
||||
|
||||
[Install]
|
||||
WantedBy=mail.target
|
|
@ -2,8 +2,8 @@
|
|||
Description=Offlineimap Query Timer
|
||||
|
||||
[Timer]
|
||||
OnBootSec=1m
|
||||
OnUnitInactiveSec=15m
|
||||
Unit=offlineimap.service
|
||||
|
||||
[Install]
|
||||
WantedBy=mail.target
|
||||
WantedBy=default.target
|
|
@ -0,0 +1,12 @@
|
|||
[Unit]
|
||||
Description=Offlineimap Service for account %i (oneshot)
|
||||
Documentation=man:offlineimap(1)
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/bin/offlineimap -o -a %i -u basic
|
||||
# Give 120 seconds for offlineimap to gracefully stop before hard killing it.
|
||||
TimeoutStopSec=120
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
|
@ -2,8 +2,8 @@
|
|||
Description=Offlineimap Query Timer for account %i
|
||||
|
||||
[Timer]
|
||||
OnBootSec=1m
|
||||
OnUnitInactiveSec=15m
|
||||
Unit=offlineimap@%i.service
|
||||
|
||||
[Install]
|
||||
WantedBy=mail.target
|
||||
WantedBy=default.target
|
|
@ -1,13 +1,11 @@
|
|||
[Unit]
|
||||
Description=Offlineimap Service
|
||||
Documentation=man:offlineimap(1)
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/bin/offlineimap -o -u syslog
|
||||
# Give 12 seconds for offlineimap to gracefully stop before hard killing it.
|
||||
TimeoutStopSec=12
|
||||
#Restart=on-watchdog
|
||||
#WatchdogSec=300
|
||||
ExecStart=/usr/bin/offlineimap -u basic
|
||||
Restart=on-failure
|
||||
RestartSec=60
|
||||
|
||||
[Install]
|
||||
WantedBy=mail.target
|
||||
WantedBy=default.target
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
[Unit]
|
||||
Description=Offlineimap Service for account %i
|
||||
Documentation=man:offlineimap(1)
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/bin/offlineimap -o -a %i -u syslog
|
||||
#Restart=on-watchdog
|
||||
#WatchdogSec=300
|
||||
ExecStart=/usr/bin/offlineimap -a %i -u basic
|
||||
Restart=on-failure
|
||||
RestartSec=60
|
||||
|
||||
[Install]
|
||||
WantedBy=mail.target
|
||||
WantedBy=default.target
|
||||
|
|
|
@ -0,0 +1,142 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
"""
|
||||
|
||||
Put into Public Domain, by Nicolas Sebrecht.
|
||||
|
||||
Manage the feedbacks of the testers for the release notes.
|
||||
|
||||
"""
|
||||
|
||||
from os import system
|
||||
import argparse
|
||||
|
||||
from helpers import CACHEDIR, EDITOR, Testers, User, Git
|
||||
|
||||
|
||||
class App(object):
|
||||
def __init__(self):
|
||||
self.args = None
|
||||
self.testers = Testers()
|
||||
self.feedbacks = None
|
||||
|
||||
|
||||
def _getTestersByFeedback(self):
|
||||
if self.feedbacks is not None:
|
||||
return self.feedbacks
|
||||
|
||||
feedbackOk = []
|
||||
feedbackNo = []
|
||||
|
||||
for tester in self.testers.get():
|
||||
if tester.getFeedback() is True:
|
||||
feedbackOk.append(tester)
|
||||
else:
|
||||
feedbackNo.append(tester)
|
||||
|
||||
for array in [feedbackOk, feedbackNo]:
|
||||
array.sort(key=lambda t: t.getName())
|
||||
|
||||
self.feedbacks = feedbackOk + feedbackNo
|
||||
|
||||
def parseArgs(self):
|
||||
parser = argparse.ArgumentParser(description='Manage the feedbacks.')
|
||||
|
||||
parser.add_argument('--add', '-a', dest='add_tester',
|
||||
help='Add tester')
|
||||
parser.add_argument('--delete', '-d', dest='delete_tester',
|
||||
type=int,
|
||||
help='Delete tester NUMBER')
|
||||
parser.add_argument('--list', '-l', dest='list_all_testers',
|
||||
action='store_true',
|
||||
help='List the testers')
|
||||
parser.add_argument('--switchFeedback', '-s', dest='switch_feedback',
|
||||
action='store_true',
|
||||
help='Switch the feedback of a tester')
|
||||
|
||||
self.args = parser.parse_args()
|
||||
|
||||
def run(self):
|
||||
if self.args.list_all_testers is True:
|
||||
self.listTesters()
|
||||
if self.args.switch_feedback is True:
|
||||
self.switchFeedback()
|
||||
elif self.args.add_tester:
|
||||
self.addTester(self.args.add_tester)
|
||||
elif type(self.args.delete_tester) == int:
|
||||
self.deleteTester(self.args.delete_tester)
|
||||
|
||||
def addTester(self, strTester):
|
||||
try:
|
||||
splitted = strTester.split('<')
|
||||
name = splitted[0].strip()
|
||||
email = "<{}".format(splitted[1]).strip()
|
||||
except Exception as e:
|
||||
print(e)
|
||||
print("expected format is: 'Firstname Lastname <email>'")
|
||||
exit(2)
|
||||
self.testers.add(name, email)
|
||||
self.testers.write()
|
||||
|
||||
def deleteTester(self, number):
|
||||
self.listTesters()
|
||||
removed = self.feedbacks.pop(number)
|
||||
self.testers.remove(removed)
|
||||
|
||||
print("New list:")
|
||||
self.feedbacks = None
|
||||
self.listTesters()
|
||||
print("Removed: {}".format(removed))
|
||||
ans = User.request("Save on disk? (s/Q)").lower()
|
||||
if ans in ['s']:
|
||||
self.testers.write()
|
||||
|
||||
|
||||
def listTesters(self):
|
||||
self._getTestersByFeedback()
|
||||
|
||||
count = 0
|
||||
for tester in self.feedbacks:
|
||||
feedback = "ok"
|
||||
if tester.getFeedback() is not True:
|
||||
feedback = "no"
|
||||
print("{:02d} - {} {}: {}".format(
|
||||
count, tester.getName(), tester.getEmail(), feedback
|
||||
)
|
||||
)
|
||||
count += 1
|
||||
|
||||
def switchFeedback(self):
|
||||
self._getTestersByFeedback()
|
||||
msg = "Switch tester: [<number>/s/q]"
|
||||
|
||||
self.listTesters()
|
||||
number = User.request(msg)
|
||||
while number.lower() not in ['s', 'save', 'q', 'quit']:
|
||||
if number == '':
|
||||
continue
|
||||
try:
|
||||
number = int(number)
|
||||
self.feedbacks[number].switchFeedback()
|
||||
except (ValueError, IndexError) as e:
|
||||
print(e)
|
||||
exit(1)
|
||||
finally:
|
||||
self.listTesters()
|
||||
number = User.request(msg)
|
||||
if number in ['s', 'save']:
|
||||
self.testers.write()
|
||||
self.listTesters()
|
||||
|
||||
def reset(self):
|
||||
self.testers.reset()
|
||||
self.testers.write()
|
||||
|
||||
#def updateMailaliases(self):
|
||||
|
||||
if __name__ == '__main__':
|
||||
Git.chdirToRepositoryTopLevel()
|
||||
|
||||
app = App()
|
||||
app.parseArgs()
|
||||
app.run()
|
|
@ -0,0 +1,78 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
"""
|
||||
|
||||
Put into Public Domain, by Nicolas Sebrecht.
|
||||
|
||||
Produce the "upcoming release" notes.
|
||||
|
||||
"""
|
||||
|
||||
from os import system
|
||||
|
||||
from helpers import (
|
||||
MAILING_LIST, CACHEDIR, EDITOR, Testers, Git, OfflineimapInfo, User
|
||||
)
|
||||
|
||||
|
||||
|
||||
UPCOMING_FILE = "{}/upcoming.txt".format(CACHEDIR)
|
||||
UPCOMING_HEADER = "{}/upcoming-header.txt".format(CACHEDIR)
|
||||
|
||||
# Header is like:
|
||||
#
|
||||
#Message-Id: <{messageId}>
|
||||
#Date: {date}
|
||||
#From: {name} <{email}>
|
||||
#To: {mailinglist}
|
||||
#Cc: {ccList}
|
||||
#Subject: [ANNOUNCE] upcoming offlineimap v{expectedVersion}
|
||||
#
|
||||
## Notes
|
||||
#
|
||||
#I think it's time for a new release.
|
||||
#
|
||||
#I aim to make the new release in one week, approximately. If you'd like more
|
||||
#time, please let me know. ,-)
|
||||
#
|
||||
#Please, send me a mail to confirm it works for you. This will be written in the
|
||||
#release notes and the git logs.
|
||||
#
|
||||
#
|
||||
## Authors
|
||||
#
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
offlineimapInfo = OfflineimapInfo()
|
||||
|
||||
print("Will read headers from {}".format(UPCOMING_HEADER))
|
||||
Git.chdirToRepositoryTopLevel()
|
||||
oVersion = offlineimapInfo.getVersion()
|
||||
ccList = Testers.listTestersInTeam()
|
||||
authors = Git.getAuthorsList(oVersion)
|
||||
for author in authors:
|
||||
email = author.getEmail()
|
||||
if email not in ccList:
|
||||
ccList.append(email)
|
||||
|
||||
with open(UPCOMING_FILE, 'w') as upcoming, \
|
||||
open(UPCOMING_HEADER, 'r') as fd_header:
|
||||
header = {}
|
||||
|
||||
header['messageId'] = Git.buildMessageId()
|
||||
header['date'] = Git.buildDate()
|
||||
header['name'], header['email'] = Git.getLocalUser()
|
||||
header['mailinglist'] = MAILING_LIST
|
||||
header['expectedVersion'] = User.request("Expected new version?")
|
||||
header['ccList'] = ", ".join(ccList)
|
||||
|
||||
upcoming.write(fd_header.read().format(**header).lstrip())
|
||||
upcoming.write(Git.getShortlog(oVersion))
|
||||
|
||||
upcoming.write("\n\n# Diffstat\n\n")
|
||||
upcoming.write(Git.getDiffstat(oVersion))
|
||||
upcoming.write("\n\n\n-- \n{}\n".format(Git.getLocalUser()[0]))
|
||||
|
||||
system("{} {}".format(EDITOR, UPCOMING_FILE))
|
||||
print("{} written".format(UPCOMING_FILE))
|
|
@ -7,7 +7,7 @@ HTML_TARGETS = $(patsubst %.rst,%.html,$(SOURCES))
|
|||
RM = rm
|
||||
RST2HTML=`type rst2html >/dev/null 2>&1 && echo rst2html || echo rst2html.py`
|
||||
RST2MAN=`type rst2man >/dev/null 2>&1 && echo rst2man || echo rst2man.py`
|
||||
SPHINXBUILD = sphinx-build
|
||||
SPHINXBUILD = python2 -msphinx
|
||||
|
||||
docs: man api
|
||||
|
||||
|
@ -16,6 +16,15 @@ html: $(HTML_TARGETS)
|
|||
$(HTML_TARGETS): %.html : %.rst
|
||||
$(RST2HTML) $? $@
|
||||
|
||||
manhtml: offlineimap.html offlineimapui.html
|
||||
|
||||
offlineimap.html: offlineimap.txt offlineimap.known_issues.txt
|
||||
a2x -v -d manpage -D manhtml -f xhtml $<
|
||||
|
||||
offlineimapui.html: offlineimapui.txt
|
||||
a2x -v -d manpage -D manhtml -f xhtml $<
|
||||
|
||||
|
||||
man: offlineimap.1 offlineimapui.7
|
||||
|
||||
offlineimap.1: offlineimap.txt offlineimap.known_issues.txt
|
||||
|
@ -30,13 +39,15 @@ api:
|
|||
websitedoc:
|
||||
./website-doc.sh releases
|
||||
./website-doc.sh api
|
||||
./website-doc.sh html
|
||||
./website-doc.sh contrib
|
||||
|
||||
clean:
|
||||
$(RM) -f $(HTML_TARGETS)
|
||||
$(RM) -f offlineimap.1
|
||||
$(RM) -f offlineimap.7
|
||||
$(RM) -f manhtml/*
|
||||
$(RM) -rf html/*
|
||||
-find ./docs -name '*.html' -exec rm -f {} \;
|
||||
-find . -name '*.html' -exec rm -f {} \;
|
||||
|
||||
.PHONY: clean doc
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# vim: expandtab ts=2 :
|
||||
|
||||
WEBSITE_UPLOADS='./website/_uploads'
|
||||
|
||||
while true
|
||||
do
|
||||
test -d .git && break
|
||||
cd ..
|
||||
done
|
||||
|
||||
set -e
|
||||
|
||||
echo "make clean"
|
||||
make clean >/dev/null
|
||||
echo "make targz"
|
||||
make targz >/dev/null
|
||||
|
||||
# Defined in the root Makefile.
|
||||
version="$(./offlineimap.py --version)"
|
||||
abbrev="$(git log --format='%h' HEAD~1..)"
|
||||
targz="../offlineimap-v${version}-${abbrev}.tar.gz"
|
||||
|
||||
filename="offlineimap-v${version}.tar.gz"
|
||||
|
||||
mv -v "$targz" "${WEBSITE_UPLOADS}/${filename}"
|
||||
cd "$WEBSITE_UPLOADS"
|
||||
for digest in sha1 sha256 sha512
|
||||
do
|
||||
target="${filename}.${digest}"
|
||||
echo "Adding digest ${WEBSITE_UPLOADS}/${target}"
|
||||
"${digest}sum" "$filename" > "$target"
|
||||
done
|
|
@ -102,6 +102,11 @@ folder behind offlineimap's back, causing them to get assigned a new UID, or
|
|||
when offlineimap first syncs a pre-existing Maildir. In the latter case, it
|
||||
could appear as if a noticeable and random subset of old messages are synced.
|
||||
|
||||
* Offlineimap hangs.
|
||||
+
|
||||
When having unexpected hangs it's advised to set `singlethreadperfolder' to
|
||||
'yes', especially when in IMAP/IMAP mode (no maildir).
|
||||
|
||||
* Passwords in netrc.
|
||||
+
|
||||
Offlineimap doesn't know how to retrieve passwords when more than one account is
|
||||
|
@ -120,7 +125,7 @@ Sometimes, you might hit one of the following error:
|
|||
- oauth2handler got: {u'error': u'invalid_grant'}
|
||||
|
||||
+
|
||||
In such case, we had report that generating a new refesh token from the same
|
||||
In such case, we had reports that generating a new refresh token from the same
|
||||
client ID and secret can help.
|
||||
+
|
||||
.Google documentation on "invalid_grant"
|
||||
|
@ -151,3 +156,12 @@ See <https://developers.google.com/analytics/devguides/config/mgmt/v3/authorizat
|
|||
and <https://developers.google.com/identity/protocols/OAuth2#expiration>
|
||||
to know more.
|
||||
|
||||
* "does not have message with UID" with Microsoft servers
|
||||
+
|
||||
`ERROR: IMAP server 'Server ### Remote' does not have a message with UID 'xxx'`
|
||||
+
|
||||
Microsoft IMAP servers are not compliant with the RFC. It is currently required
|
||||
to folderfilter some faulting folders. See
|
||||
http://www.offlineimap.org/doc/FAQ.html#exchange-and-office365 for a detailed
|
||||
list.
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ offlineimap(1)
|
|||
|
||||
NAME
|
||||
----
|
||||
offlineimap - Synchronize mailboxes and Maildirs
|
||||
offlineimap - Synchronize mailboxes and Maildirs both ways or one either way.
|
||||
|
||||
SYNOPSIS
|
||||
--------
|
||||
|
@ -18,7 +18,7 @@ Synchronize the accounts configured in the configuration file via IMAP. Each
|
|||
account has two sides. One of the side must be an IMAP server. The other side
|
||||
can either be a Maildir or another IMAP server.
|
||||
|
||||
Python 3 is supported while still EXPERIMENTAL.
|
||||
Works with Python 2.
|
||||
|
||||
|
||||
OPTIONS
|
||||
|
@ -160,9 +160,10 @@ blinkenlights, machineui.
|
|||
--delete-folder::
|
||||
Delete a folder on the remote repository.
|
||||
+
|
||||
Only one account must be specified/configured for this feature to work. The
|
||||
folder name must be provided in IMAP encoding with the remote separators (likely
|
||||
'/'). E.g.: "Remote/folder/name".
|
||||
Only one account must be specified/configured for this feature to work or you
|
||||
must provide one account with -a. The folder name must be provided with the
|
||||
remote separators (likely '/') in UTF-8 if utf8foldernames is enabled or in IMAP
|
||||
otherwise. E.g.: "Remote/folder/name".
|
||||
|
||||
|
||||
--migrate-fmd5-using-nametrans::
|
||||
|
@ -225,17 +226,17 @@ done this yet, do it :). See the 'folderfilter' section in 'offlineimap.conf'.
|
|||
OfflineImap caches the state of the synchronisation to e.g. be able to determine
|
||||
if a mail has been added or deleted on either side.
|
||||
+
|
||||
The historical status cache is a plain text file that writes out the complete
|
||||
file for each single new message (or even changed flag) to a temporary file. If
|
||||
you have plenty of files in a folder, this is a few hundred kilo to megabytes
|
||||
for each mail and is bound to make things slow. The latest default status cache
|
||||
is sqlite. This saves plenty of disk activity. The sqlite engine and the Python
|
||||
sqlite module must be installed. Enable the 'status_backend = plain' setting in
|
||||
'offlineimap.conf' for legacy compatibility with versions prior to '6.4.0'.
|
||||
The historical status cache was a plain text file that was writing out the
|
||||
complete file for each single new message (or even changed flag) to a temporary
|
||||
file. If there was plenty of files in a folder this was bound to make things
|
||||
slow. The latest status cache is sqlite. This saves plenty of disk activity.
|
||||
+
|
||||
If you switch the backend from plain to sqlite, you may want to delete the old
|
||||
cache directory in '<metadata>/Account-<account>/LocalStatus' manually (the
|
||||
sqlite cache stands in the 'LocalStatus-sqlite' folder).
|
||||
The historical plain status cache is not supported anymore but migrating from a
|
||||
very old installation using the plain text cache is still supported. In this
|
||||
case, you may want to delete the old cache directory in
|
||||
'<metadata>/Account-<account>/LocalStatus' manually (the sqlite cache stands in
|
||||
the 'LocalStatus-sqlite' folder). First, make sure you have run the new version
|
||||
of offlineimap for all your accounts so that the status cache was migrated.
|
||||
|
||||
4. Use quick sync.
|
||||
+
|
||||
|
|
|
@ -11,6 +11,7 @@ DOCBASE="${WEBSITE}/_doc"
|
|||
DESTBASE="${DOCBASE}/versions"
|
||||
VERSIONS_YML="${WEBSITE}/_data/versions.yml"
|
||||
ANNOUNCES_YML="${WEBSITE}/_data/announces.yml"
|
||||
ANNOUNCES_YML_LIMIT=31
|
||||
ANNOUNCES_YML_TMP="${ANNOUNCES_YML}.tmp"
|
||||
CONTRIB_YML="${WEBSITE}/_data/contribs.yml"
|
||||
CONTRIB="${DOCBASE}/contrib"
|
||||
|
@ -54,11 +55,17 @@ function api () {
|
|||
# This let know the website about the available APIs documentations.
|
||||
echo "Building Jekyll data: $VERSIONS_YML"
|
||||
# Erase previous content.
|
||||
echo "$HEADER" > "$VERSIONS_YML"
|
||||
echo > "$VERSIONS_YML" <<EOF
|
||||
$HEADER
|
||||
# Used to publish the APIs.
|
||||
#
|
||||
# However, it's correct to _remove_ old API docs here. In this case, don't
|
||||
# forget to adjust the _doc/versions directory too.
|
||||
EOF
|
||||
for version in $(ls "$DESTBASE" -1 | sort -nr)
|
||||
do
|
||||
echo "- $version"
|
||||
done >> "$VERSIONS_YML"
|
||||
done | sort -V >> "$VERSIONS_YML"
|
||||
}
|
||||
|
||||
|
||||
|
@ -120,10 +127,19 @@ function releases () {
|
|||
d="$(parse_releases_get_date "$title")"
|
||||
echo "- {date: '${d}', version: '${v}', link: 'Changelog.maint.html#${link}'}"
|
||||
done | tee -a "$ANNOUNCES_YML_TMP"
|
||||
sort -nr "$ANNOUNCES_YML_TMP" >> "$ANNOUNCES_YML"
|
||||
sort -nr "$ANNOUNCES_YML_TMP" | head -n $ANNOUNCES_YML_LIMIT >> "$ANNOUNCES_YML"
|
||||
rm -f "$ANNOUNCES_YML_TMP"
|
||||
}
|
||||
|
||||
function manhtml () {
|
||||
set -e
|
||||
|
||||
cd ./docs
|
||||
make manhtml
|
||||
cd ..
|
||||
cp -afv ./docs/manhtml/* "$DOCBASE"
|
||||
}
|
||||
|
||||
|
||||
exit_code=0
|
||||
test "n$ARGS" = 'n' && ARGS='usage' # no option passed
|
||||
|
@ -137,6 +153,9 @@ do
|
|||
"napi")
|
||||
api
|
||||
;;
|
||||
"nhtml")
|
||||
manhtml
|
||||
;;
|
||||
"ncontrib")
|
||||
contrib
|
||||
;;
|
||||
|
|
182
offlineimap.conf
182
offlineimap.conf
|
@ -77,14 +77,16 @@ accounts = Test
|
|||
|
||||
# This option stands in the [general] section.
|
||||
#
|
||||
# Offlineimap can synchronize more than one account at a time. If you want to
|
||||
# enable this feature, set the below value to something greater than 1. To
|
||||
# force it to synchronize only one account at a time, set it to 1.
|
||||
# Offlineimap can synchronize more than one account at a time. There are two
|
||||
# ways to sync accounts concurrently:
|
||||
#
|
||||
# NOTE: if you are using autorefresh and have more than one account, you must
|
||||
# set this number to be >= to the number of accounts you have; since any given
|
||||
# sync run never "finishes" due to a timer, you will never sync your additional
|
||||
# accounts if this is 1.
|
||||
# 1. By running one offlineimap instance for each account (with the -a CLI
|
||||
# option). This is the recommended way. In this case, keep the following option
|
||||
# to 1.
|
||||
#
|
||||
# 2. By allowing offlineimap to sync more than one account for an instance (not
|
||||
# recommended). In this case, set the maxsyncaccounts option to a value greater
|
||||
# than 1.
|
||||
#
|
||||
#maxsyncaccounts = 1
|
||||
|
||||
|
@ -228,6 +230,9 @@ accounts = Test
|
|||
# return foldername in allowed[accountname]
|
||||
# return False
|
||||
#
|
||||
# For correct folderfilter with Microsoft servers, please see
|
||||
# http://www.offlineimap.org/doc/FAQ.html#exchange-and-office365
|
||||
#
|
||||
#folderfilter = mbnames_folderfilter
|
||||
|
||||
|
||||
|
@ -270,8 +275,13 @@ remoterepository = RemoteExample
|
|||
# your mail periodically. If you want that, specify how frequently to do that
|
||||
# (in minutes) here. Fractional minutes (ie, 3.25) is allowed.
|
||||
#
|
||||
# If you want more than one account concurrently synced in this mode, don't
|
||||
# forget to set the maxsyncaccounts option accordingly.
|
||||
# If you want more than one account concurrently synced in one instance of
|
||||
# offlineimap (not recommended), don't forget to increase the maxsyncaccounts
|
||||
# option accordingly.
|
||||
#
|
||||
# NOTE: If you run systemd it's recommended to not enable this option and use
|
||||
# the systemd timer instead (see the ./contrib/systemd/ directory in the
|
||||
# repository).
|
||||
#
|
||||
#autorefresh = 5
|
||||
|
||||
|
@ -314,14 +324,6 @@ remoterepository = RemoteExample
|
|||
#postsynchook = notifysync.sh
|
||||
|
||||
|
||||
# This option stands in the [Account Test] section.
|
||||
#
|
||||
# The historical backend is 'plain' which writes out the state in plain text
|
||||
# files. See manual.
|
||||
#
|
||||
#status_backend = sqlite
|
||||
|
||||
|
||||
# This option stands in the [Account Test] section.
|
||||
#
|
||||
# If you have a limited amount of bandwidth available you can exclude larger
|
||||
|
@ -363,7 +365,7 @@ remoterepository = RemoteExample
|
|||
# This option stands in the [Account Test] section.
|
||||
#
|
||||
# Maildir file format uses colon (:) separator between uniq name and info.
|
||||
# Unfortunatelly colon is not allowed character in windows file name. If you
|
||||
# Unfortunately colon is not allowed character in windows file name. If you
|
||||
# enable maildir-windows-compatible option, Offlineimap will be able to store
|
||||
# messages on windows drive, but you will probably loose compatibility with
|
||||
# other programs working with the maildir.
|
||||
|
@ -418,7 +420,7 @@ remoterepository = RemoteExample
|
|||
#
|
||||
# This knob is respected only by IMAP-based accounts. Value of labelsheader
|
||||
# for GMail-based accounts is automatically added to this list, you don't
|
||||
# need to specify it explicitely.
|
||||
# need to specify it explicitly.
|
||||
#
|
||||
# Use ASCII characters only.
|
||||
#
|
||||
|
@ -427,7 +429,7 @@ remoterepository = RemoteExample
|
|||
|
||||
# This option stands in the [Account Test] section.
|
||||
#
|
||||
# Use proxy connection for this account. Usefull to bypass the GFW in China.
|
||||
# Use proxy connection for this account. Useful to bypass the GFW in China.
|
||||
# To specify a proxy connection, join proxy type, host and port with colons.
|
||||
# Available proxy types are SOCKS5, SOCKS4, HTTP.
|
||||
# You also need to install PySocks through pip.
|
||||
|
@ -437,6 +439,55 @@ remoterepository = RemoteExample
|
|||
#proxy = SOCKS5:IP:9999
|
||||
|
||||
|
||||
# EXPERIMENTAL: This option stands in the [Account Test] section.
|
||||
#
|
||||
# IMAP defines an encoding for non-ASCII ("international") characters, and most
|
||||
# IMAP servers store folder names in this encoding. Note that the IMAP 4rev1
|
||||
# specification (RFC 3501) allows both UTF-8 and modified UTF-7 folder names
|
||||
# so it is *possible* that an IMAP server already uses UTF-8 encoded folder
|
||||
# names. But usually Folders that are shown as, say, "Gäste" will be represented
|
||||
# as "G&AOQ-ste", and by default will be synchronized like this by offlineIMAP.
|
||||
#
|
||||
# This option converts IMAP folder names from IMAP4-UTF-7 to UTF-8 and back
|
||||
# in order to have nicely readable UTF-8 folder names in the local copy.
|
||||
#
|
||||
# WARNING: with this option enabled:
|
||||
# - compatibility with any other version is NOT GUARANTEED (including newer);
|
||||
# - existing set-ups will probably break.
|
||||
# - no support is provided.
|
||||
#
|
||||
# IMPORTANT: READ THIS SECTION if you intend to enable this feature for an
|
||||
# EXISTING ACCOUNT that has already been synchronized!
|
||||
# Enabling UTF-8 encoded folder names will change many things on the local
|
||||
# repository of an account, so you really have to create a new local repository
|
||||
# and review the configuration. The least that would happen otherwise is a
|
||||
# duplication of all folders containing non-ASCII characters.
|
||||
# But also the following functionality may change, so the configuration in the
|
||||
# remote repository configuration has to be reviewed/updated:
|
||||
# - decodefoldernames
|
||||
# This option is replaced by utf8foldernames and must be removed
|
||||
# If both utf8foldernames and decodefoldernames are enabled the synchronization
|
||||
# for the given account is aborted before doing any changes.
|
||||
# - nametrans
|
||||
# With utf8foldernames enabled any nametrans function will operate on the
|
||||
# UTF-8 encoded folder names, while even with decodefoldernames enabled they
|
||||
# operate on the original IMAP4-UTF-7 encoded names.
|
||||
# - folderfilter
|
||||
# Folder filters still work on the untranslated names before applying a
|
||||
# nametrans function, but still this operates on the UTF-8 encoded names.
|
||||
# - folderincludes
|
||||
# With utf8foldernames enabled this function expects UTF-8 encoded folder
|
||||
# names.
|
||||
# - foldersort
|
||||
# With utf8foldernames enabled the folder names passed to the sorting routine
|
||||
# will be the UTF encoded names.
|
||||
# - idlefolders
|
||||
# With utf8foldernames enabled folders passed to this function are expected to
|
||||
# be UTF-8 encoded.
|
||||
#
|
||||
#utf8foldernames = no
|
||||
|
||||
|
||||
# TESTING: This option stands in the [Account Test] section.
|
||||
#
|
||||
# Use authproxy connection for this account. Useful to bypass the GFW in China.
|
||||
|
@ -538,20 +589,19 @@ localfolders = ~/Test
|
|||
|
||||
# This option stands in the [Repository LocalExample] section.
|
||||
#
|
||||
# This option is similar to "utime_from_header" and could be use as a
|
||||
# This option is similar to "utime_from_header" and could be used 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.
|
||||
# the filename to sort your boxes.
|
||||
#
|
||||
# If set to "yes" the file name prefix if build on the message "Date" header
|
||||
# If set to "yes" the filename prefix is built from 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.
|
||||
# date is used. Now you can quickly sort your messages using their filenames.
|
||||
#
|
||||
# Used in combination with "utime_from_header" all your message would be in
|
||||
# order with the correct mtime attribute.
|
||||
|
@ -648,13 +698,14 @@ remotehost = examplehost
|
|||
# This option stands in the [Repository RemoteExample] section.
|
||||
#
|
||||
# Whether or not to use STARTTLS. STARTTLS allows to upgrade a plain connection
|
||||
# to TLS or SSL after negociation with the server. While a server might pretend
|
||||
# to TLS or SSL after negotiation with the server. While a server might pretend
|
||||
# to support STARTTLS, the communication might not be properly established or
|
||||
# the secure tunnel might be broken in some way. In this case you might want to
|
||||
# disable STARTTLS. Unless you hit issues with STARTTLS, you are strongly
|
||||
# encouraged to keep STARTTLS enabled.
|
||||
#
|
||||
# STARTTLS can be used even if the 'ssl' option is disabled.
|
||||
# STARTTLS can be used even if the 'ssl' option is disabled. If you want to
|
||||
# _force_ STARTTLS, you might need to disable 'ssl'.
|
||||
#
|
||||
# Default is yes.
|
||||
#
|
||||
|
@ -735,10 +786,14 @@ remotehost = examplehost
|
|||
#
|
||||
# In Windows, Microsoft uses the term "thumbprint" instead of "fingerprint".
|
||||
#
|
||||
# Fingerprints must be in hexadecimal form without leading '0x':
|
||||
# 40 hex digits like bbfe29cf97acb204591edbafe0aa8c8f914287c9.
|
||||
# Supported fingerprint hashes are sha512, sha384, sha256, sha224 and sha1.
|
||||
# Fingerprints must be in hexadecimal form without leading '0x', and may have
|
||||
# the separating colons. This is non case-sensitive.
|
||||
# Examples:
|
||||
# sha1 "bbfe29cf97acb204591edbafe0aa8c8f914287c9".
|
||||
# sha1 with colons "BB:FE:29:CF:97:AC:B2:04:59:1E:DB:AF:E0:AA:8C:8F:91:42:87:C9"
|
||||
#
|
||||
#cert_fingerprint = <SHA1_of_server_certificate_here>[, <another_SHA1>]
|
||||
#cert_fingerprint = <SHAn_of_server_certificate_here>[, <another_SHAm>]
|
||||
|
||||
|
||||
# This option stands in the [Repository RemoteExample] section.
|
||||
|
@ -747,7 +802,7 @@ remotehost = examplehost
|
|||
#
|
||||
# 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
|
||||
# particular version from: tls1, tls1_1, tls_1_2, ssl3, ssl23.
|
||||
# particular version from: tls1, tls1_1, tls1_2, ssl3, ssl23.
|
||||
#
|
||||
# tls1_1 and tls1_2 are available with OpenSSL since v1.0.1.
|
||||
#
|
||||
|
@ -788,8 +843,8 @@ remotehost = examplehost
|
|||
# - ssl3 (less desirable than tls1)
|
||||
# - ssl23 (can fallback up to ssl3)
|
||||
#
|
||||
# When tls_level is not set to tls_compat, the ssl_version configuration option
|
||||
# must be explicitly set.
|
||||
# When tls_level is not set to tls_compat and ssl is still enabled,
|
||||
# the ssl_version configuration option must be explicitly set.
|
||||
#
|
||||
#tls_level = tls_compat
|
||||
|
||||
|
@ -849,7 +904,7 @@ remoteuser = username
|
|||
# For Gmail (and maybe others), XOAUTH2 requires ssl. This means that STARTTLS
|
||||
# won't work and that Offlineimap will perform certificate validation. IOW, the
|
||||
# following configuration is used:
|
||||
# - sslcacertfile: MUST BE correclty configured
|
||||
# - sslcacertfile: MUST BE correctly configured
|
||||
# - ssl = yes (optional, will be used anyway)
|
||||
# - starttls = no (optional, will be tried but won't work anyway)
|
||||
#
|
||||
|
@ -861,15 +916,25 @@ remoteuser = username
|
|||
# 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
|
||||
# Here's how to register an OAuth2 client for Gmail, as of 2017-05-15:
|
||||
# - Go to the Gmail API overview console
|
||||
# https://console.developers.google.com/apis/api/gmail.googleapis.com/overview
|
||||
# - Create a new project, name doesn't matter, e.g. 'gmail-sync-bob'
|
||||
# - 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
|
||||
# - Once created, click 'Enable'
|
||||
# - Click 'Create credentials' in the enabled API overview
|
||||
|
||||
# - In 'Add credentials to your project' select 'Gmail API' as the
|
||||
# API type, and 'Other UI ...' (not 'Other non-UI ...') for
|
||||
# 'Where will you be calling the API from?'. For 'What data will
|
||||
# you be accessing?' select 'User data'.
|
||||
# - Click 'What credentials do I need?'
|
||||
# - Create an arbitrary 'Create an OAuth 2.0 client ID',
|
||||
# e.g. 'gmail-sync-bob-client'. For 'Set up the OAuth 2.0 consent
|
||||
# screen' select an arbitrary 'Product name shown to users',
|
||||
# e.g. 'gmail-sync-bob-client' & click 'Continue'.
|
||||
# - This gives you your client ID displayed on the screen. Click
|
||||
# 'Download' to get a JSON file that also has the client secret.
|
||||
#
|
||||
#oauth2_client_id = YOUR_CLIENT_ID
|
||||
#oauth2_client_secret = YOUR_CLIENT_SECRET
|
||||
|
@ -935,14 +1000,14 @@ remoteuser = username
|
|||
#
|
||||
# 4. With a preauth tunnel. With this method, you invoke an external
|
||||
# program that is guaranteed *NOT* to ask for a password, but rather
|
||||
# to read from stdin and write to stdout an IMAP procotol stream that
|
||||
# to read from stdin and write to stdout an IMAP protocol stream that
|
||||
# begins life in the PREAUTH state. When you use a tunnel, you do
|
||||
# NOT specify a user or password (if you do, they'll be ignored.)
|
||||
# Instead, you specify a preauthtunnel, as this example illustrates
|
||||
# for Courier IMAP on Debian:
|
||||
#preauthtunnel = ssh -q imaphost '/usr/bin/imapd ./Maildir'
|
||||
#
|
||||
# 5. If you are using Kerberos and have the Python Kerberos package
|
||||
# 5. If you are using Kerberos and have the Python gssapi package
|
||||
# installed, you should not specify a remotepass. If the user has a
|
||||
# valid Kerberos TGT, Offlineimap will figure out the rest all by
|
||||
# itself, and fall back to password authentication if needed.
|
||||
|
@ -990,7 +1055,7 @@ remoteuser = username
|
|||
#
|
||||
# This is most commonly needed with UW IMAP, where you might need to specify the
|
||||
# directory in which your mail is stored. The 'reference' value will be prefixed
|
||||
# to all folder paths refering to that repository. E.g. accessing folder 'INBOX'
|
||||
# to all folder paths referring to that repository. E.g. accessing folder 'INBOX'
|
||||
# with "reference = Mail" will try to access Mail/INBOX.
|
||||
#
|
||||
# The nametrans and folderfilter functions will apply to the full path,
|
||||
|
@ -999,7 +1064,7 @@ remoteuser = username
|
|||
#reference = Mail
|
||||
|
||||
|
||||
# This option stands in the [Repository RemoteExample] section.
|
||||
# DEPRECATED: 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.
|
||||
|
@ -1007,10 +1072,26 @@ remoteuser = username
|
|||
# Note that the IMAP 4rev1 specification (RFC 3501) allows both UTF-8 and
|
||||
# modified UTF-7 folder names.
|
||||
#
|
||||
# This option converts IMAP folder names from IMAP4-UTF-7 to UTF-8.
|
||||
#
|
||||
# NOTE/LIMITATION:
|
||||
# - The reencoding is applied *after* a nametrans function that may be given,
|
||||
# so it is important to note that nametrans will work on the undecoded
|
||||
# UTF-7 names.
|
||||
# - This option only works from a remote IMAP to a local Maildir repository
|
||||
# - It only works *once*, so it can only be used for one-off backups
|
||||
# (see https://github.com/OfflineIMAP/offlineimap/issues/299 and especially
|
||||
# https://github.com/OfflineIMAP/offlineimap/issues/299#issuecomment-331243827)
|
||||
#
|
||||
# WARNING: with this option enabled:
|
||||
# - compatibility with any other version is NOT GUARANTED (including newer);
|
||||
# - no support is provided.
|
||||
#
|
||||
# DEPRECATION:
|
||||
# This option is only there for backward compatibility with existing set-ups.
|
||||
# For newly created accounts please use the utf8foldernames option on account
|
||||
# level.
|
||||
#
|
||||
# This feature was merged because it's small changes in the code. However, this
|
||||
# might seriously decrease the stability of the program. That's why it will
|
||||
# likely never be marked stable. The approach is: if it works for you, you're
|
||||
|
@ -1075,7 +1156,8 @@ remoteuser = username
|
|||
# download in UIDs order.
|
||||
#
|
||||
# If this is unset (the default), then up to maxconnections threads are used
|
||||
# across all currently syncing folders.
|
||||
# across all currently syncing folders. This option is sightly recommended in
|
||||
# IMAP/IMAP mode (no maildir).
|
||||
#
|
||||
#singlethreadperfolder = no
|
||||
|
||||
|
@ -1221,7 +1303,7 @@ remoteuser = username
|
|||
# This option stands in the [Repository RemoteExample] section.
|
||||
#
|
||||
# Propagate deletions from remote to local. Messages deleted in this repository
|
||||
# won't get deleted on the local repositor if set to "no". Default is yes.
|
||||
# won't get deleted on the local repository if set to "no". Default is yes.
|
||||
#
|
||||
# See sync_deletes in the LocalExample section, too.
|
||||
#
|
||||
|
@ -1292,7 +1374,7 @@ remoteuser = username
|
|||
|
||||
# This option stands in the [Repository RemoteExample] section.
|
||||
#
|
||||
# If offlineiamp is having troubles to download some UIDS, it's possible to get
|
||||
# If offlineimap is having troubles to download some UIDS, it's possible to get
|
||||
# them ignored in a list. This only ignore the download.
|
||||
#
|
||||
# The function must return the list of UIDs (integers), None otherwise. It is
|
||||
|
@ -1315,7 +1397,7 @@ remoteuser = username
|
|||
#
|
||||
# http://mail.google.com/support/bin/answer.py?answer=78799&topic=12814
|
||||
#
|
||||
# This means ssl is enabled and must be configured correcly when connecting to
|
||||
# This means ssl is enabled and must be configured correctly when connecting to
|
||||
# Gmail.
|
||||
#
|
||||
# In addition we provide defaults for "oauth2_request_url",
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
#!/usr/bin/env python
|
||||
#!/usr/bin/env python2
|
||||
# Startup from single-user installation
|
||||
# Copyright (C) 2002 - 2008 John Goerzen
|
||||
# <jgoerzen@complete.org>
|
||||
# Copyright (C) 2002-2018 John Goerzen & contributors
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -15,7 +14,7 @@
|
|||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
|
|
@ -1,18 +1,17 @@
|
|||
__all__ = ['OfflineImap']
|
||||
|
||||
__productname__ = 'OfflineIMAP'
|
||||
# Expecting trailing "-rcN" or "" for stable releases.
|
||||
__version__ = "7.0.13"
|
||||
__copyright__ = "Copyright 2002-2016 John Goerzen & contributors"
|
||||
__author__ = "John Goerzen"
|
||||
__author_email__= "offlineimap-project@lists.alioth.debian.org"
|
||||
__description__ = "Disconnected Universal IMAP Mail Synchronization/Reader Support"
|
||||
__license__ = "Licensed under the GNU GPL v2 or any later version (with an OpenSSL exception)"
|
||||
__bigcopyright__ = """%(__productname__)s %(__version__)s
|
||||
%(__license__)s""" % locals()
|
||||
__homepage__ = "http://www.offlineimap.org"
|
||||
|
||||
banner = __bigcopyright__
|
||||
from offlineimap.version import (
|
||||
__productname__,
|
||||
__version__,
|
||||
__copyright__,
|
||||
__author__,
|
||||
__author_email__,
|
||||
__description__,
|
||||
__license__,
|
||||
__bigcopyright__,
|
||||
__homepage__,
|
||||
banner
|
||||
)
|
||||
|
||||
from offlineimap.error import OfflineImapError
|
||||
# put this last, so we don't run into circular dependencies using
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
|
||||
from subprocess import Popen, PIPE
|
||||
from threading import Event
|
||||
from threading import Event, Lock
|
||||
import os
|
||||
import time
|
||||
from sys import exc_info
|
||||
|
@ -29,11 +29,17 @@ from offlineimap.ui import getglobalui
|
|||
from offlineimap.threadutil import InstanceLimitedThread
|
||||
|
||||
FOLDER_NAMESPACE = 'LIMITED_FOLDER_'
|
||||
# Key: account name, Value: Dict of Key: remotefolder name, Value: lock.
|
||||
SYNC_MUTEXES = {}
|
||||
SYNC_MUTEXES_LOCK = Lock()
|
||||
|
||||
try:
|
||||
import fcntl
|
||||
import portalocker
|
||||
except:
|
||||
pass # Ok if this fails, we can do without.
|
||||
try:
|
||||
import fcntl
|
||||
except:
|
||||
pass # Ok if this fails, we can do without.
|
||||
|
||||
# FIXME: spaghetti code alert!
|
||||
def getaccountlist(customconfig):
|
||||
|
@ -66,6 +72,8 @@ class Account(CustomConfig.ConfigHelperMixin):
|
|||
self.name = name
|
||||
self.metadatadir = config.getmetadatadir()
|
||||
self.localeval = config.getlocaleval()
|
||||
# Store utf-8 support as a property of Account object
|
||||
self.utf_8_support = self.getconfboolean('utf8foldernames', False)
|
||||
# Current :mod:`offlineimap.ui`, can be used for logging:
|
||||
self.ui = getglobalui()
|
||||
self.refreshperiod = self.getconffloat('autorefresh', 0.0)
|
||||
|
@ -227,10 +235,13 @@ class SyncableAccount(Account):
|
|||
|
||||
self._lockfd = open(self._lockfilepath, 'w')
|
||||
try:
|
||||
fcntl.lockf(self._lockfd, fcntl.LOCK_EX|fcntl.LOCK_NB)
|
||||
portalocker.lock(self._lockfd, portalocker.LOCK_EX)
|
||||
except NameError:
|
||||
#fcntl not available (Windows), disable file locking... :(
|
||||
pass
|
||||
# portalocker not available for Windows.
|
||||
try:
|
||||
fcntl.lockf(self._lockfd, fcntl.LOCK_EX|fcntl.LOCK_NB)
|
||||
except NameError:
|
||||
pass # fnctl not available, disable file locking... :(
|
||||
except IOError:
|
||||
self._lockfd.close()
|
||||
six.reraise(OfflineImapError,
|
||||
|
@ -245,6 +256,10 @@ class SyncableAccount(Account):
|
|||
|
||||
#If we own the lock file, delete it
|
||||
if self._lockfd and not self._lockfd.closed:
|
||||
try:
|
||||
portalocker.unlock(self._lockfd)
|
||||
except NameError:
|
||||
pass
|
||||
self._lockfd.close()
|
||||
try:
|
||||
os.unlink(self._lockfilepath)
|
||||
|
@ -306,6 +321,9 @@ class SyncableAccount(Account):
|
|||
remotefolder.getvisiblename().
|
||||
replace(self.remoterepos.getsep(), self.localrepos.getsep()))
|
||||
|
||||
|
||||
# The syncrunner will loop on this method. This means it is called more than
|
||||
# once during the run.
|
||||
def __sync(self):
|
||||
"""Synchronize the account once, then return.
|
||||
|
||||
|
@ -315,8 +333,20 @@ class SyncableAccount(Account):
|
|||
|
||||
folderthreads = []
|
||||
|
||||
hook = self.getconf('presynchook', '')
|
||||
self.callhook(hook)
|
||||
hook_env = {
|
||||
'OIMAP_ACCOUNT_NAME': self.getname(),
|
||||
}
|
||||
|
||||
self.callhook('presynchook', hook_env)
|
||||
|
||||
if self.utf_8_support and self.remoterepos.getdecodefoldernames():
|
||||
raise OfflineImapError("Configuration mismatch in account " +
|
||||
"'%s'. "% self.getname() +
|
||||
"\nAccount setting 'utf8foldernames' and repository " +
|
||||
"setting 'decodefoldernames'\nmay not be used at the " +
|
||||
"same time. This account has not been synchronized.\n" +
|
||||
"Please check the configuration and documentation.",
|
||||
OfflineImapError.ERROR.REPO)
|
||||
|
||||
quickconfig = self.getconfint('quick', 0)
|
||||
if quickconfig < 0:
|
||||
|
@ -332,6 +362,7 @@ class SyncableAccount(Account):
|
|||
quick = False
|
||||
|
||||
try:
|
||||
startedThread = False
|
||||
remoterepos = self.remoterepos
|
||||
localrepos = self.localrepos
|
||||
statusrepos = self.statusrepos
|
||||
|
@ -354,7 +385,7 @@ class SyncableAccount(Account):
|
|||
|
||||
if not remotefolder.sync_this:
|
||||
self.ui.debug('', "Not syncing filtered folder '%s'"
|
||||
"[%s]"% (remotefolder, remoterepos))
|
||||
"[%s]"% (remotefolder.getname(), remoterepos))
|
||||
continue # Ignore filtered folder.
|
||||
|
||||
# The remote folder names must not have the local sep char in
|
||||
|
@ -372,7 +403,7 @@ class SyncableAccount(Account):
|
|||
localfolder = self.get_local_folder(remotefolder)
|
||||
if not localfolder.sync_this:
|
||||
self.ui.debug('', "Not syncing filtered folder '%s'"
|
||||
"[%s]"% (localfolder, localfolder.repository))
|
||||
"[%s]"% (localfolder.getname(), localfolder.repository))
|
||||
continue # Ignore filtered folder.
|
||||
|
||||
if not globals.options.singlethreading:
|
||||
|
@ -388,10 +419,15 @@ class SyncableAccount(Account):
|
|||
folderthreads.append(thread)
|
||||
else:
|
||||
syncfolder(self, remotefolder, quick)
|
||||
startedThread = True
|
||||
# Wait for all threads to finish.
|
||||
for thr in folderthreads:
|
||||
thr.join()
|
||||
mbnames.writeIntermediateFile(self.name) # Write out mailbox names.
|
||||
if startedThread is True:
|
||||
mbnames.writeIntermediateFile(self.name) # Write out mailbox names.
|
||||
else:
|
||||
msg = "Account {}: no folder to sync (folderfilter issue?)".format(self)
|
||||
raise OfflineImapError(msg, OfflineImapError.ERROR.REPO)
|
||||
localrepos.forgetfolders()
|
||||
remoterepos.forgetfolders()
|
||||
except:
|
||||
|
@ -405,20 +441,23 @@ class SyncableAccount(Account):
|
|||
localrepos.holdordropconnections()
|
||||
remoterepos.holdordropconnections()
|
||||
|
||||
hook = self.getconf('postsynchook', '')
|
||||
self.callhook(hook)
|
||||
self.callhook('postsynchook', hook_env)
|
||||
|
||||
def callhook(self, cmd):
|
||||
def callhook(self, name, env={}):
|
||||
# Check for CTRL-C or SIGTERM and run postsynchook.
|
||||
if Account.abort_NOW_signal.is_set():
|
||||
return
|
||||
cmd = self.getconf(name, '')
|
||||
if not cmd:
|
||||
return
|
||||
try:
|
||||
self.ui.callhook("Calling hook: " + cmd)
|
||||
if self.dryrun:
|
||||
return
|
||||
p = Popen(cmd, shell=True,
|
||||
env = env.copy()
|
||||
env.update(os.environ)
|
||||
env['OIMAP_HOOK_NAME'] = name
|
||||
p = Popen(cmd, shell=True, env=env,
|
||||
stdin=PIPE, stdout=PIPE, stderr=PIPE,
|
||||
close_fds=True)
|
||||
r = p.communicate()
|
||||
|
@ -430,11 +469,35 @@ class SyncableAccount(Account):
|
|||
self.ui.error(e, exc_info()[2], msg="Calling hook")
|
||||
|
||||
|
||||
#XXX: This function should likely be refactored. This should not be passed the
|
||||
# account instance.
|
||||
def syncfolder(account, remotefolder, quick):
|
||||
"""Synchronizes given remote folder for the specified account.
|
||||
|
||||
Filtered folders on the remote side will not invoke this function. However,
|
||||
this might be called in a concurrently."""
|
||||
Filtered folders on the remote side will not invoke this function.
|
||||
|
||||
When called in concurrently for the same localfolder, syncs are
|
||||
serialized."""
|
||||
|
||||
def acquire_mutex():
|
||||
account_name = account.getname()
|
||||
localfolder_name = localfolder.getfullname()
|
||||
|
||||
with SYNC_MUTEXES_LOCK:
|
||||
if SYNC_MUTEXES.get(account_name) is None:
|
||||
SYNC_MUTEXES[account_name] = {}
|
||||
# The localfolder full name is good to uniquely identify the sync
|
||||
# transaction.
|
||||
if SYNC_MUTEXES[account_name].get(localfolder_name) is None:
|
||||
#XXX: This lock could be an external file lock so we can remove
|
||||
# the lock at the account level.
|
||||
SYNC_MUTEXES[account_name][localfolder_name] = Lock()
|
||||
|
||||
# Acquire the lock.
|
||||
SYNC_MUTEXES[account_name][localfolder_name].acquire()
|
||||
|
||||
def release_mutex():
|
||||
SYNC_MUTEXES[account.getname()][localfolder.getfullname()].release()
|
||||
|
||||
def check_uid_validity():
|
||||
# If either the local or the status folder has messages and
|
||||
|
@ -459,23 +522,30 @@ def syncfolder(account, remotefolder, quick):
|
|||
def cachemessagelists_upto_date(date):
|
||||
"""Returns messages with uid > min(uids of messages newer than date)."""
|
||||
|
||||
# Warning: this makes sense only if the cached list is empty.
|
||||
localfolder.cachemessagelist(min_date=date)
|
||||
check_uid_validity()
|
||||
# Local messagelist had date restriction applied already. Restrict
|
||||
# sync to messages with UIDs >= min_uid from this list.
|
||||
#
|
||||
# Local messagelist might contain new messages (with uid's < 0).
|
||||
positive_uids = [uid for uid in localfolder.getmessageuidlist() if uid > 0]
|
||||
if len(positive_uids) > 0:
|
||||
remotefolder.cachemessagelist(min_uid=min(positive_uids))
|
||||
remotefolder.cachemessagelist(
|
||||
min_date=time.gmtime(time.mktime(date) + 24*60*60))
|
||||
uids = remotefolder.getmessageuidlist()
|
||||
localfolder.dropmessagelistcache()
|
||||
if len(uids) > 0:
|
||||
# Reload the remote message list from min_uid. This avoid issues for
|
||||
# old messages, which has been added from local on any previous run
|
||||
# (IOW, message is older than maxage _and_ has high enough UID).
|
||||
remotefolder.dropmessagelistcache()
|
||||
remotefolder.cachemessagelist(min_uid=min(uids))
|
||||
localfolder.cachemessagelist(min_uid=min(uids))
|
||||
else:
|
||||
# No messages with UID > 0 in range in localfolder.
|
||||
# date restriction was applied with respect to local dates but
|
||||
# remote folder timezone might be different from local, so be
|
||||
# safe and make sure the range isn't bigger than in local.
|
||||
remotefolder.cachemessagelist(
|
||||
min_date=time.gmtime(time.mktime(date) + 24*60*60))
|
||||
# Remote folder UIDs list is empty for the given range. We still
|
||||
# might have valid local UIDs for this range (e.g.: new local
|
||||
# emails).
|
||||
localfolder.cachemessagelist(min_date=date)
|
||||
uids = localfolder.getmessageuidlist()
|
||||
# Take care to only consider positive uids. Negative UIDs might be
|
||||
# present due to new emails.
|
||||
uids = [uid for uid in uids if uid > 0]
|
||||
if len(uids) > 0:
|
||||
# Update the remote cache list for this new min(uids).
|
||||
remotefolder.dropmessagelistcache()
|
||||
remotefolder.cachemessagelist(min_uid=min(uids))
|
||||
|
||||
def cachemessagelists_startdate(new, partial, date):
|
||||
"""Retrieve messagelists when startdate has been set for
|
||||
|
@ -528,6 +598,9 @@ def syncfolder(account, remotefolder, quick):
|
|||
# Load local folder.
|
||||
localfolder = account.get_local_folder(remotefolder)
|
||||
|
||||
# Acquire the mutex to start syncing.
|
||||
acquire_mutex()
|
||||
|
||||
# Add the folder to the mbnames mailboxes.
|
||||
mbnames.add(account.name, localrepos.getlocalroot(),
|
||||
localfolder.getname())
|
||||
|
@ -616,3 +689,5 @@ def syncfolder(account, remotefolder, quick):
|
|||
if folder in locals():
|
||||
locals()[folder].dropmessagelistcache()
|
||||
statusfolder.closefiles()
|
||||
# Release the mutex of this sync transaction.
|
||||
release_mutex()
|
||||
|
|
|
@ -18,9 +18,9 @@ __all__ = ("IMAP4", "IMAP4_SSL", "IMAP4_stream",
|
|||
"Internaldate2Time", "ParseFlags", "Time2Internaldate",
|
||||
"Mon2num", "MonthNames", "InternalDate")
|
||||
|
||||
__version__ = "2.55"
|
||||
__version__ = "2.101"
|
||||
__release__ = "2"
|
||||
__revision__ = "55"
|
||||
__revision__ = "101"
|
||||
__credits__ = """
|
||||
Authentication code contributed by Donn Cave <donn@u.washington.edu> June 1998.
|
||||
String method conversion by ESR, February 2001.
|
||||
|
@ -53,7 +53,7 @@ Fix for correct Python 3 exception handling by Tobias Brink <tobias.brink@gmail.
|
|||
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> & offlineimap team"
|
||||
__URL__ = "http://imaplib2.sourceforge.net"
|
||||
__license__ = "Python License"
|
||||
|
||||
|
@ -67,7 +67,6 @@ if bytes != str:
|
|||
else:
|
||||
import Queue as queue
|
||||
string_types = basestring
|
||||
threading.TIMEOUT_MAX = 9223372036854.0
|
||||
|
||||
select_module = select
|
||||
|
||||
|
@ -109,6 +108,7 @@ Commands = {
|
|||
'CREATE': ((AUTH, SELECTED), True),
|
||||
'DELETE': ((AUTH, SELECTED), True),
|
||||
'DELETEACL': ((AUTH, SELECTED), True),
|
||||
'ENABLE': ((AUTH,), False),
|
||||
'EXAMINE': ((AUTH, SELECTED), False),
|
||||
'EXPUNGE': ((SELECTED,), True),
|
||||
'FETCH': ((SELECTED,), True),
|
||||
|
@ -191,7 +191,7 @@ class Request(object):
|
|||
def get_response(self, exc_fmt=None):
|
||||
self.callback = None
|
||||
if __debug__: self.parent._log(3, '%s:%s.ready.wait' % (self.name, self.tag))
|
||||
self.ready.wait(threading.TIMEOUT_MAX)
|
||||
self.ready.wait()
|
||||
|
||||
if self.aborted is not None:
|
||||
typ, val = self.aborted
|
||||
|
@ -300,17 +300,18 @@ class IMAP4(object):
|
|||
class readonly(abort): pass # Mailbox status changed to READ-ONLY
|
||||
|
||||
|
||||
# These must be encoded according to utf8 setting in _mode_xxx():
|
||||
_literal = br'.*{(?P<size>\d+)}$'
|
||||
_untagged_status = br'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?'
|
||||
|
||||
continuation_cre = re.compile(r'\+( (?P<data>.*))?')
|
||||
literal_cre = re.compile(r'.*{(?P<size>\d+)}$')
|
||||
mapCRLF_cre = re.compile(r'\r\n|\r|\n')
|
||||
# Need to quote "atom-specials" :-
|
||||
# "(" / ")" / "{" / SP / 0x00 - 0x1f / 0x7f / "%" / "*" / DQUOTE / "\" / "]"
|
||||
# so match not the inverse set
|
||||
mustquote_cre = re.compile(r"[^!#$&'+,./0-9:;<=>?@A-Z\[^_`a-z|}~-]")
|
||||
response_code_cre = re.compile(r'\[(?P<type>[A-Z-]+)( (?P<data>[^\]]*))?\]')
|
||||
# sequence_set_cre = re.compile(r"^[0-9]+(:([0-9]+|\*))?(,[0-9]+(:([0-9]+|\*))?)*$")
|
||||
untagged_response_cre = re.compile(r'\* (?P<type>[A-Z-]+)( (?P<data>.*))?')
|
||||
untagged_status_cre = re.compile(r'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?')
|
||||
|
||||
|
||||
def __init__(self, host=None, port=None, debug=None, debug_file=None, identifier=None, timeout=None, debug_buf_lvl=None):
|
||||
|
@ -340,7 +341,9 @@ class IMAP4(object):
|
|||
self.tagpre = Int2AP(random.randint(4096, 65535))
|
||||
self.tagre = re.compile(r'(?P<tag>'
|
||||
+ self.tagpre
|
||||
+ r'\d+) (?P<type>[A-Z]+) (?P<data>.*)')
|
||||
+ r'\d+) (?P<type>[A-Z]+) ?(?P<data>.*)')
|
||||
|
||||
self._mode_ascii() # Only option in py2
|
||||
|
||||
if __debug__: self._init_debug(debug, debug_file, debug_buf_lvl)
|
||||
|
||||
|
@ -428,6 +431,28 @@ class IMAP4(object):
|
|||
raise AttributeError("Unknown IMAP4 command: '%s'" % attr)
|
||||
|
||||
|
||||
def _mode_ascii(self):
|
||||
self.utf8_enabled = False
|
||||
self._encoding = 'ascii'
|
||||
if bytes != str:
|
||||
self.literal_cre = re.compile(self._literal, re.ASCII)
|
||||
self.untagged_status_cre = re.compile(self._untagged_status, re.ASCII)
|
||||
else:
|
||||
self.literal_cre = re.compile(self._literal)
|
||||
self.untagged_status_cre = re.compile(self._untagged_status)
|
||||
|
||||
|
||||
def _mode_utf8(self):
|
||||
self.utf8_enabled = True
|
||||
self._encoding = 'utf-8'
|
||||
if bytes != str:
|
||||
self.literal_cre = re.compile(self._literal)
|
||||
self.untagged_status_cre = re.compile(self._untagged_status)
|
||||
else:
|
||||
self.literal_cre = re.compile(self._literal, re.UNICODE)
|
||||
self.untagged_status_cre = re.compile(self._untagged_status, re.UNICODE)
|
||||
|
||||
|
||||
|
||||
# Overridable methods
|
||||
|
||||
|
@ -519,7 +544,16 @@ class IMAP4(object):
|
|||
|
||||
ssl_version = TLS_MAP[self.tls_level][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)
|
||||
if getattr(ssl, 'HAS_SNI', False):
|
||||
ctx = ssl.SSLContext(ssl_version)
|
||||
ctx.verify_mode = cert_reqs
|
||||
if self.ca_certs is not None:
|
||||
ctx.load_verify_locations(self.ca_certs)
|
||||
if self.certfile or self.keyfile:
|
||||
ctx.load_cert_chain(self.certfile, self.keyfile)
|
||||
self.sock = ctx.wrap_socket(self.sock, server_hostname=self.host)
|
||||
else:
|
||||
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
|
||||
self.read_fd = self.sock.fileno()
|
||||
except ImportError:
|
||||
|
@ -676,7 +710,10 @@ class IMAP4(object):
|
|||
date_time = Time2Internaldate(date_time)
|
||||
else:
|
||||
date_time = None
|
||||
self.literal = self.mapCRLF_cre.sub(CRLF, message)
|
||||
literal = self.mapCRLF_cre.sub(CRLF, message)
|
||||
if self.utf8_enabled:
|
||||
literal = b'UTF8 (' + literal + b')'
|
||||
self.literal = literal
|
||||
try:
|
||||
return self._simple_command(name, mailbox, flags, date_time, **kw)
|
||||
finally:
|
||||
|
@ -774,6 +811,19 @@ class IMAP4(object):
|
|||
return self._simple_command('DELETEACL', mailbox, who, **kw)
|
||||
|
||||
|
||||
def enable(self, capability):
|
||||
"""Send an RFC5161 enable string to the server.
|
||||
|
||||
(typ, [data]) = <intance>.enable(capability)
|
||||
"""
|
||||
if 'ENABLE' not in self.capabilities:
|
||||
raise self.error("Server does not support ENABLE")
|
||||
typ, data = self._simple_command('ENABLE', capability)
|
||||
if typ == 'OK' and 'UTF8=ACCEPT' in capability.upper():
|
||||
self._mode_utf8()
|
||||
return typ, data
|
||||
|
||||
|
||||
def examine(self, mailbox='INBOX', **kw):
|
||||
"""(typ, [data]) = examine(mailbox='INBOX')
|
||||
Select a mailbox for READ-ONLY access. (Flushes all untagged responses.)
|
||||
|
@ -1025,11 +1075,14 @@ class IMAP4(object):
|
|||
def search(self, charset, *criteria, **kw):
|
||||
"""(typ, [data]) = search(charset, criterion, ...)
|
||||
Search mailbox for matching messages.
|
||||
If UTF8 is enabled, charset MUST be None.
|
||||
'data' is space separated list of matching message numbers."""
|
||||
|
||||
name = 'SEARCH'
|
||||
kw['untagged_response'] = name
|
||||
if charset:
|
||||
if self.utf8_enabled:
|
||||
raise self.error("Non-None charset not valid in UTF8 mode")
|
||||
return self._simple_command(name, 'CHARSET', charset, *criteria, **kw)
|
||||
return self._simple_command(name, *criteria, **kw)
|
||||
|
||||
|
@ -1346,7 +1399,7 @@ class IMAP4(object):
|
|||
self.commands_lock.release()
|
||||
if need_event:
|
||||
if __debug__: self._log(3, 'sync command %s waiting for empty commands Q' % name)
|
||||
self.state_change_free.wait(threading.TIMEOUT_MAX)
|
||||
self.state_change_free.wait()
|
||||
if __debug__: self._log(3, 'sync command %s proceeding' % name)
|
||||
|
||||
if self.state not in Commands[name][CMD_VAL_STATES]:
|
||||
|
@ -1412,6 +1465,9 @@ class IMAP4(object):
|
|||
if not ok:
|
||||
break
|
||||
|
||||
if data == 'go ahead': # Apparently not uncommon broken IMAP4 server response to AUTHENTICATE command
|
||||
data = ''
|
||||
|
||||
# Send literal
|
||||
|
||||
if literator is not None:
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# Gmail IMAP folder support
|
||||
# Copyright (C) 2002-2016 John Goerzen & contributors.
|
||||
# Copyright (C) 2002-2017 John Goerzen & contributors.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -15,6 +15,8 @@
|
|||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
|
||||
"""Folder implementation to support features of the Gmail IMAP server."""
|
||||
|
||||
import re
|
||||
import six
|
||||
from sys import exc_info
|
||||
|
@ -23,7 +25,6 @@ from offlineimap import imaputil, imaplibutil, OfflineImapError
|
|||
import offlineimap.accounts
|
||||
from .IMAP import IMAPFolder
|
||||
|
||||
"""Folder implementation to support features of the Gmail IMAP server."""
|
||||
|
||||
class GmailFolder(IMAPFolder):
|
||||
"""Folder implementation to support features of the Gmail IMAP server.
|
||||
|
@ -41,11 +42,8 @@ class GmailFolder(IMAPFolder):
|
|||
https://developers.google.com/google-apps/gmail/imap_extensions
|
||||
"""
|
||||
|
||||
def __init__(self, imapserver, name, repository):
|
||||
super(GmailFolder, self).__init__(imapserver, name, repository)
|
||||
self.trash_folder = repository.gettrashfolder(name)
|
||||
# Gmail will really delete messages upon EXPUNGE in these folders
|
||||
self.real_delete_folders = [self.trash_folder, repository.getspamfolder()]
|
||||
def __init__(self, imapserver, name, repository, decode=True):
|
||||
super(GmailFolder, self).__init__(imapserver, name, repository, decode)
|
||||
|
||||
# The header under which labels are stored
|
||||
self.labelsheader = self.repository.account.getconf('labelsheader', 'X-Keywords')
|
||||
|
@ -81,7 +79,7 @@ class GmailFolder(IMAPFolder):
|
|||
|
||||
# Embed the labels into the message headers
|
||||
if self.synclabels:
|
||||
m = re.search('X-GM-LABELS\s*\(([^\)]*)\)', data[0][0])
|
||||
m = re.search('X-GM-LABELS\s*[(](.*)[)]', data[0][0])
|
||||
if m:
|
||||
labels = set([imaputil.dequote(lb) for lb in imaputil.imapsplit(m.group(1))])
|
||||
else:
|
||||
|
@ -155,6 +153,7 @@ class GmailFolder(IMAPFolder):
|
|||
if messagestr == None:
|
||||
continue
|
||||
messagestr = messagestr.split(' ', 1)[1]
|
||||
# e.g.: {'X-GM-LABELS': '("Webserver (RW.net)" "\\Inbox" GInbox)', 'FLAGS': '(\\Seen)', 'UID': '275440'}
|
||||
options = imaputil.flags2hash(messagestr)
|
||||
if not 'UID' in options:
|
||||
self.ui.warn('No UID in message with options %s' %\
|
||||
|
@ -164,7 +163,8 @@ class GmailFolder(IMAPFolder):
|
|||
uid = int(options['UID'])
|
||||
self.messagelist[uid] = self.msglist_item_initializer(uid)
|
||||
flags = imaputil.flagsimap2maildir(options['FLAGS'])
|
||||
m = re.search('\(([^\)]*)\)', options['X-GM-LABELS'])
|
||||
# e.g.: '("Webserver (RW.net)" "\\Inbox" GInbox)'
|
||||
m = re.search('^[(](.*)[)]', options['X-GM-LABELS'])
|
||||
if m:
|
||||
labels = set([imaputil.dequote(lb) for lb in imaputil.imapsplit(m.group(1))])
|
||||
else:
|
||||
|
|
|
@ -41,12 +41,21 @@ MSGCOPY_NAMESPACE = 'MSGCOPY_'
|
|||
|
||||
|
||||
class IMAPFolder(BaseFolder):
|
||||
def __init__(self, imapserver, name, repository):
|
||||
# FIXME: decide if unquoted name is from the responsability of the
|
||||
# caller or not, but not both.
|
||||
def __init__(self, imapserver, name, repository, decode=True):
|
||||
# decode the folder name from IMAP4_utf_7 to utf_8 if
|
||||
# - utf8foldernames is enabled for the *account*
|
||||
# - the decode argument is given
|
||||
# (default True is used when the folder name is the result of
|
||||
# querying the IMAP server, while False is used when creating
|
||||
# a folder object from a locally available utf_8 name)
|
||||
# In any case the given name is first dequoted.
|
||||
name = imaputil.dequote(name)
|
||||
if decode and repository.account.utf_8_support:
|
||||
name = imaputil.IMAP_utf8(name)
|
||||
self.sep = imapserver.delim
|
||||
super(IMAPFolder, self).__init__(name, repository)
|
||||
if repository.getdecodefoldernames():
|
||||
self.visiblename = imaputil.decode_mailbox_name(self.visiblename)
|
||||
self.idle_mode = False
|
||||
self.expunge = repository.getexpunge()
|
||||
self.root = None # imapserver.root
|
||||
|
@ -67,7 +76,6 @@ class IMAPFolder(BaseFolder):
|
|||
if self.repository.getidlefolders():
|
||||
self.idle_mode = True
|
||||
|
||||
|
||||
def __selectro(self, imapobj, force=False):
|
||||
"""Select this folder when we do not need write access.
|
||||
|
||||
|
@ -78,9 +86,15 @@ class IMAPFolder(BaseFolder):
|
|||
:param: Enforce new SELECT even if we are on that folder already.
|
||||
:returns: raises :exc:`OfflineImapError` severity FOLDER on error"""
|
||||
try:
|
||||
imapobj.select(self.getfullname(), force = force)
|
||||
imapobj.select(self.getfullIMAPname(), force=force)
|
||||
except imapobj.readonly:
|
||||
imapobj.select(self.getfullname(), readonly = True, force = force)
|
||||
imapobj.select(self.getfullIMAPname(), readonly=True, force=force)
|
||||
|
||||
def getfullIMAPname(self):
|
||||
name = self.getfullname()
|
||||
if self.repository.account.utf_8_support:
|
||||
name = imaputil.utf8_IMAP(name)
|
||||
return name
|
||||
|
||||
# Interface from BaseFolder
|
||||
def suggeststhreads(self):
|
||||
|
@ -145,7 +159,7 @@ class IMAPFolder(BaseFolder):
|
|||
imapobj = self.imapserver.acquireconnection()
|
||||
try:
|
||||
# Select folder and get number of messages.
|
||||
restype, imapdata = imapobj.select(self.getfullname(), True,
|
||||
restype, imapdata = imapobj.select(self.getfullIMAPname(), True,
|
||||
True)
|
||||
self.imapserver.releaseconnection(imapobj)
|
||||
except OfflineImapError as e:
|
||||
|
@ -195,11 +209,17 @@ class IMAPFolder(BaseFolder):
|
|||
|
||||
Returns: range(s) for messages or None if no messages
|
||||
are to be fetched."""
|
||||
res_type, res_data = imapobj.search(None, search_conditions)
|
||||
if res_type != 'OK':
|
||||
try:
|
||||
res_type, res_data = imapobj.search(None, search_conditions)
|
||||
if res_type != 'OK':
|
||||
raise OfflineImapError("SEARCH in folder [%s]%s failed. "
|
||||
"Search string was '%s'. Server responded '[%s] %s'"% (
|
||||
self.getrepository(), self, search_cond, res_type, res_data),
|
||||
OfflineImapError.ERROR.FOLDER)
|
||||
except Exception as e:
|
||||
raise OfflineImapError("SEARCH in folder [%s]%s failed. "
|
||||
"Search string was '%s'. Server responded '[%s] %s'"% (
|
||||
self.getrepository(), self, search_cond, res_type, res_data),
|
||||
"Search string was '%s'. Error: %s"% (
|
||||
self.getrepository(), self, search_cond, str(e)),
|
||||
OfflineImapError.ERROR.FOLDER)
|
||||
# Davmail returns list instead of list of one element string.
|
||||
# On first run the first element is empty.
|
||||
|
@ -211,7 +231,7 @@ class IMAPFolder(BaseFolder):
|
|||
res_data.remove(0)
|
||||
return res_data
|
||||
|
||||
res_type, imapdata = imapobj.select(self.getfullname(), True, True)
|
||||
res_type, imapdata = imapobj.select(self.getfullIMAPname(), True, True)
|
||||
if imapdata == [None] or imapdata[0] == '0':
|
||||
# Empty folder, no need to populate message list.
|
||||
return None
|
||||
|
@ -290,13 +310,6 @@ class IMAPFolder(BaseFolder):
|
|||
'keywords': keywords}
|
||||
self.ui.messagelistloaded(self.repository, self, self.getmessagecount())
|
||||
|
||||
# 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
|
||||
def getmessage(self, uid):
|
||||
"""Retrieve message with UID from the IMAP server (incl body).
|
||||
|
@ -389,12 +402,16 @@ class IMAPFolder(BaseFolder):
|
|||
return 0
|
||||
|
||||
matchinguids = matchinguids.split(' ')
|
||||
matchinguids = list(set(matchinguids)) # Remove duplicates.
|
||||
self.ui.debug('imap', '__savemessage_searchforheader: matchinguids now '
|
||||
+ repr(matchinguids))
|
||||
if len(matchinguids) != 1 or matchinguids[0] is None:
|
||||
raise ValueError("While attempting to find UID for message with "
|
||||
"header %s, got wrong-sized matchinguids of %s"%
|
||||
(headername, str(matchinguids)))
|
||||
raise OfflineImapError(
|
||||
"While attempting to find UID for message with "
|
||||
"header %s, got wrong-sized matchinguids of %s"%
|
||||
(headername, str(matchinguids)),
|
||||
OfflineImapError.ERROR.MESSAGE
|
||||
)
|
||||
return int(matchinguids[0])
|
||||
|
||||
def __savemessage_fetchheaders(self, imapobj, headername, headervalue):
|
||||
|
@ -445,24 +462,46 @@ class IMAPFolder(BaseFolder):
|
|||
raise OfflineImapError('Error fetching mail headers: %s'%
|
||||
'. '.join(result[1]), OfflineImapError.ERROR.MESSAGE)
|
||||
|
||||
# result is like:
|
||||
# [
|
||||
# ('185 (RFC822.HEADER {1789}', '... mail headers ...'), ' UID 2444)',
|
||||
# ('186 (RFC822.HEADER {1789}', '... 2nd mail headers ...'), ' UID 2445)'
|
||||
# ]
|
||||
result = result[1]
|
||||
|
||||
found = 0
|
||||
found = None
|
||||
# item is like:
|
||||
# ('185 (RFC822.HEADER {1789}', '... mail headers ...'), ' UID 2444)'
|
||||
for item in result:
|
||||
if found == 0 and type(item) == type( () ):
|
||||
if found is None and type(item) == tuple:
|
||||
# Walk just tuples.
|
||||
if re.search("(?:^|\\r|\\n)%s:\s*%s(?:\\r|\\n)"% (headername, headervalue),
|
||||
item[1], flags=re.IGNORECASE):
|
||||
found = 1
|
||||
elif found == 1:
|
||||
if type(item) == type (""):
|
||||
found = item[0]
|
||||
elif found is not None:
|
||||
if type(item) == type(""):
|
||||
uid = re.search("UID\s+(\d+)", item, flags=re.IGNORECASE)
|
||||
if uid:
|
||||
return int(uid.group(1))
|
||||
else:
|
||||
self.ui.warn("Can't parse FETCH response, can't find UID: %s", result.__repr__())
|
||||
# This parsing is for Davmail.
|
||||
# https://github.com/OfflineIMAP/offlineimap/issues/479
|
||||
# item is like:
|
||||
# ')'
|
||||
# and item[0] stored in "found" is like:
|
||||
# '1694 (UID 1694 RFC822.HEADER {1294}'
|
||||
uid = re.search("\d+\s+\(UID\s+(\d+)", found, flags=re.IGNORECASE)
|
||||
if uid:
|
||||
return int(uid.group(1))
|
||||
|
||||
self.ui.warn("Can't parse FETCH response, can't find UID in %s"%
|
||||
item
|
||||
)
|
||||
self.ui.debug('imap', "Got: %s"% repr(result))
|
||||
else:
|
||||
self.ui.warn("Can't parse FETCH response, we awaited string: %s", result.__repr__())
|
||||
self.ui.warn("Can't parse FETCH response, we awaited string: %s"%
|
||||
repr(item)
|
||||
)
|
||||
|
||||
return 0
|
||||
|
||||
|
@ -610,7 +649,7 @@ class IMAPFolder(BaseFolder):
|
|||
|
||||
try:
|
||||
# Select folder for append and make the box READ-WRITE.
|
||||
imapobj.select(self.getfullname())
|
||||
imapobj.select(self.getfullIMAPname())
|
||||
except imapobj.readonly:
|
||||
# readonly exception. Return original uid to notify that
|
||||
# we did not save the message. (see savemessage in Base.py)
|
||||
|
@ -619,7 +658,7 @@ class IMAPFolder(BaseFolder):
|
|||
|
||||
# Do the APPEND.
|
||||
try:
|
||||
(typ, dat) = imapobj.append(self.getfullname(),
|
||||
(typ, dat) = imapobj.append(self.getfullIMAPname(),
|
||||
imaputil.flagsmaildir2imap(flags), date, content)
|
||||
# This should only catch 'NO' responses since append()
|
||||
# will raise an exception for 'BAD' responses:
|
||||
|
@ -679,31 +718,48 @@ class IMAPFolder(BaseFolder):
|
|||
resp = imapobj._get_untagged_response('APPENDUID')
|
||||
if resp == [None] or resp is None:
|
||||
self.ui.warn("Server supports UIDPLUS but got no APPENDUID "
|
||||
"appending a message.")
|
||||
"appending a message. Got: %s."% str(resp))
|
||||
return 0
|
||||
uid = int(resp[-1].split(' ')[1])
|
||||
try:
|
||||
uid = int(resp[-1].split(' ')[1])
|
||||
except ValueError as e:
|
||||
uid = 0 # Definetly not what we should have.
|
||||
except Exception as e:
|
||||
raise OfflineImapError("Unexpected response: %s"% str(resp),
|
||||
OfflineImapError.ERROR.MESSAGE)
|
||||
if uid == 0:
|
||||
self.ui.warn("savemessage: Server supports UIDPLUS, but"
|
||||
" we got no usable uid back. APPENDUID reponse was "
|
||||
" we got no usable UID back. APPENDUID reponse was "
|
||||
"'%s'"% str(resp))
|
||||
else:
|
||||
# We don't support UIDPLUS.
|
||||
uid = self.__savemessage_searchforheader(imapobj, headername,
|
||||
headervalue)
|
||||
# See docs for savemessage in Base.py for explanation
|
||||
# of this and other return values.
|
||||
if uid == 0:
|
||||
self.ui.debug('imap', 'savemessage: attempt to get new UID '
|
||||
'UID failed. Search headers manually.')
|
||||
uid = self.__savemessage_fetchheaders(imapobj, headername,
|
||||
try:
|
||||
# We don't use UIDPLUS.
|
||||
uid = self.__savemessage_searchforheader(imapobj, headername,
|
||||
headervalue)
|
||||
self.ui.warn('imap', "savemessage: Searching mails for new "
|
||||
"Message-ID failed. Could not determine new UID.")
|
||||
# See docs for savemessage in Base.py for explanation
|
||||
# of this and other return values.
|
||||
if uid == 0:
|
||||
self.ui.debug('imap', 'savemessage: attempt to get new UID '
|
||||
'UID failed. Search headers manually.')
|
||||
uid = self.__savemessage_fetchheaders(imapobj, headername,
|
||||
headervalue)
|
||||
self.ui.warn("savemessage: Searching mails for new "
|
||||
"Message-ID failed. Could not determine new UID "
|
||||
"on %s."% self.getname())
|
||||
# Something wrong happened while trying to get the UID. Explain
|
||||
# the error might be about the 'get UID' process not necesseraly
|
||||
# the APPEND.
|
||||
except Exception:
|
||||
self.ui.warn("%s: could not determine the UID while we got "
|
||||
"no error while appending the email with '%s: %s'"%
|
||||
(self.getname(), headername, headervalue)
|
||||
)
|
||||
raise
|
||||
finally:
|
||||
if imapobj:
|
||||
self.imapserver.releaseconnection(imapobj)
|
||||
|
||||
if uid: # Avoid UID FETCH 0 crash happening later on
|
||||
if uid: # Avoid UID FETCH 0 crash happening later on.
|
||||
self.messagelist[uid] = self.msglist_item_initializer(uid)
|
||||
self.messagelist[uid]['flags'] = flags
|
||||
|
||||
|
@ -726,7 +782,7 @@ class IMAPFolder(BaseFolder):
|
|||
fails_left = retry_num # Retry on dropped connection.
|
||||
while fails_left:
|
||||
try:
|
||||
imapobj.select(self.getfullname(), readonly=True)
|
||||
imapobj.select(self.getfullIMAPname(), readonly=True)
|
||||
res_type, data = imapobj.uid('fetch', uids, query)
|
||||
break
|
||||
except imapobj.abort as e:
|
||||
|
@ -786,7 +842,7 @@ class IMAPFolder(BaseFolder):
|
|||
- field: field name to be stored/updated
|
||||
- data: field contents
|
||||
"""
|
||||
imapobj.select(self.getfullname())
|
||||
imapobj.select(self.getfullIMAPname())
|
||||
res_type, retdata = imapobj.uid('store', uid, field, data)
|
||||
if res_type != 'OK':
|
||||
severity = OfflineImapError.ERROR.MESSAGE
|
||||
|
@ -847,7 +903,7 @@ class IMAPFolder(BaseFolder):
|
|||
imapobj = self.imapserver.acquireconnection()
|
||||
try:
|
||||
try:
|
||||
imapobj.select(self.getfullname())
|
||||
imapobj.select(self.getfullIMAPname())
|
||||
except imapobj.readonly:
|
||||
self.ui.flagstoreadonly(self, uidlist, flags)
|
||||
return
|
||||
|
@ -922,7 +978,7 @@ class IMAPFolder(BaseFolder):
|
|||
imapobj = self.imapserver.acquireconnection()
|
||||
try:
|
||||
try:
|
||||
imapobj.select(self.getfullname())
|
||||
imapobj.select(self.getfullIMAPname())
|
||||
except imapobj.readonly:
|
||||
self.ui.deletereadonly(self, uidlist)
|
||||
return
|
||||
|
|
|
@ -119,7 +119,7 @@ class LocalStatusFolder(BaseFolder):
|
|||
|
||||
# Convert from format v1.
|
||||
elif line == (self.magicline % 1):
|
||||
self.ui._msg('Upgrading LocalStatus cache from version 1'
|
||||
self.ui._msg('Upgrading LocalStatus cache from version 1 '
|
||||
'to version 2 for %s:%s'% (self.repository, self))
|
||||
self.readstatus_v1(cachefd)
|
||||
cachefd.close()
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# Local status cache virtual folder: SQLite backend
|
||||
# Copyright (C) 2009-2016 Stewart Smith and contributors.
|
||||
# Copyright (C) 2009-2017 Stewart Smith and contributors.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -282,16 +282,17 @@ class LocalStatusSQLiteFolder(BaseFolder):
|
|||
def saveall(self):
|
||||
"""Saves the entire messagelist to the database."""
|
||||
|
||||
data = []
|
||||
for uid, msg in self.messagelist.items():
|
||||
mtime = msg['mtime']
|
||||
flags = ''.join(sorted(msg['flags']))
|
||||
labels = ', '.join(sorted(msg['labels']))
|
||||
data.append((uid, flags, mtime, labels))
|
||||
with self._databaseFileLock.getLock():
|
||||
data = []
|
||||
for uid, msg in self.messagelist.items():
|
||||
mtime = msg['mtime']
|
||||
flags = ''.join(sorted(msg['flags']))
|
||||
labels = ', '.join(sorted(msg['labels']))
|
||||
data.append((uid, flags, mtime, labels))
|
||||
|
||||
self.__sql_write('INSERT OR REPLACE INTO status '
|
||||
'(id,flags,mtime,labels) VALUES (?,?,?,?)',
|
||||
data, executemany=True)
|
||||
self.__sql_write('INSERT OR REPLACE INTO status '
|
||||
'(id,flags,mtime,labels) VALUES (?,?,?,?)',
|
||||
data, executemany=True)
|
||||
|
||||
|
||||
# Following some pure SQLite functions, where we chose to use
|
||||
|
@ -351,8 +352,14 @@ class LocalStatusSQLiteFolder(BaseFolder):
|
|||
self.messagelist[uid] = {'uid': uid, 'flags': flags, 'time': rtime, 'mtime': mtime, 'labels': labels}
|
||||
flags = ''.join(sorted(flags))
|
||||
labels = ', '.join(sorted(labels))
|
||||
self.__sql_write('INSERT INTO status (id,flags,mtime,labels) VALUES (?,?,?,?)',
|
||||
(uid,flags,mtime,labels))
|
||||
try:
|
||||
self.__sql_write('INSERT INTO status (id,flags,mtime,labels) VALUES (?,?,?,?)',
|
||||
(uid,flags,mtime,labels))
|
||||
except Exception as e:
|
||||
six.reraise(UserWarning,
|
||||
UserWarning("%s while inserting UID %s"%
|
||||
(str(e), str(uid))),
|
||||
exc_info()[2])
|
||||
return uid
|
||||
|
||||
|
||||
|
|
|
@ -82,6 +82,10 @@ class MaildirFolder(BaseFolder):
|
|||
"general", "utime_from_header", False)
|
||||
self._utime_from_header = self.config.getdefaultboolean(
|
||||
self.repoconfname, "utime_from_header", utime_from_header_global)
|
||||
# What do we substitute pathname separator in names (if any)
|
||||
self.sep_subst = '-'
|
||||
if os.path.sep == self.sep_subst:
|
||||
self.sep_subst = '_'
|
||||
|
||||
# Interface from BaseFolder
|
||||
def getfullname(self):
|
||||
|
@ -286,9 +290,10 @@ class MaildirFolder(BaseFolder):
|
|||
:returns: String containing unique message filename"""
|
||||
|
||||
timeval, timeseq = _gettimeseq(date)
|
||||
return '%d_%d.%d.%s,U=%d,FMD5=%s%s2,%s'% \
|
||||
uniq_name = '%d_%d.%d.%s,U=%d,FMD5=%s%s2,%s' % \
|
||||
(timeval, timeseq, os.getpid(), socket.gethostname(),
|
||||
uid, self._foldermd5, self.infosep, ''.join(sorted(flags)))
|
||||
return uniq_name.replace(os.path.sep, self.sep_subst)
|
||||
|
||||
|
||||
def save_to_tmp_file(self, filename, content):
|
||||
|
|
|
@ -40,8 +40,8 @@ class MappedIMAPFolder(IMAPFolder):
|
|||
diskr2l: dict mapping message uids: self.r2l[remoteuid]=localuid
|
||||
diskl2r: dict mapping message uids: self.r2l[localuid]=remoteuid"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
IMAPFolder.__init__(self, *args, **kwargs)
|
||||
def __init__(self, imapserver, name, repository, decode=True):
|
||||
IMAPFolder.__init__(self, imapserver, name, repository, decode=False)
|
||||
self.dryrun = self.config.getdefaultboolean("general", "dry-run", True)
|
||||
self.maplock = Lock()
|
||||
self.diskr2l, self.diskl2r = self._loadmaps()
|
||||
|
@ -49,7 +49,7 @@ class MappedIMAPFolder(IMAPFolder):
|
|||
# Representing the local IMAP Folder using local UIDs.
|
||||
# XXX: This should be removed since we inherit from IMAPFolder.
|
||||
# See commit 3ce514e92ba7 to know more.
|
||||
self._mb = IMAPFolder(*args, **kwargs)
|
||||
self._mb = IMAPFolder(imapserver, name, repository, decode=False)
|
||||
|
||||
def _getmapfilename(self):
|
||||
return os.path.join(self.repository.getmapdir(),
|
||||
|
@ -142,8 +142,22 @@ class MappedIMAPFolder(IMAPFolder):
|
|||
for luid in self.diskl2r.keys():
|
||||
if not luid in reallist:
|
||||
ruid = self.diskl2r[luid]
|
||||
del self.diskr2l[ruid]
|
||||
del self.diskl2r[luid]
|
||||
#XXX: the following KeyError are sightly unexpected. This
|
||||
# would require more digging to understand how it's
|
||||
# possible.
|
||||
errorMessage = ("unexpected error: key {} was not found "
|
||||
"in memory, see "
|
||||
"https://github.com/OfflineIMAP/offlineimap/issues/445"
|
||||
" to know more."
|
||||
)
|
||||
try:
|
||||
del self.diskr2l[ruid]
|
||||
except KeyError as e:
|
||||
self.ui.warn(errorMessage.format(ruid))
|
||||
try:
|
||||
del self.diskl2r[luid]
|
||||
except KeyError as e:
|
||||
self.ui.warn(errorMessage.format(ruid))
|
||||
|
||||
# Now, assign negative UIDs to local items.
|
||||
self._savemaps()
|
||||
|
@ -253,8 +267,12 @@ class MappedIMAPFolder(IMAPFolder):
|
|||
|
||||
newluid = self._mb.savemessage(-1, content, flags, rtime)
|
||||
if newluid < 1:
|
||||
raise ValueError("Backend could not find uid for message, "
|
||||
"returned %s"% newluid)
|
||||
raise OfflineImapError("server of repository '%s' did not return "
|
||||
"a valid UID (got '%s') for UID '%s' from '%s'"% (
|
||||
self._mb.getname(), newluid, uid, self.getname()
|
||||
),
|
||||
OfflineImapError.ERROR.MESSAGE
|
||||
)
|
||||
with self.maplock:
|
||||
self.diskl2r[newluid] = uid
|
||||
self.diskr2l[uid] = newluid
|
||||
|
|
|
@ -19,11 +19,12 @@ import fcntl
|
|||
import time
|
||||
import subprocess
|
||||
import threading
|
||||
import rfc6555
|
||||
import socket
|
||||
import errno
|
||||
import zlib
|
||||
from sys import exc_info
|
||||
from hashlib import sha1
|
||||
from hashlib import sha512, sha384, sha256, sha224, sha1
|
||||
|
||||
import six
|
||||
|
||||
|
@ -78,8 +79,15 @@ class UsefulIMAPMixIn(object):
|
|||
def open_socket(self):
|
||||
"""open_socket()
|
||||
Open socket choosing first address family available."""
|
||||
if self.af == socket.AF_UNSPEC:
|
||||
# happy-eyeballs!
|
||||
return rfc6555.create_connection((self.host, self.port))
|
||||
else:
|
||||
return self._open_socket_for_af(self.af)
|
||||
|
||||
def _open_socket_for_af(self, af):
|
||||
msg = (-1, 'could not open socket')
|
||||
for res in socket.getaddrinfo(self.host, self.port, self.af, socket.SOCK_STREAM):
|
||||
for res in socket.getaddrinfo(self.host, self.port, af, socket.SOCK_STREAM):
|
||||
af, socktype, proto, canonname, sa = res
|
||||
try:
|
||||
# use socket of our own, possiblly socksified socket.
|
||||
|
@ -124,7 +132,7 @@ class IMAP4_Tunnel(UsefulIMAPMixIn, IMAP4):
|
|||
"""The tunnelcmd comes in on host!"""
|
||||
|
||||
self.host = host
|
||||
self.process = subprocess.Popen(host, shell=True, close_fds=True,
|
||||
self.process = subprocess.Popen('exec %s'%host, shell=True, close_fds=True,
|
||||
stdin=subprocess.PIPE, stdout=subprocess.PIPE)
|
||||
(self.outfd, self.infd) = (self.process.stdin, self.process.stdout)
|
||||
# imaplib2 polls on this fd
|
||||
|
@ -201,15 +209,18 @@ class WrappedIMAP4_SSL(UsefulIMAPMixIn, IMAP4_SSL):
|
|||
"having SSL helps nothing.", OfflineImapError.ERROR.REPO)
|
||||
super(WrappedIMAP4_SSL, self).open(host, port)
|
||||
if self._fingerprint:
|
||||
server_cert = self.sock.getpeercert(True)
|
||||
hashes = sha512, sha384, sha256, sha224, sha1
|
||||
server_fingerprints = [hash(server_cert).hexdigest() for hash in hashes]
|
||||
# compare fingerprints
|
||||
fingerprint = sha1(self.sock.getpeercert(True)).hexdigest()
|
||||
if fingerprint not in self._fingerprint:
|
||||
raise OfflineImapError("Server SSL fingerprint '%s' "
|
||||
matches = [(server_fingerprint in self._fingerprint) for server_fingerprint in server_fingerprints]
|
||||
if not any(matches):
|
||||
raise OfflineImapError("Server SSL fingerprint(s) '%s' "
|
||||
"for hostname '%s' "
|
||||
"does not match configured fingerprint(s) %s. "
|
||||
"Please verify and set 'cert_fingerprint' accordingly "
|
||||
"if not set yet."%
|
||||
(fingerprint, host, self._fingerprint),
|
||||
(zip([hash.__name__ for hash in hashes], server_fingerprints), host, self._fingerprint),
|
||||
OfflineImapError.ERROR.REPO)
|
||||
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# IMAP server support
|
||||
# Copyright (C) 2002-2016 John Goerzen & contributors.
|
||||
# Copyright (C) 2002-2018 John Goerzen & contributors.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -15,9 +15,9 @@
|
|||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
|
||||
import datetime
|
||||
import hmac
|
||||
import socket
|
||||
import base64
|
||||
import json
|
||||
import urllib
|
||||
import time
|
||||
|
@ -36,13 +36,10 @@ from offlineimap.ui import getglobalui
|
|||
|
||||
|
||||
try:
|
||||
# do we have a recent pykerberos?
|
||||
have_gss = False
|
||||
import kerberos
|
||||
if 'authGSSClientWrap' in dir(kerberos):
|
||||
have_gss = True
|
||||
import gssapi
|
||||
have_gss = True
|
||||
except ImportError:
|
||||
pass
|
||||
have_gss = False
|
||||
|
||||
|
||||
class IMAPServer(object):
|
||||
|
@ -55,9 +52,6 @@ class IMAPServer(object):
|
|||
delim The server's folder delimiter. Only valid after acquireconnection()
|
||||
"""
|
||||
|
||||
GSS_STATE_STEP = 0
|
||||
GSS_STATE_WRAP = 1
|
||||
|
||||
def __init__(self, repos):
|
||||
""":repos: a IMAPRepository instance."""
|
||||
|
||||
|
@ -92,7 +86,7 @@ class IMAPServer(object):
|
|||
self.af = socket.AF_INET
|
||||
else:
|
||||
self.af = socket.AF_UNSPEC
|
||||
self.hostname = None if self.preauth_tunnel else repos.gethost()
|
||||
self.hostname = None if self.transport_tunnel or self.preauth_tunnel else repos.gethost()
|
||||
self.port = repos.getport()
|
||||
if self.port is None:
|
||||
self.port = 993 if self.usessl else 143
|
||||
|
@ -107,8 +101,10 @@ class IMAPServer(object):
|
|||
self.sslversion = repos.getsslversion()
|
||||
self.starttls = repos.getstarttls()
|
||||
|
||||
if self.tlslevel is not "tls_compat" and self.sslversion is None:
|
||||
raise Exception("When 'tls_version' is not 'tls_compat' "
|
||||
if self.usessl \
|
||||
and self.tlslevel != "tls_compat" \
|
||||
and self.sslversion is None:
|
||||
raise Exception("When 'tls_level' is not 'tls_compat' "
|
||||
"the 'ssl_version' must be set explicitly.")
|
||||
|
||||
self.oauth2_refresh_token = repos.getoauth2_refresh_token()
|
||||
|
@ -116,6 +112,7 @@ class IMAPServer(object):
|
|||
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.oauth2_access_token_expires_at = None
|
||||
|
||||
self.delim = None
|
||||
self.root = None
|
||||
|
@ -127,7 +124,6 @@ class IMAPServer(object):
|
|||
self.connectionlock = Lock()
|
||||
self.reference = repos.getreference()
|
||||
self.idlefolders = repos.getidlefolders()
|
||||
self.gss_step = self.GSS_STATE_STEP
|
||||
self.gss_vc = None
|
||||
self.gssapi = False
|
||||
|
||||
|
@ -183,8 +179,7 @@ class IMAPServer(object):
|
|||
|
||||
# get 1) configured password first 2) fall back to asking via UI
|
||||
self.password = self.repos.getpassword() or \
|
||||
self.ui.getpass(self.repos.getname(), self.config,
|
||||
self.passworderror)
|
||||
self.ui.getpass(self.username, self.config, self.passworderror)
|
||||
self.passworderror = None
|
||||
return self.password
|
||||
|
||||
|
@ -208,6 +203,11 @@ class IMAPServer(object):
|
|||
http://tools.ietf.org/html/rfc4616"""
|
||||
|
||||
authc = self.username
|
||||
if not authc:
|
||||
raise OfflineImapError("No username provided for '%s'"
|
||||
% self.repos.getname(),
|
||||
OfflineImapError.ERROR.REPO)
|
||||
|
||||
passwd = self.__getpassword()
|
||||
authz = b''
|
||||
if self.user_identity != None:
|
||||
|
@ -221,9 +221,11 @@ class IMAPServer(object):
|
|||
return retval
|
||||
|
||||
def __xoauth2handler(self, response):
|
||||
if self.oauth2_refresh_token is None \
|
||||
and self.oauth2_access_token is None:
|
||||
return None
|
||||
now = datetime.datetime.now()
|
||||
if self.oauth2_access_token_expires_at \
|
||||
and self.oauth2_access_token_expires_at < now:
|
||||
self.oauth2_access_token = None
|
||||
self.ui.debug('imap', 'xoauth2handler: oauth2_access_token expired')
|
||||
|
||||
if self.oauth2_access_token is None:
|
||||
if self.oauth2_request_url is None:
|
||||
|
@ -262,41 +264,65 @@ class IMAPServer(object):
|
|||
raise OfflineImapError("xoauth2handler got: %s"% resp,
|
||||
OfflineImapError.ERROR.REPO)
|
||||
self.oauth2_access_token = resp['access_token']
|
||||
if u'expires_in' in resp:
|
||||
self.oauth2_access_token_expires_at = now + datetime.timedelta(
|
||||
seconds=resp['expires_in']/2
|
||||
)
|
||||
|
||||
self.ui.debug('imap', 'xoauth2handler: access_token "%s"'%
|
||||
self.oauth2_access_token)
|
||||
self.ui.debug('imap', 'xoauth2handler: access_token "%s expires %s"'% (
|
||||
self.oauth2_access_token, self.oauth2_access_token_expires_at))
|
||||
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):
|
||||
data = base64.b64encode(response)
|
||||
# Perform the next step handling a GSSAPI connection.
|
||||
# Client sends first, so token will be ignored if there is no context.
|
||||
def __gsshandler(self, token):
|
||||
if token == "":
|
||||
token = None
|
||||
try:
|
||||
if self.gss_step == self.GSS_STATE_STEP:
|
||||
if not self.gss_vc:
|
||||
rc, self.gss_vc = kerberos.authGSSClientInit(
|
||||
'imap@' + self.hostname)
|
||||
response = kerberos.authGSSClientResponse(self.gss_vc)
|
||||
rc = kerberos.authGSSClientStep(self.gss_vc, data)
|
||||
if rc != kerberos.AUTH_GSS_CONTINUE:
|
||||
self.gss_step = self.GSS_STATE_WRAP
|
||||
elif self.gss_step == self.GSS_STATE_WRAP:
|
||||
rc = kerberos.authGSSClientUnwrap(self.gss_vc, data)
|
||||
response = kerberos.authGSSClientResponse(self.gss_vc)
|
||||
rc = kerberos.authGSSClientWrap(
|
||||
self.gss_vc, response, self.username)
|
||||
response = kerberos.authGSSClientResponse(self.gss_vc)
|
||||
except kerberos.GSSError as err:
|
||||
# Kerberos errored out on us, respond with None to cancel the
|
||||
# authentication
|
||||
self.ui.debug('imap', '%s: %s'% (err[0][0], err[1][0]))
|
||||
return None
|
||||
if not self.gss_vc:
|
||||
name = gssapi.Name('imap@' + self.hostname,
|
||||
gssapi.NameType.hostbased_service)
|
||||
self.gss_vc = gssapi.SecurityContext(usage="initiate",
|
||||
name=name)
|
||||
|
||||
if not response:
|
||||
response = ''
|
||||
return base64.b64decode(response)
|
||||
if not self.gss_vc.complete:
|
||||
response = self.gss_vc.step(token)
|
||||
return response if response else ""
|
||||
elif token is None:
|
||||
# uh... context is complete, so there's no negotiation we can
|
||||
# do. But we also don't have a token, so we can't send any
|
||||
# kind of response. Empirically, some (but not all) servers
|
||||
# seem to put us in this state, and seem fine with getting no
|
||||
# GSSAPI content in response, so give it to them.
|
||||
return ""
|
||||
|
||||
# Don't bother checking qop because we're over a TLS channel
|
||||
# already. But hey, if some server started encrypting tomorrow,
|
||||
# we'd be ready since krb5 always requests integrity and
|
||||
# confidentiality support.
|
||||
response = self.gss_vc.unwrap(token)
|
||||
|
||||
# This is a behavior we got from pykerberos. First byte is one,
|
||||
# first four bytes are preserved (pykerberos calls this a length).
|
||||
# Any additional bytes are username.
|
||||
reply = []
|
||||
reply[0:4] = response.message[0:4]
|
||||
reply[0] = '\x01'
|
||||
if self.username:
|
||||
reply[5:] = self.username
|
||||
reply = ''.join(reply)
|
||||
|
||||
response = self.gss_vc.wrap(reply, response.encrypted)
|
||||
return response.message if response.message else ""
|
||||
except gssapi.exceptions.GSSError as err:
|
||||
# GSSAPI errored out on us; respond with None to cancel the
|
||||
# authentication
|
||||
self.ui.debug('imap', err.gen_message())
|
||||
return None
|
||||
|
||||
def __start_tls(self, imapobj):
|
||||
if 'STARTTLS' in imapobj.capabilities and not self.usessl:
|
||||
|
@ -330,18 +356,13 @@ class IMAPServer(object):
|
|||
return False
|
||||
|
||||
self.connectionlock.acquire()
|
||||
self.gssapi = False
|
||||
try:
|
||||
imapobj.authenticate('GSSAPI', self.__gssauth)
|
||||
return True
|
||||
except imapobj.error as e:
|
||||
self.gssapi = False
|
||||
raise
|
||||
else:
|
||||
imapobj.authenticate('GSSAPI', self.__gsshandler)
|
||||
self.gssapi = True
|
||||
kerberos.authGSSClientClean(self.gss_vc)
|
||||
self.gss_vc = None
|
||||
self.gss_step = self.GSS_STATE_STEP
|
||||
return True
|
||||
finally:
|
||||
self.gss_vc = None
|
||||
self.connectionlock.release()
|
||||
|
||||
def __authn_cram_md5(self, imapobj):
|
||||
|
@ -353,6 +374,10 @@ class IMAPServer(object):
|
|||
return True
|
||||
|
||||
def __authn_xoauth2(self, imapobj):
|
||||
if self.oauth2_refresh_token is None \
|
||||
and self.oauth2_access_token is None:
|
||||
return False
|
||||
|
||||
imapobj.authenticate('XOAUTH2', self.__xoauth2handler)
|
||||
return True
|
||||
|
||||
|
@ -527,6 +552,8 @@ class IMAPServer(object):
|
|||
elif self.usessl:
|
||||
self.ui.connecting(
|
||||
self.repos.getname(), self.hostname, self.port)
|
||||
self.ui.debug('imap', "%s: level '%s', version '%s'"%
|
||||
(self.repos.getname(), self.tlslevel, self.sslversion))
|
||||
imapobj = imaplibutil.WrappedIMAP4_SSL(
|
||||
host=self.hostname,
|
||||
port=self.port,
|
||||
|
@ -617,7 +644,8 @@ class IMAPServer(object):
|
|||
if self.port != 993:
|
||||
reason = "Could not connect via SSL to host '%s' and non-s"\
|
||||
"tandard ssl port %d configured. Make sure you connect"\
|
||||
" to the correct port."% (self.hostname, self.port)
|
||||
" to the correct port. Got: %s"% (
|
||||
self.hostname, self.port, e)
|
||||
else:
|
||||
reason = "Unknown SSL protocol connecting to host '%s' for "\
|
||||
"repository '%s'. OpenSSL responded:\n%s"\
|
||||
|
@ -677,8 +705,7 @@ class IMAPServer(object):
|
|||
self.assignedconnections = []
|
||||
self.availableconnections = []
|
||||
self.lastowner = {}
|
||||
# reset kerberos state
|
||||
self.gss_step = self.GSS_STATE_STEP
|
||||
# reset GSSAPI state
|
||||
self.gss_vc = None
|
||||
self.gssapi = False
|
||||
|
||||
|
@ -794,13 +821,14 @@ class IdleThread(object):
|
|||
localrepos = account.localrepos
|
||||
remoterepos = account.remoterepos
|
||||
statusrepos = account.statusrepos
|
||||
remotefolder = remoterepos.getfolder(self.folder)
|
||||
remotefolder = remoterepos.getfolder(self.folder, decode=False)
|
||||
|
||||
hook = account.getconf('presynchook', '')
|
||||
account.callhook(hook)
|
||||
hook_env = {
|
||||
'OIMAP_ACCOUNT_NAME': account.getname(),
|
||||
}
|
||||
account.callhook('presynchook', hook_env)
|
||||
offlineimap.accounts.syncfolder(account, remotefolder, quick=False)
|
||||
hook = account.getconf('postsynchook', '')
|
||||
account.callhook(hook)
|
||||
account.callhook('postsynchook', hook_env)
|
||||
|
||||
ui = getglobalui()
|
||||
ui.unregisterthread(currentThread()) #syncfolder registered the thread
|
||||
|
|
|
@ -17,6 +17,8 @@
|
|||
|
||||
import re
|
||||
import string
|
||||
import binascii
|
||||
import codecs
|
||||
from offlineimap.ui import getglobalui
|
||||
|
||||
|
||||
|
@ -370,3 +372,85 @@ def decode_mailbox_name(name):
|
|||
return ret.decode('utf-7').encode('utf-8')
|
||||
except (UnicodeDecodeError, UnicodeEncodeError):
|
||||
return name
|
||||
|
||||
# Functionality to convert folder names encoded in IMAP_utf_7 to utf_8.
|
||||
# This is achieved by defining 'imap4_utf_7' as a proper encoding scheme.
|
||||
|
||||
# Public API, to be used in repository definitions
|
||||
|
||||
def IMAP_utf8(foldername):
|
||||
"""Convert IMAP4_utf_7 encoded string to utf-8"""
|
||||
return foldername.decode('imap4-utf-7').encode('utf-8')
|
||||
|
||||
def utf8_IMAP(foldername):
|
||||
"""Convert utf-8 encoded string to IMAP4_utf_7"""
|
||||
return foldername.decode('utf-8').encode('imap4-utf-7')
|
||||
|
||||
# Codec definition
|
||||
|
||||
def modified_base64(s):
|
||||
s = s.encode('utf-16be')
|
||||
return binascii.b2a_base64(s).rstrip('\n=').replace('/', ',')
|
||||
|
||||
def doB64(_in, r):
|
||||
if _in:
|
||||
r.append('&%s-' % modified_base64(''.join(_in)))
|
||||
del _in[:]
|
||||
|
||||
def encoder(s):
|
||||
r = []
|
||||
_in = []
|
||||
for c in s:
|
||||
ordC = ord(c)
|
||||
if 0x20 <= ordC <= 0x25 or 0x27 <= ordC <= 0x7e:
|
||||
doB64(_in, r)
|
||||
r.append(c)
|
||||
elif c == '&':
|
||||
doB64(_in, r)
|
||||
r.append('&-')
|
||||
else:
|
||||
_in.append(c)
|
||||
doB64(_in, r)
|
||||
return (str(''.join(r)), len(s))
|
||||
|
||||
# decoding
|
||||
def modified_unbase64(s):
|
||||
b = binascii.a2b_base64(s.replace(',', '/') + '===')
|
||||
return unicode(b, 'utf-16be')
|
||||
|
||||
def decoder(s):
|
||||
r = []
|
||||
decode = []
|
||||
for c in s:
|
||||
if c == '&' and not decode:
|
||||
decode.append('&')
|
||||
elif c == '-' and decode:
|
||||
if len(decode) == 1:
|
||||
r.append('&')
|
||||
else:
|
||||
r.append(modified_unbase64(''.join(decode[1:])))
|
||||
decode = []
|
||||
elif decode:
|
||||
decode.append(c)
|
||||
else:
|
||||
r.append(c)
|
||||
|
||||
if decode:
|
||||
r.append(modified_unbase64(''.join(decode[1:])))
|
||||
bin_str = ''.join(r)
|
||||
return (bin_str, len(s))
|
||||
|
||||
class StreamReader(codecs.StreamReader):
|
||||
def decode(self, s, errors='strict'):
|
||||
return decoder(s)
|
||||
|
||||
class StreamWriter(codecs.StreamWriter):
|
||||
def decode(self, s, errors='strict'):
|
||||
return encoder(s)
|
||||
|
||||
def imap4_utf_7(name):
|
||||
if name == 'imap4-utf-7':
|
||||
return (encoder, decoder, StreamReader, StreamWriter)
|
||||
|
||||
|
||||
codecs.register(imap4_utf_7)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# OfflineIMAP initialization code
|
||||
# Copyright (C) 2002-2016 John Goerzen & contributors
|
||||
# Copyright (C) 2002-2017 John Goerzen & contributors
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -72,6 +72,17 @@ class OfflineImap(object):
|
|||
oi.run()
|
||||
"""
|
||||
|
||||
def get_env_info(self):
|
||||
info = "imaplib2 v%s (%s), Python v%s"% (
|
||||
imaplib.__version__, imaplib.DESC, PYTHON_VERSION
|
||||
)
|
||||
try:
|
||||
import ssl
|
||||
info = "%s, %s"% (info, ssl.OPENSSL_VERSION)
|
||||
except:
|
||||
pass
|
||||
return info
|
||||
|
||||
def run(self):
|
||||
"""Parse the commandline and invoke everything"""
|
||||
# next line also sets self.config and self.ui
|
||||
|
@ -180,9 +191,8 @@ class OfflineImap(object):
|
|||
glob.set_options(options)
|
||||
|
||||
if options.version:
|
||||
print("offlineimap v%s, imaplib2 v%s (%s), Python v%s"% (
|
||||
offlineimap.__version__, imaplib.__version__, imaplib.DESC,
|
||||
PYTHON_VERSION)
|
||||
print("offlineimap v%s, %s"% (
|
||||
offlineimap.__version__, self.get_env_info())
|
||||
)
|
||||
sys.exit(0)
|
||||
|
||||
|
@ -273,6 +283,7 @@ class OfflineImap(object):
|
|||
|
||||
# Welcome blurb.
|
||||
self.ui.init_banner()
|
||||
self.ui.info(self.get_env_info())
|
||||
|
||||
if options.debugtype:
|
||||
self.ui.logger.setLevel(logging.DEBUG)
|
||||
|
@ -422,8 +433,9 @@ class OfflineImap(object):
|
|||
accounts.Account.set_abort_event(self.config, 2)
|
||||
elif sig in (signal.SIGTERM, signal.SIGINT, signal.SIGHUP):
|
||||
# tell each account to ABORT ASAP (ctrl-c)
|
||||
getglobalui().warn("Terminating NOW (this may "\
|
||||
"take a few seconds)...")
|
||||
getglobalui().warn("Preparing to shutdown after sync (this may "\
|
||||
"take some time), press CTRL-C three "\
|
||||
"times to shutdown immediately")
|
||||
accounts.Account.set_abort_event(self.config, 3)
|
||||
if 'thread' in self.ui.debuglist:
|
||||
self.__dumpstacks(5)
|
||||
|
|
|
@ -32,6 +32,7 @@ _mbLock = Lock()
|
|||
_mbnames = None
|
||||
|
||||
|
||||
# Called at sync time for each folder.
|
||||
def add(accountname, folder_root, foldername):
|
||||
global _mbnames
|
||||
if _mbnames.is_enabled() is not True:
|
||||
|
@ -41,12 +42,14 @@ def add(accountname, folder_root, foldername):
|
|||
_mbnames.addAccountFolder(accountname, folder_root, foldername)
|
||||
|
||||
|
||||
# Called once.
|
||||
def init(conf, ui, dry_run):
|
||||
global _mbnames
|
||||
if _mbnames is None:
|
||||
_mbnames = _Mbnames(conf, ui, dry_run)
|
||||
|
||||
|
||||
# Called once.
|
||||
def prune(accounts):
|
||||
global _mbnames
|
||||
if _mbnames.is_enabled() is True:
|
||||
|
@ -55,6 +58,7 @@ def prune(accounts):
|
|||
_mbnames.pruneAll()
|
||||
|
||||
|
||||
# Called once.
|
||||
def write():
|
||||
"""Write the mbnames file."""
|
||||
|
||||
|
@ -66,6 +70,7 @@ def write():
|
|||
_mbnames.write()
|
||||
|
||||
|
||||
# Called as soon as all the folders are synced for the account.
|
||||
def writeIntermediateFile(accountname):
|
||||
"""Write intermediate mbnames file."""
|
||||
|
||||
|
@ -93,7 +98,8 @@ class _IntermediateMbnames(object):
|
|||
self._dryrun = dry_run
|
||||
|
||||
def add(self, foldername):
|
||||
self._foldernames.append(foldername)
|
||||
if foldername not in self._foldernames:
|
||||
self._foldernames.append(foldername)
|
||||
|
||||
def get_folder_root(self):
|
||||
return self._folder_root
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
""" Base repository support """
|
||||
|
||||
# Copyright (C) 2002-2016 John Goerzen & contributors
|
||||
# Copyright (C) 2002-2017 John Goerzen & contributors
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -165,7 +165,13 @@ class BaseRepository(CustomConfig.ConfigHelperMixin):
|
|||
def deletefolder(self, foldername):
|
||||
raise NotImplementedError
|
||||
|
||||
def getfolder(self, foldername):
|
||||
def getfolder(self, foldername, decode=True):
|
||||
"""Get the folder for this repo.
|
||||
|
||||
WARNING: the signature changes whether it's remote or local:
|
||||
- remote types have the decode arg
|
||||
- local types don't have the decode arg
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def sync_folder_structure(self, local_repo, status_repo):
|
||||
|
@ -242,7 +248,7 @@ class BaseRepository(CustomConfig.ConfigHelperMixin):
|
|||
# Get IMAPFolder and see if the reverse nametrans works fine.
|
||||
# TODO: getfolder() works only because we succeed in getting
|
||||
# inexisting folders which I would like to change. Take care!
|
||||
tmp_remotefolder = remote_repo.getfolder(remote_name)
|
||||
tmp_remotefolder = remote_repo.getfolder(remote_name, decode=False)
|
||||
loop_name = tmp_remotefolder.getvisiblename().replace(
|
||||
remote_repo.getsep(), local_repo.getsep())
|
||||
if local_name != loop_name:
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
from offlineimap.repository.IMAP import IMAPRepository
|
||||
from offlineimap import folder, OfflineImapError
|
||||
|
||||
|
||||
class GmailRepository(IMAPRepository):
|
||||
"""Gmail IMAP repository.
|
||||
|
||||
|
@ -87,9 +88,9 @@ class GmailRepository(IMAPRepository):
|
|||
def getpreauthtunnel(self):
|
||||
return None
|
||||
|
||||
def getfolder(self, foldername):
|
||||
def getfolder(self, foldername, decode=True):
|
||||
return self.getfoldertype()(self.imapserver, foldername,
|
||||
self)
|
||||
self, decode)
|
||||
|
||||
def getfoldertype(self):
|
||||
return folder.Gmail.GmailFolder
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
""" IMAP repository support """
|
||||
|
||||
# Copyright (C) 2002-2016 John Goerzen & contributors
|
||||
# Copyright (C) 2002-2019 John Goerzen & contributors
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -222,7 +222,7 @@ class IMAPRepository(BaseRepository):
|
|||
return self.getconfint('remoteport', None)
|
||||
|
||||
def getipv6(self):
|
||||
return self.getconfboolean('ipv6', False)
|
||||
return self.getconfboolean('ipv6', None)
|
||||
|
||||
def getssl(self):
|
||||
return self.getconfboolean('ssl', True)
|
||||
|
@ -293,7 +293,7 @@ class IMAPRepository(BaseRepository):
|
|||
comma-separated fingerprints in hex form."""
|
||||
|
||||
value = self.getconf('cert_fingerprint', "")
|
||||
return [f.strip().lower() for f in value.split(',') if f]
|
||||
return [f.strip().lower().replace(":", "") for f in value.split(',') if f]
|
||||
|
||||
def setoauth2_request_url(self, url):
|
||||
self.oauth2_request_url = url
|
||||
|
@ -428,10 +428,10 @@ class IMAPRepository(BaseRepository):
|
|||
# No strategy yielded a password!
|
||||
return None
|
||||
|
||||
def getfolder(self, foldername):
|
||||
def getfolder(self, foldername, decode=True):
|
||||
"""Return instance of OfflineIMAP representative folder."""
|
||||
|
||||
return self.getfoldertype()(self.imapserver, foldername, self)
|
||||
return self.getfoldertype()(self.imapserver, foldername, self, decode)
|
||||
|
||||
def getfoldertype(self):
|
||||
return folder.IMAP.IMAPFolder
|
||||
|
@ -480,8 +480,7 @@ class IMAPRepository(BaseRepository):
|
|||
flaglist = [x.lower() for x in imaputil.flagsplit(flags)]
|
||||
if '\\noselect' in flaglist:
|
||||
continue
|
||||
foldername = imaputil.dequote(name)
|
||||
retval.append(self.getfoldertype()(self.imapserver, foldername,
|
||||
retval.append(self.getfoldertype()(self.imapserver, name,
|
||||
self))
|
||||
# Add all folderincludes
|
||||
if len(self.folderincludes):
|
||||
|
@ -489,7 +488,7 @@ class IMAPRepository(BaseRepository):
|
|||
try:
|
||||
for foldername in self.folderincludes:
|
||||
try:
|
||||
imapobj.select(foldername, readonly=True)
|
||||
imapobj.select(imaputil.utf8_IMAP(foldername), readonly=True)
|
||||
except OfflineImapError as e:
|
||||
# couldn't select this folderinclude, so ignore folder.
|
||||
if e.severity > OfflineImapError.ERROR.FOLDER:
|
||||
|
@ -498,7 +497,7 @@ class IMAPRepository(BaseRepository):
|
|||
'Invalid folderinclude:')
|
||||
continue
|
||||
retval.append(self.getfoldertype()(
|
||||
self.imapserver, foldername, self))
|
||||
self.imapserver, foldername, self, decode=False))
|
||||
finally:
|
||||
self.imapserver.releaseconnection(imapobj)
|
||||
|
||||
|
@ -525,6 +524,8 @@ class IMAPRepository(BaseRepository):
|
|||
def deletefolder(self, foldername):
|
||||
"""Delete a folder on the IMAP server."""
|
||||
|
||||
if self.account.utf_8_support:
|
||||
foldername = imaputil.utf8_IMAP(foldername)
|
||||
imapobj = self.imapserver.acquireconnection()
|
||||
try:
|
||||
result = imapobj.delete(foldername)
|
||||
|
@ -544,18 +545,35 @@ class IMAPRepository(BaseRepository):
|
|||
|
||||
:param foldername: Full path of the folder to be created."""
|
||||
|
||||
if foldername is '':
|
||||
if foldername == '':
|
||||
return
|
||||
|
||||
if self.getreference():
|
||||
foldername = self.getreference() + self.getsep() + foldername
|
||||
if not foldername: # Create top level folder as folder separator.
|
||||
foldername = self.getsep()
|
||||
self.makefolder_single(foldername)
|
||||
return
|
||||
|
||||
parts = foldername.split(self.getsep())
|
||||
folder_paths = [self.getsep().join(parts[:n + 1]) for n in range(len(parts))]
|
||||
for folder_path in folder_paths:
|
||||
try:
|
||||
self.makefolder_single(folder_path)
|
||||
except OfflineImapError as e:
|
||||
reasonLower = e.reason.lower() # Handle reasons '[ALREADYEXISTS]' and 'Mailbox already exists!' @chris001
|
||||
if not ('already' in reasonLower and 'exists' in reasonLower):
|
||||
raise
|
||||
|
||||
def makefolder_single(self, foldername):
|
||||
self.ui.makefolder(self, foldername)
|
||||
if self.account.dryrun:
|
||||
return
|
||||
imapobj = self.imapserver.acquireconnection()
|
||||
try:
|
||||
if self.account.utf_8_support:
|
||||
foldername = imaputil.utf8_IMAP(foldername)
|
||||
|
||||
result = imapobj.create(foldername)
|
||||
if result[0] != 'OK':
|
||||
raise OfflineImapError("Folder '%s'[%s] could not be created. "
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# Local status cache repository support
|
||||
# Copyright (C) 2002-2016 John Goerzen & contributors
|
||||
# Copyright (C) 2002-2017 John Goerzen & contributors
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -20,30 +20,37 @@ import os
|
|||
from offlineimap.folder.LocalStatus import LocalStatusFolder
|
||||
from offlineimap.folder.LocalStatusSQLite import LocalStatusSQLiteFolder
|
||||
from offlineimap.repository.Base import BaseRepository
|
||||
from offlineimap.error import OfflineImapError
|
||||
|
||||
|
||||
class LocalStatusRepository(BaseRepository):
|
||||
def __init__(self, reposname, account):
|
||||
BaseRepository.__init__(self, reposname, account)
|
||||
|
||||
# class and root for all backends
|
||||
# class and root for all backends.
|
||||
self.backends = {}
|
||||
self.backends['sqlite'] = {
|
||||
'class': LocalStatusSQLiteFolder,
|
||||
'root': os.path.join(account.getaccountmeta(), 'LocalStatus-sqlite')
|
||||
}
|
||||
|
||||
self.backends['plain'] = {
|
||||
'class': LocalStatusFolder,
|
||||
'root': os.path.join(account.getaccountmeta(), 'LocalStatus')
|
||||
}
|
||||
|
||||
# Set class and root for the configured backend
|
||||
self.setup_backend(self.account.getconf('status_backend', 'sqlite'))
|
||||
if self.account.getconf('status_backend', None) is not None:
|
||||
raise OfflineImapError(
|
||||
"the 'status_backend' configuration option is not supported"
|
||||
" anymore; please, remove this configuration option.",
|
||||
OfflineImapError.ERROR.REPO
|
||||
)
|
||||
# Set class and root for sqlite.
|
||||
self.setup_backend('sqlite')
|
||||
|
||||
if not os.path.exists(self.root):
|
||||
os.mkdir(self.root, 0o700)
|
||||
|
||||
# self._folders is a dict of name:LocalStatusFolders()
|
||||
# self._folders is a dict of name:LocalStatusFolders().
|
||||
self._folders = {}
|
||||
|
||||
def _instanciatefolder(self, foldername):
|
||||
|
@ -55,10 +62,6 @@ class LocalStatusRepository(BaseRepository):
|
|||
self.root = self.backends[backend]['root']
|
||||
self.LocalStatusFolderClass = self.backends[backend]['class']
|
||||
|
||||
else:
|
||||
raise SyntaxWarning("Unknown status_backend '%s' for account '%s'"%
|
||||
(backend, self.account.name))
|
||||
|
||||
def import_other_backend(self, folder):
|
||||
for bk, dic in self.backends.items():
|
||||
# Skip folder's own type.
|
||||
|
|
|
@ -15,13 +15,12 @@
|
|||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
|
||||
from threading import Lock, Thread, BoundedSemaphore, currentThread
|
||||
from threading import Lock, Thread, BoundedSemaphore
|
||||
try:
|
||||
from Queue import Queue, Empty
|
||||
except ImportError: # python3
|
||||
from queue import Queue, Empty
|
||||
import traceback
|
||||
import os.path
|
||||
from offlineimap.ui import getglobalui
|
||||
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# Curses-based interfaces
|
||||
# Copyright (C) 2003-2016 John Goerzen & contributors.
|
||||
# Copyright (C) 2003-2018 John Goerzen & contributors.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -137,7 +137,13 @@ class CursesAccountFrame:
|
|||
sleepstr = '%3d:%02d'% (secs // 60, secs % 60) if secs else 'active'
|
||||
accstr = '%s: [%s] %12.12s: '% (self.acc_num, sleepstr, self.account)
|
||||
|
||||
self.ui.exec_locked(self.window.addstr, 0, 0, accstr)
|
||||
def addstr():
|
||||
try:
|
||||
self.window.addstr(0, 0, accstr)
|
||||
except curses.error as e: # Occurs when the terminal is very small
|
||||
pass
|
||||
self.ui.exec_locked(addstr);
|
||||
|
||||
self.location = len(accstr)
|
||||
|
||||
def setwindow(self, curses_win, acc_num):
|
||||
|
@ -211,7 +217,10 @@ class CursesThreadFrame:
|
|||
|
||||
def display(self):
|
||||
def locked_display():
|
||||
self.window.addch(self.y, self.x, '@', self.curses_color)
|
||||
try:
|
||||
self.window.addch(self.y, self.x, '@', self.curses_color)
|
||||
except curses.error: # Occurs when the terminal is very small
|
||||
pass
|
||||
self.window.refresh()
|
||||
# lock the curses IO while fudging stuff
|
||||
self.ui.exec_locked(locked_display)
|
||||
|
@ -315,11 +324,11 @@ class CursesLogHandler(logging.StreamHandler):
|
|||
y,x = self.ui.logwin.getyx()
|
||||
if y or x: self.ui.logwin.addch(10) # no \n before 1st item
|
||||
self.ui.logwin.addstr(log_str, color)
|
||||
self.ui.logwin.noutrefresh()
|
||||
self.ui.stdscr.refresh()
|
||||
finally:
|
||||
self.ui.unlock()
|
||||
self.ui.tframe_lock.release()
|
||||
self.ui.logwin.noutrefresh()
|
||||
self.ui.stdscr.refresh()
|
||||
|
||||
class Blinkenlights(UIBase, CursesUtil):
|
||||
"""Curses-cased fancy UI.
|
||||
|
@ -549,7 +558,7 @@ class Blinkenlights(UIBase, CursesUtil):
|
|||
def mainException(self):
|
||||
UIBase.mainException(self)
|
||||
|
||||
def getpass(self, accountname, config, errmsg=None):
|
||||
def getpass(self, username, config, errmsg=None):
|
||||
# disable the hotkeys inputhandler
|
||||
self.inputhandler.input_acquire()
|
||||
|
||||
|
@ -558,8 +567,8 @@ class Blinkenlights(UIBase, CursesUtil):
|
|||
try:
|
||||
#s.gettf().setcolor('white')
|
||||
self.warn(" *** Input Required")
|
||||
self.warn(" *** Please enter password for account %s: " % \
|
||||
accountname)
|
||||
self.warn(" *** Please enter password for user '%s': " % \
|
||||
username)
|
||||
self.logwin.refresh()
|
||||
password = self.logwin.getstr()
|
||||
finally:
|
||||
|
@ -611,11 +620,12 @@ class Blinkenlights(UIBase, CursesUtil):
|
|||
color = curses.A_REVERSE
|
||||
self.bannerwin.clear() # Delete old content (eg before resizes)
|
||||
self.bannerwin.bkgd(' ', color) # Fill background with that color
|
||||
string = "%s %s"% (offlineimap.__productname__,
|
||||
offlineimap.__version__)
|
||||
self.bannerwin.addstr(0, 0, string, color)
|
||||
self.bannerwin.addstr(0, self.width -len(offlineimap.__copyright__) -1,
|
||||
offlineimap.__copyright__, color)
|
||||
string = "%s %s" % (offlineimap.__productname__,
|
||||
offlineimap.__version__)
|
||||
spaces = " " * max(1, (self.width - len(offlineimap.__copyright__)
|
||||
- len(string) - 1))
|
||||
string = "%s%s%s" % (string, spaces, offlineimap.__copyright__)
|
||||
self.bannerwin.addnstr(0, 0, string, self.width - 1, color)
|
||||
self.bannerwin.noutrefresh()
|
||||
|
||||
def draw_logwin(self):
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (C) 2007-2016 John Goerzen & contributors.
|
||||
# Copyright (C) 2007-2018 John Goerzen & contributors.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -182,15 +182,15 @@ class MachineUI(UIBase):
|
|||
return 0
|
||||
|
||||
|
||||
def getpass(s, accountname, config, errmsg=None):
|
||||
def getpass(s, username, config, errmsg=None):
|
||||
if errmsg:
|
||||
s._printData(s.logger.warning,
|
||||
'getpasserror', "%s\n%s"% (accountname, errmsg),
|
||||
'getpasserror', "%s\n%s"% (username, errmsg),
|
||||
False)
|
||||
|
||||
s._log_con_handler.acquire() # lock the console output
|
||||
try:
|
||||
s._printData(s.logger.info, 'getpass', accountname)
|
||||
s._printData(s.logger.info, 'getpass', username)
|
||||
return (sys.stdin.readline()[:-1])
|
||||
finally:
|
||||
s._log_con_handler.release()
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# TTY UI
|
||||
# Copyright (C) 2002-2015 John Goerzen & contributors
|
||||
# Copyright (C) 2002-2018 John Goerzen & contributors
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -76,14 +76,14 @@ class TTYUI(UIBase):
|
|||
|
||||
return sys.stdout.isatty() and sys.stdin.isatty()
|
||||
|
||||
def getpass(self, accountname, config, errmsg=None):
|
||||
def getpass(self, username, config, errmsg=None):
|
||||
"""TTYUI backend is capable of querying the password."""
|
||||
|
||||
if errmsg:
|
||||
self.warn("%s: %s"% (accountname, errmsg))
|
||||
self.warn("%s: %s"% (username, errmsg))
|
||||
self._log_con_handler.acquire() # lock the console output
|
||||
try:
|
||||
return getpass("Enter password for account '%s': " % accountname)
|
||||
return getpass("Enter password for user '%s': " % username)
|
||||
finally:
|
||||
self._log_con_handler.release()
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# UI base class
|
||||
# Copyright (C) 2002-2016 John Goerzen & contributors.
|
||||
# Copyright (C) 2002-2018 John Goerzen & contributors.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -96,8 +96,13 @@ class UIBase(object):
|
|||
def setup_sysloghandler(self):
|
||||
"""Backend specific syslog handler."""
|
||||
|
||||
if sys.platform == 'darwin':
|
||||
address = '/var/run/syslog'
|
||||
else:
|
||||
address = '/dev/log'
|
||||
|
||||
# create syslog handler
|
||||
ch = logging.handlers.SysLogHandler('/dev/log')
|
||||
ch = logging.handlers.SysLogHandler(address)
|
||||
# create formatter and add it to the handlers
|
||||
self.formatter = logging.Formatter("%(message)s")
|
||||
ch.setFormatter(self.formatter)
|
||||
|
@ -257,7 +262,7 @@ class UIBase(object):
|
|||
|
||||
################################################## INPUT
|
||||
|
||||
def getpass(self, accountname, config, errmsg = None):
|
||||
def getpass(self, username, config, errmsg = None):
|
||||
raise NotImplementedError("Prompting for a password is not supported"
|
||||
" in this UI backend.")
|
||||
|
||||
|
@ -396,9 +401,9 @@ class UIBase(object):
|
|||
def copyingmessage(self, uid, num, num_to_copy, src, destfolder):
|
||||
"""Output a log line stating which message we copy."""
|
||||
|
||||
self.logger.info("Copy message UID %s (%d/%d) %s:%s -> %s"% (
|
||||
self.logger.info("Copy message UID %s (%d/%d) %s:%s -> %s:%s"% (
|
||||
uid, num, num_to_copy, src.repository, src,
|
||||
destfolder.repository))
|
||||
destfolder.repository, destfolder))
|
||||
|
||||
def deletingmessages(self, uidlist, destlist):
|
||||
ds = self.folderlist(destlist)
|
||||
|
|
|
@ -24,7 +24,7 @@ UI_LIST = {'ttyui': TTY.TTYUI,
|
|||
'syslog': Noninteractive.Syslog,
|
||||
'machineui': Machine.MachineUI}
|
||||
|
||||
#add Blinkenlights UI if it imports correctly (curses installed)
|
||||
# add Blinkenlights UI if it imports correctly (curses installed)
|
||||
try:
|
||||
from offlineimap.ui import Curses
|
||||
UI_LIST['blinkenlights'] = Curses.Blinkenlights
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright 2014-2006 Eygene A. Ryabinkin & contributors.
|
||||
# Copyright 2006-2018 Eygene A. Ryabinkin & contributors.
|
||||
#
|
||||
# Module that supports distribution-specific functions.
|
||||
|
||||
|
@ -18,6 +18,8 @@ __DEF_OS_LOCATIONS = {
|
|||
'darwin': [
|
||||
# MacPorts, port curl-ca-bundle
|
||||
'/opt/local/share/curl/curl-ca-bundle.crt',
|
||||
# homebrew, package openssl
|
||||
'/usr/local/etc/openssl/cert.pem',
|
||||
],
|
||||
'linux-ubuntu': '/etc/ssl/certs/ca-certificates.crt',
|
||||
'linux-debian': '/etc/ssl/certs/ca-certificates.crt',
|
||||
|
@ -26,6 +28,7 @@ __DEF_OS_LOCATIONS = {
|
|||
'linux-redhat': '/etc/pki/tls/certs/ca-bundle.crt',
|
||||
'linux-suse': '/etc/ssl/ca-bundle.pem',
|
||||
'linux-opensuse': '/etc/ssl/ca-bundle.pem',
|
||||
'linux-arch': '/etc/ssl/certs/ca-certificates.crt',
|
||||
}
|
||||
|
||||
|
||||
|
@ -45,7 +48,9 @@ def get_os_name():
|
|||
if OS.startswith('linux'):
|
||||
DISTRO = platform.linux_distribution()[0]
|
||||
if DISTRO:
|
||||
OS = OS + "-%s" % DISTRO.split()[0].lower()
|
||||
OS = OS + "-%s" % DISTRO.split()[0].lower()
|
||||
if os.path.exists('/etc/arch-release'):
|
||||
OS = "linux-arch"
|
||||
|
||||
return OS
|
||||
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
__productname__ = 'OfflineIMAP'
|
||||
# Expecting trailing "-rcN" or "" for stable releases.
|
||||
__version__ = "7.3.4"
|
||||
__copyright__ = "Copyright 2002-2021 John Goerzen & contributors"
|
||||
__author__ = "John Goerzen"
|
||||
__author_email__= "offlineimap-project@lists.alioth.debian.org"
|
||||
__description__ = "Disconnected Universal IMAP Mail Synchronization/Reader Support"
|
||||
__license__ = "Licensed under the GNU GPL v2 or any later version (with an OpenSSL exception)"
|
||||
__bigcopyright__ = """%(__productname__)s %(__version__)s
|
||||
%(__license__)s""" % locals()
|
||||
__homepage__ = "http://www.offlineimap.org"
|
||||
|
||||
banner = __bigcopyright__
|
|
@ -25,7 +25,7 @@ this virtual_imaplib2 or we might go into troubles.
|
|||
DESC = None
|
||||
|
||||
_SUPPORTED_RELEASE = 2
|
||||
_SUPPORTED_REVISION = 55
|
||||
_SUPPORTED_REVISION = 57
|
||||
|
||||
try:
|
||||
# Try any imaplib2 in PYTHONPATH first. This allows both maintainers of
|
||||
|
|
|
@ -1,2 +1,5 @@
|
|||
# Requirements
|
||||
six
|
||||
# Minimal requirements defined in setup.py
|
||||
-e .
|
||||
# Extra "optional" requirements
|
||||
gssapi[kerberos]
|
||||
portalocker[cygwin]
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
#!/bin/bash
|
||||
#!/bin/sh
|
||||
#
|
||||
# Licence: this file is in the public deomain.
|
||||
# Licence: this file is in the public domain.
|
||||
#
|
||||
# Download and configure the repositories of the website or wiki.
|
||||
|
||||
|
@ -10,7 +10,7 @@ github_remote=$2
|
|||
#
|
||||
# TODO
|
||||
#
|
||||
function final_note () {
|
||||
final_note () {
|
||||
cat <<EOF
|
||||
|
||||
Now, you can fork the repository into Github from $2
|
||||
|
@ -22,7 +22,7 @@ and add a reference to it in your local copy:
|
|||
EOF
|
||||
}
|
||||
|
||||
function setup () {
|
||||
setup () {
|
||||
target_dir=$1
|
||||
remote_url=$2
|
||||
|
||||
|
@ -49,7 +49,7 @@ function setup () {
|
|||
fi
|
||||
}
|
||||
|
||||
function configure_website () {
|
||||
configure_website () {
|
||||
renderer='./render.sh'
|
||||
|
||||
echo "Found Github username: '$1'"
|
||||
|
@ -65,7 +65,7 @@ function configure_website () {
|
|||
fi
|
||||
}
|
||||
|
||||
function configure_wiki () {
|
||||
configure_wiki () {
|
||||
: # noop
|
||||
}
|
||||
|
||||
|
|
37
setup.py
37
setup.py
|
@ -5,7 +5,7 @@
|
|||
# IMAP synchronization
|
||||
# Module: installer
|
||||
# COPYRIGHT #
|
||||
# Copyright (C) 2002 - 2006 John Goerzen
|
||||
# Copyright (C) 2002 - 2020 John Goerzen & contributors
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -19,13 +19,17 @@
|
|||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
|
||||
import os
|
||||
from distutils.core import setup, Command
|
||||
import offlineimap
|
||||
import logging
|
||||
from test.OLItest import TextTestRunner, TestLoader, OLITestLib
|
||||
|
||||
from os import path
|
||||
here = path.abspath(path.dirname(__file__))
|
||||
|
||||
# load __version__, __doc__, __author_, ...
|
||||
exec(open(path.join(here, 'offlineimap', 'version.py')).read())
|
||||
|
||||
class TestCommand(Command):
|
||||
"""runs the OLI testsuite"""
|
||||
|
@ -42,6 +46,11 @@ class TestCommand(Command):
|
|||
pass
|
||||
|
||||
def run(self):
|
||||
# Import the test classes here instead of at the begin of the module
|
||||
# to avoid an implicit dependency of the 'offlineimap' module
|
||||
# in the setup.py (which may run *before* offlineimap is installed)
|
||||
from test.OLItest import TextTestRunner, TestLoader, OLITestLib
|
||||
|
||||
logging.basicConfig(format='%(message)s')
|
||||
# set credentials and OfflineImap command to be executed:
|
||||
OLITestLib(cred_file='./test/credentials.conf', cmd='./offlineimap.py')
|
||||
|
@ -49,19 +58,25 @@ class TestCommand(Command):
|
|||
#TODO: failfast does not seem to exist in python2.6?
|
||||
TextTestRunner(verbosity=2,failfast=True).run(suite)
|
||||
|
||||
reqs = [
|
||||
'six',
|
||||
'rfc6555'
|
||||
]
|
||||
|
||||
setup(name = "offlineimap",
|
||||
version = offlineimap.__version__,
|
||||
description = offlineimap.__description__,
|
||||
author = offlineimap.__author__,
|
||||
author_email = offlineimap.__author_email__,
|
||||
url = offlineimap.__homepage__,
|
||||
version = __version__,
|
||||
description = __description__,
|
||||
long_description = __description__,
|
||||
author = __author__,
|
||||
author_email = __author_email__,
|
||||
url = __homepage__,
|
||||
packages = ['offlineimap', 'offlineimap.folder',
|
||||
'offlineimap.repository', 'offlineimap.ui',
|
||||
'offlineimap.utils'],
|
||||
scripts = ['bin/offlineimap'],
|
||||
license = offlineimap.__copyright__ + \
|
||||
license = __copyright__ + \
|
||||
", Licensed under the GPL version 2",
|
||||
cmdclass = { 'test': TestCommand}
|
||||
cmdclass = { 'test': TestCommand},
|
||||
install_requires = reqs
|
||||
)
|
||||
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
name: offlineimap
|
||||
version: git
|
||||
summary: OfflineIMAP
|
||||
description: |
|
||||
OfflineIMAP is software that downloads your email mailbox(es) as local
|
||||
Maildirs. OfflineIMAP will synchronize both sides via IMAP.
|
||||
|
||||
grade: devel
|
||||
confinement: devmode
|
||||
|
||||
apps:
|
||||
offlineimap:
|
||||
command: bin/offlineimap
|
||||
|
||||
parts:
|
||||
offlineimap:
|
||||
plugin: python
|
||||
python-version: python2
|
||||
source: .
|
|
@ -1,2 +1,5 @@
|
|||
credentials.conf
|
||||
tmp_*
|
||||
tmp_*
|
||||
*.pyc
|
||||
OLItest/*.pyc
|
||||
tests/*.pyc
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
import imaplib
|
||||
import offlineimap.virtual_imaplib2 as imaplib
|
||||
import unittest
|
||||
import logging
|
||||
import os
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
*.pyc
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
#!/bin/env python
|
||||
# Copyright 2018 Espace LLC/espacenetworks.com. Written by @chris001.
|
||||
# This must be run from the main directory of the offlineimap project.
|
||||
# Typically this script will be run by Travis to create the config files needed for running the automated tests.
|
||||
# python ./tests/create_conf_file.py
|
||||
# Input: Seven shell environment variables.
|
||||
# Output: it writes the config settings to "filename" (./oli-travis.conf) and "additionalfilename" (./test/credentials.conf).
|
||||
# "filename" is used by normal run of ./offlineimap -c ./oli-travis.conf , "additionalfilename" is used by "pytest".
|
||||
# They are the same conf file, copie to two different locations for convenience.
|
||||
|
||||
import os
|
||||
import shutil
|
||||
try:
|
||||
import ConfigParser
|
||||
Config = ConfigParser.ConfigParser()
|
||||
except ImportError:
|
||||
import configparser
|
||||
Config = configparser.ConfigParser()
|
||||
|
||||
filename = "./oli-travis.conf"
|
||||
additionalfilename = "./test/credentials.conf" # for the 'pytest' which automatically finds and runs the unittests.
|
||||
|
||||
#TODO: detect OS we running on now, and set sslcacertfile location accordingly.
|
||||
sslcacertfile = "/etc/pki/tls/cert.pem" # CentOS 7
|
||||
sslcacertfile = "" # TODO: https://gist.github.com/1stvamp/2158128 Current Mac OSX now must download the cacertfile.
|
||||
sslcacertfile = "/etc/ssl/certs/ca-certificates.crt" # Ubuntu Trusty 14.04 (Travis linux test container 2018.)
|
||||
if os.environ["TRAVIS_OS_NAME"] == "osx":
|
||||
sslcacertfile = os.environ["OSX_BREW_SSLCACERTFILE"]
|
||||
|
||||
# lets create that config file.
|
||||
cfgfile = open(filename,'w')
|
||||
|
||||
# add the settings to the structure of the file, and lets write it out.
|
||||
sect = 'general'
|
||||
Config.add_section(sect)
|
||||
Config.set(sect,'accounts','Test')
|
||||
Config.set(sect,'maxsyncaccounts', '1')
|
||||
|
||||
sect = 'Account Test'
|
||||
Config.add_section(sect)
|
||||
Config.set(sect,'localrepository','IMAP') # Outlook.
|
||||
Config.set(sect,'remoterepository', 'Gmail')
|
||||
|
||||
### "Repository IMAP" is hardcoded in test/OLItest/TestRunner.py it should dynamically get the Repository names but it doesn't.
|
||||
sect = 'Repository IMAP' # Outlook.
|
||||
Config.add_section(sect)
|
||||
Config.set(sect,'type','IMAP')
|
||||
Config.set(sect,'remotehost', 'imap-mail.outlook.com')
|
||||
Config.set(sect,'remoteport', '993')
|
||||
Config.set(sect,'auth_mechanisms', os.environ["OUTLOOK_AUTH"])
|
||||
Config.set(sect,'ssl', 'True')
|
||||
#Config.set(sect,'tls_level', 'tls_compat') #Default is 'tls_compat'.
|
||||
#Config.set(sect,'ssl_version', 'tls1_2') # Leave this unset. Will auto select between tls1_1 and tls1_2 for tls_secure.
|
||||
Config.set(sect,'sslcacertfile', sslcacertfile)
|
||||
Config.set(sect,'remoteuser', os.environ["secure_outlook_email_address"])
|
||||
Config.set(sect,'remotepass', os.environ["secure_outlook_email_pw"])
|
||||
Config.set(sect,'createfolders', 'True')
|
||||
Config.set(sect,'folderfilter', 'lambda f: f not in ["Inbox", "[Gmail]/All Mail"]') #Capitalization of Inbox INBOX was causing runtime failure.
|
||||
#Config.set(sect,'folderfilter', 'lambda f: f not in ["[Gmail]/All Mail"]')
|
||||
|
||||
|
||||
### "Repository Gmail" is also hardcoded into test/OLItest/TestRunner.py
|
||||
sect = 'Repository Gmail'
|
||||
Config.add_section(sect)
|
||||
Config.set(sect,'type', 'Gmail')
|
||||
Config.set(sect,'remoteport', '993')
|
||||
Config.set(sect,'auth_mechanisms', os.environ["GMAIL_AUTH"])
|
||||
Config.set(sect,'oauth2_client_id', os.environ["secure_gmail_oauth2_client_id"])
|
||||
Config.set(sect,'oauth2_client_secret', os.environ["secure_gmail_oauth2_client_secret"])
|
||||
Config.set(sect,'oauth2_refresh_token', os.environ["secure_gmail_oauth2_refresh_token"])
|
||||
Config.set(sect,'remoteuser', os.environ["secure_gmail_email_address"])
|
||||
Config.set(sect,'ssl', 'True')
|
||||
#Config.set(sect,'tls_level', 'tls_compat')
|
||||
#Config.set(sect,'ssl_version', 'tls1_2')
|
||||
Config.set(sect,'sslcacertfile', sslcacertfile)
|
||||
Config.set(sect,'createfolders', 'True')
|
||||
Config.set(sect,'folderfilter', 'lambda f: f not in ["INBOX", "[Gmail]/All Mail"]')
|
||||
|
||||
Config.write(cfgfile)
|
||||
cfgfile.close()
|
||||
|
||||
shutil.copy(filename, additionalfilename)
|
|
@ -0,0 +1,4 @@
|
|||
pytest
|
||||
pytest-cov
|
||||
coverage
|
||||
codecov
|
Loading…
Reference in New Issue