├── .coveragerc ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.rst ├── COPYING ├── Changelog.maint.md ├── Changelog.md ├── MAINTAINERS.rst ├── MANIFEST.in ├── Makefile ├── README.md ├── TODO.rst ├── bin └── offlineimap ├── contrib ├── README.md ├── helpers.py ├── internet-urllib3.py ├── release.py ├── release.sh ├── store-pw-with-gpg │ ├── README.md │ ├── gpg-pw.py │ ├── offlineimaprc.sample │ └── passwords-gmail.txt ├── systemd │ ├── README.md │ ├── offlineimap-oneshot.service │ ├── offlineimap-oneshot.timer │ ├── offlineimap-oneshot@.service │ ├── offlineimap-oneshot@.timer │ ├── offlineimap.service │ └── offlineimap@.service ├── tested-by.py └── upcoming.py ├── docs ├── Makefile ├── build-uploads.sh ├── doc-src │ ├── API.rst │ ├── conf.py │ ├── dco.rst │ ├── index.rst │ ├── repository.rst │ └── ui.rst ├── manhtml │ └── .lock ├── offlineimap.known_issues.txt ├── offlineimap.txt ├── offlineimapui.txt ├── rfcs │ ├── README.md │ ├── rfc1731.IMAP4_auth.txt │ ├── rfc1732.compatibiliy_IMAP2-IMAP2bis.txt │ ├── rfc1733.models_in_IMAP4.txt │ ├── rfc1734.POP3_AUTHentication │ ├── rfc2061.compatibility_IMAP4-IMAP2bis.txt │ ├── rfc2086.IMAP4_ACL_extension.txt │ ├── rfc2087.IMAP4_QUOTA_extension.txt │ ├── rfc2088.IMAP4_non_synchronizing_literals.txt │ ├── rfc2095.IMAP-POP_AUTHorize_extension.txt │ ├── rfc2177.IMAP4_IDLE_command.txt │ ├── rfc2180.IMAP4_multi-accessed_Mailbox_practice.txt │ ├── rfc2192.IMAP_URL_scheme.txt │ ├── rfc2193.IMAP4_Mailbox_referrals.txt │ ├── rfc2195.IMAP-POP_AUTHorize_extension.txt │ ├── rfc2221.IMAP4_Login_referrals.txt │ ├── rfc2244.ACAP.txt │ ├── rfc2342.IMAP4_Namespace.txt │ ├── rfc2359.IMAP4_UIDPLUS_extension.txt │ ├── rfc2595.TLS_with_IMAP-POP3_and_ACAP.txt │ ├── rfc2683.IMAP4_Implementation_recommendations.txt │ ├── rfc2831.Obsolete_Digest_AUTHentication_as_a_SASL_mech.txt │ ├── rfc2971.IMAP4_ID_extension.txt │ ├── rfc3028.Sieve_Mail_filtering_language.txt │ ├── rfc3348.IMAP4_Child_Mailbox_extension.txt │ ├── rfc3501.IMAP4rev1.txt │ ├── rfc3502.MULTIAPPEND_extension.txt │ ├── rfc3503.Message_Disposition_Notification.txt │ ├── rfc3516.IMAP4_Binary_content_extension.txt │ ├── rfc3691.IMAP_UNSELECT_command.txt │ ├── rfc4314.IMAP4_ACL_extension.txt │ ├── rfc4315.IMAP_UIDPLUS_extension.txt │ ├── rfc4466.Collected_extensions_to_IMAP4_ABNF.txt │ ├── rfc4467.IMAP_URLAUTH_extension.txt │ ├── rfc4469.IMAP_CATENATE_extension.txt │ ├── rfc4549.Sync_operations_for_disconnected_IMAP4_Clients.txt │ ├── rfc4551.IMAP_Conditional_STORE_or_Quick_flag_changes_resync.txt │ ├── rfc4731.IMAP4_Extension_to_SEARCH_command.txt │ ├── rfc4978.IMAP_Compress_extension.txt │ ├── rfc5032.IMAP_WITHIN_Search_extension.txt │ ├── rfc5161.IMAP_ENABLE_extension.txt │ ├── rfc5162.IMAP4_Extensions_for_Quick_Mailbox_resync.txt │ ├── rfc5182.IMAP_extension_last_SEARCH_result.txt │ ├── rfc5182.Sieve_and_extensions.txt │ ├── rfc5255.IMAP_i18n.txt │ ├── rfc5257.IMAP_ANNOTATE_extension.txt │ ├── rfc5258.IMAP4_LIST_command_extension.txt │ ├── rfc5423.IM_Store_Events.txt │ ├── rfc5464.IMAP_METADATA_extension.txt │ ├── rfc5465.IMAP_NOTIFY_extension.txt │ ├── rfc5530.IMAP_Response_codes.txt │ ├── rfc5738.IMAP_UTF8.txt │ ├── rfc5788.IMAP4_Keyword_registry.txt │ ├── rfc5819.IMAP4_extension_Returning_STATUS_info_in_LIST.txt │ ├── rfc5957.IMAP4_SORT_extension.txt │ ├── rfc6154.IMAP_LIST_Special-use_Mailboxes.txt │ ├── rfc6203.IMAP4_Fuzzy_SEARCH_extension.txt │ ├── rfc6237.IMAP4_Multimailbox_SEARCH_extension.txt │ └── rfc6331.Moving_Digest-MD5_to_Historic └── website-doc.sh ├── offlineimap.conf ├── offlineimap.conf.minimal ├── offlineimap.py ├── offlineimap ├── CustomConfig.py ├── __init__.py ├── accounts.py ├── bundled_imaplib2.py ├── emailutil.py ├── error.py ├── folder │ ├── Base.py │ ├── Gmail.py │ ├── GmailMaildir.py │ ├── IMAP.py │ ├── LocalStatus.py │ ├── LocalStatusSQLite.py │ ├── Maildir.py │ ├── UIDMaps.py │ └── __init__.py ├── globals.py ├── imaplibutil.py ├── imapserver.py ├── imaputil.py ├── init.py ├── localeval.py ├── mbnames.py ├── repository │ ├── Base.py │ ├── Gmail.py │ ├── GmailMaildir.py │ ├── IMAP.py │ ├── LocalStatus.py │ ├── Maildir.py │ └── __init__.py ├── threadutil.py ├── ui │ ├── Curses.py │ ├── Machine.py │ ├── Noninteractive.py │ ├── TTY.py │ ├── UIBase.py │ ├── __init__.py │ └── debuglock.py ├── utils │ ├── __init__.py │ ├── const.py │ ├── distro.py │ └── stacktrace.py └── virtual_imaplib2.py ├── requirements.txt ├── scripts └── get-repository.sh ├── setup.cfg ├── setup.py ├── snapcraft.yaml ├── test ├── .gitignore ├── OLItest │ ├── TestRunner.py │ ├── __init__.py │ └── globals.py ├── README ├── __init__.py ├── credentials.conf.sample └── tests │ ├── __init__.py │ ├── test_00_globals.py │ ├── test_00_imaputil.py │ ├── test_01_basic.py │ └── test_02_MappedIMAP.py └── tests ├── .gitignore ├── create_conf_file.py └── requirements.txt /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | source = offlineimap 4 | 5 | [report] 6 | exclude_lines = 7 | if self.debug: 8 | pragma: no cover 9 | raise NotImplementedError 10 | if __name__ == .__main__.: 11 | ignore_errors = True 12 | omit = 13 | tests/* 14 | test/* 15 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # This is a comment. 2 | # Each line is a file pattern followed by one or more owners. 3 | 4 | # These owners will be the default owners for everything in the repo. 5 | # Unless a later match takes precedence, @global-owner1 and @global-owner2 6 | # will be requested for review when someone opens a pull request. 7 | #* @global-owner1 @global-owner2 8 | 9 | # Order is important; the last matching pattern takes the most precedence. 10 | # When someone opens a pull request that only modifies JS files, only @js-owner 11 | # and not the global owner(s) will be requested for a review. 12 | #*.js @js-owner 13 | 14 | # You can also use email addresses if you prefer. They'll be used to look up 15 | # users just like we do for commit author emails. 16 | #docs/* docs@example.com 17 | 18 | 19 | * @chris001 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | #### General informations 3 | 4 | - system/distribution (with version): 5 | - offlineimap version (`offlineimap -V`): 6 | - Python version: 7 | - server name or domain: 8 | - CLI options: 9 | 10 | #### Configuration file offlineimaprc 11 | 12 | ``` 13 | REMOVE PRIVATE DATA. 14 | ``` 15 | 16 | #### pythonfile (if any) 17 | 18 | ``` 19 | REMOVE PRIVATE DATA. 20 | ``` 21 | 22 | 23 | #### Logs, error 24 | 25 | ``` 26 | REMOVE PRIVATE DATA. 27 | ``` 28 | 29 | #### Steps to reproduce the error 30 | 31 | - 32 | - 33 | 34 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | > This v1.1 template stands in `.github/`. 2 | 3 | ### This PR 4 | 5 | > Add character x `[x]`. 6 | 7 | - [ ] I've read the [DCO](http://www.offlineimap.org/doc/dco.html). 8 | - [ ] I've read the [Coding Guidelines](http://www.offlineimap.org/doc/CodingGuidelines.html) 9 | - [ ] The relevant informations about the changes stands in the commit message, not here in the message of the pull request. 10 | - [ ] Code changes follow the style of the files they change. 11 | - [ ] Code is tested (provide details). 12 | 13 | ### References 14 | 15 | - Issue #no_space 16 | 17 | ### Additional information 18 | 19 | 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Backups. 2 | .*.swp 3 | .*.swo 4 | *~ 5 | # websites. 6 | /website/ 7 | /wiki/ 8 | # Generated files. 9 | *.html 10 | *.css 11 | /docs/dev-doc/ 12 | /build/ 13 | *.pyc 14 | offlineimap.1 15 | offlineimapui.7 16 | # Editors/IDEs 17 | tags 18 | # Generated conf files for Travis-CI tests. 19 | oli-travis.conf 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - '2.7' 4 | notifications: 5 | webhooks: 6 | urls: 7 | - https://webhooks.gitter.im/e/975e807e0314c9fa189c 8 | on_success: always # options: [always|never|change] default: always 9 | on_failure: always # options: [always|never|change] default: always 10 | on_start: never 11 | os: 12 | - linux 13 | env: 14 | global: 15 | - secure: jehlvkFxQbkvr73A0z3HGNC/knZQPKcaXLf6nByGpNE0ZTQKF7Y5KkNfeTcw4st7L7KuRZ1S/1bFtpMXTaplE6G0OtIEC4//SM+z+Dnadn2OY6wHiaapwZmmqDC5qVvcXPdmz/wTRsdrJSGLb2l6kEb91vRGbCCfHHf6Z2cF71U= 16 | - secure: kWdmWAFK4qrA73ONz1X8CJdHSER3bCBXjLfYHYEEMPCZep21bTITUXIfZBlSNN1888SQtYksuloRJmvj7xiY/hf/4lyWiqM3RgWQ+YptJMVOQX+Gara6vm4nGntKQwaXgZF2YHSh+NYwQm1VY6m0n1ye/vfOIJnYfgGTk5qAZYU= 17 | - secure: MzytYRX6HxgBj6Q3efkACTtDed8ZYO+P6UJrDA9IDtvffi8fAFb+wkQtKJrdcvMXNOap6fPe4c0EVGjgL5hFxmgC8yAh5t2YK7OhstAtq0ptKFlOcU24/drrkqoq040sAM/4Lc0nQCvYpz7bH370jzZl69rpbQWttwQR0i1e3Gw= 18 | - secure: RWvIOHSiv2kt6cfZR7MEueiAmC61bWMXAtgsC6gKq1u3BfENfqSBTA/heIy+nlu7AXK1b6hPMZDCHWK09Zz6Klkd9xZ1gkE/AARWseoo9UWgGjmfvqng1S6qpESeX2GnZGR9CuBXTPGhtbYLgtNlxAo+6uZLolz2utW2XNk3Z/Y= 19 | - secure: spivQv+vSJhE+ttn/Z6tANaINqiMSaJSucRqtoXR7PtioVDTOTmmL01Ja6dXuo8Ua5iVFtpZPDzqVpntQLKtjcywSK2zWnC9qbZYDfENr1/yIvfbSRjGeseq0eoY+fFp67FGZV4mIasdC3LOB0lRGOyrsX787fNKVQ8ZH0CRz0o= 20 | - secure: ZcY0TvTQnRCdoFkdbJPfDJJNx91tViwbpiOBkxNEa3u0RN48xkZkii35kNVBaEcVZHcT9C81ctHk4QX+plBkCsoj5GDf25scgcv1j9R9UoN/rIkmyTu1Znmc+3UQ2J+EnGLWVn5xJ7yT/l9NZeLfNbULQRjttwT4j2MBGxezgdM= 21 | matrix: 22 | - OUTLOOK_AUTH=PLAIN GMAIL_AUTH=XOAUTH2 23 | - OUTLOOK_AUTH=LOGIN GMAIL_AUTH=XOAUTH2 24 | matrix: 25 | include: 26 | - os: osx 27 | language: generic 28 | env: PYTHON=2.7.14 OUTLOOK_AUTH=PLAIN GMAIL_AUTH=XOAUTH2 29 | - os: osx 30 | language: generic 31 | env: PYTHON=2.7.14 OUTLOOK_AUTH=LOGIN GMAIL_AUTH=XOAUTH2 32 | allow_failures: 33 | - os: osx 34 | cache: pip 35 | before_install: 36 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew update && brew install openssl readline; 37 | fi 38 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then export OSX_BREW_SSLCACERTFILE="/usr/local/etc/openssl/cert.pem"; 39 | fi 40 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew outdated pyenv || brew upgrade pyenv; 41 | fi 42 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew install pyenv-virtualenv; fi 43 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then pyenv install $PYTHON; fi 44 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then export PYENV_VERSION="${PYTHON}"; fi 45 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then export PATH="/Users/travis/.pyenv/shims:${PATH}"; 46 | fi 47 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then pyenv virtualenv $PYTHON myvenv; fi 48 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then pyenv versions; fi 49 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then python --version; fi 50 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then pyenv version; fi 51 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then python --version; fi 52 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then python -m pip install -U pip; fi 53 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then python -m easy_install -U setuptools; 54 | fi 55 | install: 56 | - pip install -r requirements.txt 57 | - pip install -r tests/requirements.txt 58 | - export PATH=$PATH:. 59 | - python tests/create_conf_file.py 60 | script: 61 | - "./offlineimap.py -c ./oli-travis.conf" 62 | - codecov 63 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | # Realistic Code of Conduct 3 | 4 | 1. We mostly care about making our softwares better. 5 | 6 | 2. Everybody is free to decide how to contribute. 7 | 8 | 3. We believe in free speech. Everyone's entitled to their opinion. 9 | 10 | 4. Feel offended? This might be very well-deserved. 11 | 12 | 5. We don't need a code of conduct imposed on us, thanks. 13 | 14 | 6. Ignoring this Realistic Code of Conduct is welcome. 15 | 16 | 19 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | .. -*- coding: utf-8 -*- 2 | .. vim: spelllang=en ts=2 expandtab: 3 | 4 | .. _OfflineIMAP: https://github.com/OfflineIMAP/offlineimap 5 | .. _Github: https://github.com/OfflineIMAP/offlineimap 6 | .. _repository: git://github.com/OfflineIMAP/offlineimap.git 7 | .. _maintainers: https://github.com/OfflineIMAP/offlineimap/blob/next/MAINTAINERS.rst 8 | .. _mailing list: http://lists.alioth.debian.org/mailman/listinfo/offlineimap-project 9 | .. _Developer's Certificate of Origin: https://github.com/OfflineIMAP/offlineimap/blob/next/docs/doc-src/dco.rst 10 | .. _Community's website: http://www.offlineimap.org 11 | .. _APIs in OfflineIMAP: http://www.offlineimap.org/documentation.html#available-apis 12 | .. _documentation: http://www.offlineimap.org/documentation.html 13 | .. _Coding Guidelines: http://www.offlineimap.org/doc/CodingGuidelines.html 14 | .. _Know the status of your patches: http://www.offlineimap.org/doc/GitAdvanced.html#know-the-status-of-your-patch-after-submission 15 | .. _How to fix a bug in open source software: https://opensource.com/life/16/8/how-get-bugs-fixed-open-source-software 16 | 17 | 18 | ================= 19 | HOW TO CONTRIBUTE 20 | ================= 21 | 22 | You'll find here the **basics** to contribute to OfflineIMAP_, addressed to 23 | users as well as learning or experienced developers to quickly provide 24 | contributions. 25 | 26 | **For more detailed documentation, see the** `Community's website`_. 27 | 28 | .. contents:: :depth: 3 29 | 30 | 31 | Submit issues 32 | ============= 33 | 34 | Issues are welcome to both Github_ and the `mailing list`_, at your own 35 | convenience. Provide the following information: 36 | - system/distribution (with version) 37 | - offlineimap version (`offlineimap -V`) 38 | - Python version 39 | - server name or domain 40 | - CLI options 41 | - Configuration file (offlineimaprc) 42 | - pythonfile (if any) 43 | - Logs, error 44 | - Steps to reproduce the error 45 | 46 | Worth the read: `How to fix a bug in open source software`_. 47 | 48 | You might help closing some issues, too. :-) 49 | 50 | 51 | For the imaptients 52 | ================== 53 | 54 | - `Coding Guidelines`_ 55 | - `APIs in OfflineIMAP`_ 56 | - `Know the status of your patches`_ after submission 57 | - All the `documentation`_ 58 | 59 | 60 | Community 61 | ========= 62 | 63 | All contributors to OfflineIMAP_ are benevolent volunteers. This makes hacking 64 | to OfflineIMAP_ **fun and open**. 65 | 66 | Thanks to Python, almost every developer can quickly become productive. Students 67 | and novices are welcome. Third-parties patches are essential and proved to be a 68 | wonderful source of changes for both fixes and new features. 69 | 70 | OfflineIMAP_ is entirely written in Python, works on IMAP and source code is 71 | tracked with Git. 72 | 73 | *It is expected that most contributors don't have skills to all of these areas.* 74 | That's why the best thing you could do for you, is to ask us about any 75 | difficulty or question raising in your mind. We actually do our best to help new 76 | comers. **We've all started like this.** 77 | 78 | - The official repository_ is maintained by the core team maintainers_. 79 | 80 | - The `mailing list`_ is where all the exciting things happen. 81 | 82 | 83 | Getting started 84 | =============== 85 | 86 | Occasional contributors 87 | ----------------------- 88 | 89 | * Clone the official repository_. 90 | 91 | Regular contributors 92 | -------------------- 93 | 94 | * Create an account and login to Github. 95 | * Fork the official repository_. 96 | * Clone your own fork to your local workspace. 97 | * Add a reference to your fork (once):: 98 | 99 | $ git remote add myfork https://github.com//offlineimap.git 100 | 101 | * Regularly fetch the changes applied by the maintainers:: 102 | 103 | $ git fetch origin 104 | $ git checkout master 105 | $ git merge offlineimap/master 106 | $ git checkout next 107 | $ git merge offlineimap/next 108 | 109 | 110 | Making changes (all contributors) 111 | --------------------------------- 112 | 113 | 1. Create your own topic branch off of ``next`` (recently updated) via:: 114 | 115 | $ git checkout -b my_topic next 116 | 117 | 2. Check for unnecessary whitespaces with ``git diff --check`` before committing. 118 | 3. Commit your changes into logical/atomic commits. **Sign-off your work** to 119 | confirm you agree with the `Developer's Certificate of Origin`_. 120 | 4. Write a good *commit message* about **WHY** this patch (take samples from 121 | the ``git log``). 122 | 123 | 124 | Learn more 125 | ========== 126 | 127 | There is already a lot of documentation. Here's where you might want to look 128 | first: 129 | 130 | - The directory ``offlineimap/docs`` has all kind of additional documentation 131 | (man pages, RFCs). 132 | 133 | - The file ``offlineimap.conf`` allows to know all the supported features. 134 | 135 | - The file ``TODO.rst`` express code changes we'd like and current *Work In 136 | Progress* (WIP). 137 | 138 | -------------------------------------------------------------------------------- /Changelog.maint.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Changelog of the stable branch 4 | --- 5 | 6 | * The following excerpt is only usefull when rendered in the website. 7 | {:toc} 8 | 9 | This is the Changelog of the maintenance branch. 10 | 11 | **NOTE FROM THE MAINTAINER:** 12 | 13 | This branch comes almost as-is. With no URGENT requirements to update this 14 | branch (e.g. big security fix), it is left behind. 15 | If anyone volunteers to maintain it and backport patches, let us know! 16 | 17 | 18 | ### OfflineIMAP v6.7.0.3 (2016-07-26) 19 | 20 | #### Bug Fixes 21 | 22 | * sqlite: properly serialize operations on the database files 23 | 24 | 25 | ### OfflineIMAP v6.7.0.2 (2016-07-22) 26 | 27 | #### Bug Fixes 28 | 29 | * sqlite: close the database when no more threads need connection. 30 | 31 | 32 | ### OfflineIMAP v6.7.0.1 (2016-06-08) 33 | 34 | #### Bug Fixes 35 | 36 | * Correctly open and close sqlite databases. 37 | 38 | 39 | ### OfflineIMAP v6.3.2.1 (2011-03-23) 40 | 41 | #### Bug Fixes 42 | 43 | * Sanity checks for SSL cacertfile configuration. 44 | * Fix regression (UIBase is no more). 45 | * Make profiling mode really enforce single-threading. 46 | -------------------------------------------------------------------------------- /MAINTAINERS.rst: -------------------------------------------------------------------------------- 1 | .. -*- coding: utf-8 -*- 2 | 3 | Contacts 4 | ======== 5 | 6 | - Abdó Roig-Maranges 7 | - email: abdo.roig at gmail.com 8 | - github: aroig 9 | 10 | - Ben Boeckel 11 | - email: mathstuf at gmail.com 12 | - github: mathstuf 13 | 14 | - benutzer193 15 | - email: registerbn at gmail.com 16 | - github: benutzer193 17 | 18 | - Chris Coleman 19 | - email: chris at espacenetworks.com 20 | - github: chris001 21 | 22 | - Darshit Shah 23 | - email: darnir at gmail.com 24 | - github: darnir 25 | 26 | - Eygene Ryabinkin 27 | - email: rea at freebsd.org 28 | - github: konvpalto 29 | - other: FreeBSD maintainer 30 | 31 | - Igor Almeida 32 | - email: igor.contato at gmail.com 33 | - github: igoralmeida 34 | 35 | - Ilias Tsitsimpis 36 | - email: i.tsitsimpis at gmail.com 37 | - github: iliastsi 38 | - other: Debian maintainer 39 | 40 | - "J" 41 | - email: offlineimap at 927589452.de 42 | - github: 927589452 43 | - other: FreeBSD user 44 | 45 | - Łukasz Żarnowiecki 46 | - email: dolohow at outlook.com 47 | - github: dolohow 48 | 49 | - Nicolas Sebrecht 50 | - email: nicolas.s-dev at laposte.net 51 | - github: nicolas33 52 | - system: Linux 53 | 54 | - Remi Locherer 55 | - email: remi.locherer at relo.ch 56 | - system: OpenBSD maintainer 57 | 58 | - Sebastian Spaeth 59 | - email: sebastian at sspaeth.de 60 | - github: spaetz 61 | - other: left the project but still responding 62 | 63 | 64 | Testers 65 | ======= 66 | 67 | - Abdó Roig-Maranges 68 | - Ben Boeckel 69 | - Chris Coleman 70 | - Darshit Shah 71 | - Eygene Ryabinkin 72 | - Igor Almeida 73 | - Ilias Tsitsimpis 74 | - "J" 75 | - Łukasz Żarnowiecki 76 | - Nicolas Sebrecht 77 | - Remi Locherer 78 | 79 | 80 | Maintainers 81 | =========== 82 | 83 | - Eygene Ryabinkin 84 | - Sebastian Spaeth 85 | - Nicolas Sebrecht 86 | - Chris Coleman 87 | 88 | 89 | Github 90 | ------ 91 | 92 | - Eygene Ryabinkin 93 | - Sebastian Spaeth 94 | - Nicolas Sebrecht 95 | 96 | 97 | Mailing List 98 | ------------ 99 | 100 | - Eygene Ryabinkin 101 | - Sebastian Spaeth 102 | - Nicolas Sebrecht 103 | 104 | 105 | Twitter 106 | ------- 107 | 108 | - Nicolas Sebrecht 109 | 110 | 111 | Pypi 112 | ---- 113 | 114 | - Nicolas Sebrecht 115 | - Sebastian Spaeth 116 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | global-exclude .gitignore .git *.bak *.orig *.rej 2 | include setup.py 3 | include COPYING 4 | include Changelog* 5 | include MAINTAINERS 6 | include MANIFEST.in 7 | include Makefile 8 | include README.md 9 | include offlineimap.conf* 10 | include offlineimap.py 11 | recursive-include contrib * 12 | recursive-include offlineimap *.py 13 | recursive-include bin * 14 | recursive-include docs * 15 | recursive-include test * 16 | prune docs/rfcs 17 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2002 - 2018 John Goerzen & contributors. 2 | # 3 | # This program is free software; you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation; either version 2 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software 15 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 16 | 17 | # Warning: VERSION, ABBREV and TARGZ are used in docs/build-uploads.sh. 18 | VERSION=$(shell ./offlineimap.py --version) 19 | ABBREV=$(shell git log --format='%h' HEAD~1..) 20 | TARGZ=offlineimap-v$(VERSION)-$(ABBREV) 21 | SHELL=/bin/bash 22 | RST2HTML=`type rst2html >/dev/null 2>&1 && echo rst2html || echo rst2html.py` 23 | 24 | all: build 25 | 26 | build: 27 | python setup.py build 28 | @echo 29 | @echo "Build process finished, run 'python setup.py install' to install" \ 30 | "or 'python setup.py --help' for more information". 31 | 32 | clean: 33 | -python setup.py clean --all 34 | -rm -f bin/offlineimapc 2>/dev/null 35 | -find . -name '*.pyc' -exec rm -f {} \; 36 | -find . -name '*.pygc' -exec rm -f {} \; 37 | -find . -name '*.class' -exec rm -f {} \; 38 | -find . -name '.cache*' -exec rm -f {} \; 39 | -find . -type d -name '__pycache__' -exec rm -rf {} \; 40 | -rm -f manpage.links manpage.refs 2>/dev/null 41 | -find . -name auth -exec rm -vf {}/password {}/username \; 42 | -$(MAKE) -C docs clean 43 | 44 | .PHONY: docs 45 | docs: 46 | @$(MAKE) -C docs 47 | 48 | websitedoc: 49 | @$(MAKE) -C websitedoc 50 | 51 | targz: ../$(TARGZ) 52 | ../$(TARGZ): 53 | 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} 54 | 55 | rpm: targz 56 | cd .. && sudo rpmbuild -ta $(TARGZ) 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Upstream status (`master` branch): 2 | [![OfflineIMAP build status on Travis-CI.org](https://travis-ci.org/OfflineIMAP/offlineimap.svg?branch=master)](https://travis-ci.org/OfflineIMAP/offlineimap) 3 | [![OfflineIMAP code coverage on Codecov.io](https://codecov.io/gh/OfflineIMAP/offlineimap/branch/master/graph/badge.svg)](https://codecov.io/gh/OfflineIMAP/offlineimap) 4 | [![Gitter chat](https://badges.gitter.im/OfflineIMAP/offlineimap.png)](https://gitter.im/OfflineIMAP/offlineimap) 5 | 6 | Upstream status (`next` branch): 7 | [![OfflineIMAP build status on Travis-CI.org](https://travis-ci.org/OfflineIMAP/offlineimap.svg?branch=next)](https://travis-ci.org/OfflineIMAP/offlineimap) 8 | 9 | [offlineimap]: http://github.com/OfflineIMAP/offlineimap 10 | [website]: http://www.offlineimap.org 11 | [wiki]: http://github.com/OfflineIMAP/offlineimap/wiki 12 | [blog]: http://www.offlineimap.org/posts.html 13 | 14 | Links: 15 | * Official github code repository: [offlineimap] 16 | * Website: [website] 17 | * Wiki: [wiki] 18 | * Blog: [blog] 19 | 20 | # OfflineIMAP 21 | 22 | ***"Get the emails where you need them."*** 23 | 24 | [Official offlineimap][offlineimap]. 25 | 26 | 27 | ## Description 28 | 29 | OfflineIMAP is software that downloads your email mailbox(es) as **local 30 | Maildirs**. OfflineIMAP will synchronize both sides via *IMAP*. 31 | 32 | ## Why should I use OfflineIMAP? 33 | 34 | IMAP's main downside is that you have to **trust** your email provider to 35 | not lose your email. While certainly unlikely, it's not impossible. 36 | With OfflineIMAP, you can download your Mailboxes and make you own backups of 37 | your [Maildir](https://en.wikipedia.org/wiki/Maildir). 38 | 39 | This allows reading your email offline without the need for your mail 40 | reader (MUA) to support IMAP operations. Need an attachment from a 41 | message without internet connection? No problem, the message is still there. 42 | 43 | 44 | ## Project status and future 45 | 46 | > As one of the maintainer of OfflineIMAP, I'd like to put my efforts into 47 | > [imapfw](http://github.com/OfflineIMAP/imapfw). **imapfw** is software in 48 | > development that I intend to replace OfflineIMAP with in the long term. 49 | > 50 | > That's why I'm not going to continue OfflineIMAP development. I'll continue 51 | > to maintain OfflineIMAP (fixing small bugs, reviewing patches and merging, 52 | > and rolling out new releases), but that's all. 53 | > 54 | > While I keep tracking issues for OfflineIMAP, you should not expect future support. 55 | > 56 | > You won't be left at the side. OfflineIMAP's community is large enough so that 57 | > you'll find people for most of your issues. 58 | > 59 | > Get news from the [blog][blog]. 60 | > 61 | > Nicolas Sebrecht. ,-) 62 | 63 | 64 | ## License 65 | 66 | GNU General Public License v2. 67 | 68 | 69 | ## Downloads 70 | 71 | You should first check if your distribution already packages OfflineIMAP for you. 72 | Downloads releases as [tarball or zipball](https://github.com/OfflineIMAP/offlineimap/tags). 73 | 74 | If you are running Linux Os, you can install offlineimap with: 75 | 76 | - openSUSE `zypper in offlineimap` 77 | - archLinux `pacman -S offlineimap` 78 | - fedora `dnf install offlineimap` 79 | 80 | ## Feedbacks and contributions 81 | 82 | **The user discussions, development, announcements and all the exciting stuff take 83 | place on the mailing list.** While not mandatory to send emails, you can 84 | [subscribe here](http://lists.alioth.debian.org/mailman/listinfo/offlineimap-project). 85 | 86 | Bugs, issues and contributions can be requested to both the mailing list or the 87 | [official Github project][offlineimap]. Provide the following information: 88 | - system/distribution (with version) 89 | - offlineimap version (`offlineimap -V`) 90 | - Python version 91 | - server name or domain 92 | - CLI options 93 | - Configuration file (offlineimaprc) 94 | - pythonfile (if any) 95 | - Logs, error 96 | - Steps to reproduce the error 97 | 98 | 99 | ## The community 100 | 101 | * OfflineIMAP's main site is the [project page at Github][offlineimap]. 102 | * There is the [OfflineIMAP community's website][website]. 103 | * And finally, [the wiki][wiki]. 104 | 105 | 106 | ## Requirements & dependencies 107 | 108 | * Python v2.7+ 109 | * six (required) 110 | * rfc6555 (required) 111 | * imaplib2 >= 2.57 (optional) 112 | * gssapi (optional), for Kerberos authentication 113 | * portalocker (optional), if you need to run offlineimap in Cygwin for Windows 114 | 115 | * Python v3.4+ ***[STALLED] (experimental: [see known issues](https://github.com/OfflineIMAP/offlineimap/issues?q=is%3Aissue+is%3Aopen+label%3APy3))*** 116 | 117 | ## Documentation 118 | 119 | All current and updated documentation is on the [community's website][website]. 120 | 121 | 122 | ### Read documentation locally 123 | 124 | You might want to read the documentation locally. Get the sources of the website. 125 | For the other documentation, run the appropriate make target: 126 | 127 | ```sh 128 | $ ./scripts/get-repository.sh website 129 | $ cd docs 130 | $ make html # Requires rst2html 131 | $ make man # Requires a2x (http://asciidoc.org) 132 | $ make api # Requires sphinx 133 | ``` 134 | -------------------------------------------------------------------------------- /TODO.rst: -------------------------------------------------------------------------------- 1 | .. vim: spelllang=en ts=2 expandtab : 2 | 3 | .. _coding style: https://github.com/OfflineIMAP/offlineimap/blob/next/docs/CodingGuidelines.rst 4 | 5 | ============================ 6 | TODO list by relevance order 7 | ============================ 8 | 9 | Should be the starting point to improve the `coding style`_. 10 | 11 | Write your WIP directly in this file. 12 | 13 | TODO list 14 | --------- 15 | 16 | * Better names for variables, objects, etc. 17 | 18 | 19 | * Improve comments. 20 | 21 | Most of the current comments assume a very good 22 | knowledge of the internals. That sucks because I guess nobody is 23 | anymore aware of ALL of them. Time when this was a one guy made 24 | project has long passed. 25 | 26 | 27 | * Better policy on objects. 28 | 29 | - Turn ALL attributes private and use accessors. This is not 30 | "pythonic" but such pythonic thing turn the code into intricated 31 | code. 32 | 33 | - Turn ALL methods not intended to be used outside, private. 34 | 35 | 36 | * Revamp the factorization. 37 | 38 | It's not unusual to find "factorized" code 39 | for bad reasons: because it made the code /look/ nicer, but the 40 | factorized function/methods is actually called from ONE place. While it 41 | might locally help, such practice globally defeat the purpose because 42 | we lose the view of what is true factorized code and what is not. 43 | 44 | 45 | * Namespace the factorized code. 46 | 47 | If a method require a local function, DON'T USE yet another method. Use a 48 | local namespaced function.:: 49 | 50 | class BLah(object): 51 | def _internal_method(self, arg): 52 | def local_factorized(local_arg): 53 | # local_factorized's code 54 | # _internal_method's code. 55 | 56 | Python allows local namespaced functions for good reasons. 57 | 58 | 59 | * Better inheritance policy. 60 | 61 | Take the sample of the folder/LocalStatus(SQlite) and folder/Base stuffs. It's 62 | *nearly IMPOSSIBLE* to know and understand what parent method is used by what 63 | child, for what purpose, etc. So, instead of (re)defining methods in the wild, 64 | keep the well common NON-redefined stuff into the parent and define the 65 | required methods in the childs. We really don't want anything like:: 66 | 67 | def method(self): 68 | raise NotImplemented 69 | 70 | While this is common practice in Python, think about that again: how a 71 | parent object should know all the expected methods/accessors of all the 72 | possible kind of childs? 73 | 74 | Inheritance is about factorizing, certainly **NOT** about **defining the 75 | interface** of the childs. 76 | 77 | 78 | * Introduce as many as intermediate inherited objects as required. 79 | 80 | Keeping linear inheritance is good because Python sucks at playing 81 | with multiple parents and it keeps things simple. But a parent should 82 | have ALL its methods used in ALL the childs. If not, it's a good 83 | sign that a new intermediate object should be introduced in the 84 | inheritance line. 85 | 86 | * Don't blindly inherit from library objects. 87 | 88 | We do want **well defined interfaces**. For example, we do too much things 89 | like imapobj.methodcall() while the imapobj is far inherited from imaplib2. 90 | 91 | We have NO clue about what we currently use from the library. 92 | Having a dump wrappper for each call should be made mandatory for 93 | objects inherited from a library. Using composed objects should be 94 | seriously considered in this case, instead of using inheritance. 95 | 96 | * Use factories. 97 | 98 | Current objects do too much initialization stuff varying with the context it 99 | is used. Move things like that into factories and keep the objects definitions 100 | clean. 101 | 102 | 103 | * Make it clear when we expect a composite object and what we expect 104 | exactly. 105 | 106 | Even the more obvious composed objects are badly defined. For example, 107 | the ``conf`` instances are spread across a lot of objects. Did you know 108 | that such composed objects are sometimes restricted to the section the 109 | object works on, and most of the time it's not restricted at all? 110 | How many time it requires to find and understand on what we are 111 | currently working? 112 | 113 | 114 | * Seriously improve our debugging/hacking sessions (AGAIN). 115 | 116 | Until now, we have limited the improvements to allow better/full stack traces. 117 | While this was actually required, we now hit some limitations of the whole 118 | exception-based paradigm. For example, it's very HARD to follow an instance 119 | during its life time. I have a good overview of what we could do in this area, 120 | so don't matter much about that if you don't get the point or what could be 121 | done. 122 | 123 | * Support Unicode. 124 | -------------------------------------------------------------------------------- /bin/offlineimap: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | # Startup from system-wide installation 3 | # Copyright (C) 2002-2018 John Goerzen & contributors 4 | # 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program; if not, write to the Free Software 17 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 18 | 19 | from offlineimap import OfflineImap 20 | 21 | oi = OfflineImap() 22 | oi.run() 23 | -------------------------------------------------------------------------------- /contrib/README.md: -------------------------------------------------------------------------------- 1 | 2 | README 3 | ====== 4 | 5 | **This "./contrib" directory is where users share their own scripts and tools.** 6 | 7 | Everything here is submitted and maintained *by the users for the users*. You're 8 | welcome to add your own stuff. There is no barrier on your contributions here. 9 | We think it's expected to find contributions of various quality. 10 | -------------------------------------------------------------------------------- /contrib/internet-urllib3.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import urllib3 4 | import certifi 5 | 6 | def isInternetConnected(url="www.ietf.org"): 7 | result = False 8 | http = urllib3.PoolManager( 9 | cert_reqs='CERT_REQUIRED', # Force certificate check. 10 | ca_certs=certifi.where(), # Path to the Certifi bundle. 11 | ) 12 | try: 13 | r = http.request('HEAD', 'https://' + url) 14 | result = True 15 | except Exception as e: # urllib3.exceptions.SSLError 16 | result = False 17 | return result 18 | 19 | print isInternetConnected() 20 | -------------------------------------------------------------------------------- /contrib/store-pw-with-gpg/README.md: -------------------------------------------------------------------------------- 1 | # gpg-offlineimap 2 | 3 | Python bindings for offlineimap to use gpg instead of storing cleartext passwords 4 | 5 | Author: Lorenzo G. 6 | [GitHub](https://github.com/lorenzog/gpg-offlineimap) 7 | 8 | ## Quickstart 9 | 10 | Requirements: a working GPG set-up. Ideally with gpg-agent. Should work 11 | out of the box on most modern Linux desktop environments. 12 | 13 | 1. Enable IMAP in gmail (if you have two factor authentication, you 14 | need to create an app-specific password) 15 | 16 | 2. Create a directory `~/Mail` 17 | 18 | 3. In `~/Mail`, create a password file `passwords-gmail.txt`. Format: 19 | `account@gmail.com password`. Look at the example file in this 20 | directory. 21 | 22 | 4. **ENCRYPT** the file: `gpg -e passwords-gmail.txt`. It should create 23 | a file `passwords-gmail.txt.gpg`. Check you can decrypt it: `gpg -d 24 | passwords-gmail.txt.gpg`: it will ask you for your GPG password and 25 | show it to you. 26 | 27 | 5. Use the file `offlineimaprc.sample` as a sample for your own 28 | `.offlineimaprc`; edit it by following the comments. Minimal items 29 | to configure: the `remoteuser` field and the `pythonfile` parameter 30 | pointing at the `offlineimap.py` file in this directory. 31 | 32 | 6. Run it: `offlineimap`. It should ask you for your GPG passphrase to 33 | decrypt the password file. 34 | 35 | 7. If all works well, delete the cleartext password file. 36 | 37 | 38 | -------------------------------------------------------------------------------- /contrib/store-pw-with-gpg/gpg-pw.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # Originally taken from: http://stevelosh.com/blog/2012/10/the-homely-mutt/ 3 | # by Steve Losh 4 | # Modified by Lorenzo Grespan on Jan, 2014 5 | 6 | import re 7 | import subprocess 8 | from sys import argv 9 | import logging 10 | from os.path import expanduser 11 | import unittest 12 | import os 13 | import sys 14 | 15 | logging.basicConfig(level=logging.INFO) 16 | 17 | 18 | DEFAULT_PASSWORDS_FILE = os.path.join( 19 | os.path.expanduser('~/Mail'), 20 | 'passwords.gpg') 21 | 22 | 23 | def get_keychain_pass(account=None, server=None): 24 | '''Mac OSX keychain password extraction''' 25 | params = { 26 | 'security': '/usr/bin/security', 27 | 'command': 'find-internet-password', 28 | 'account': account, 29 | 'server': server, 30 | 'keychain': expanduser('~') + '/Library/Keychains/login.keychain', 31 | } 32 | command = ("%(security)s -v %(command)s" 33 | " -g -a %(account)s -s %(server)s %(keychain)s" % params) 34 | output = subprocess.check_output( 35 | command, shell=True, stderr=subprocess.STDOUT) 36 | outtext = [l for l in output.splitlines() 37 | if l.startswith('password: ')][0] 38 | return find_password(outtext) 39 | 40 | 41 | def find_password(text): 42 | '''Helper method for osx password extraction''' 43 | # a non-capturing group 44 | r = re.match(r'password: (?:0x[A-F0-9]+ )?"(.*)"', text) 45 | if r: 46 | return r.group(1) 47 | else: 48 | logging.warn("Not found") 49 | return None 50 | 51 | 52 | def get_gpg_pass(account, storage): 53 | '''GPG method''' 54 | command = ("gpg", "-d", storage) 55 | # get attention 56 | print '\a' # BEL 57 | output = subprocess.check_output(command) 58 | # p = subprocess.Popen(command, stdout=subprocess.PIPE) 59 | # output, err = p.communicate() 60 | for line in output.split('\n'): 61 | r = re.match(r'{} ([a-zA-Z0-9]+)'.format(account), line) 62 | if r: 63 | return r.group(1) 64 | return None 65 | 66 | 67 | def get_pass(account=None, server=None, passwd_file=None): 68 | '''Main method''' 69 | if not passwd_file: 70 | storage = DEFAULT_PASSWORDS_FILE 71 | else: 72 | storage = os.path.join( 73 | os.path.expanduser('~/Mail'), 74 | passwd_file) 75 | if os.path.exists('/usr/bin/security'): 76 | return get_keychain_pass(account, server) 77 | if os.path.exists(storage): 78 | logging.info("Using {}".format(storage)) 79 | return get_gpg_pass(account, storage) 80 | else: 81 | logging.warn("No password file found") 82 | sys.exit(1) 83 | return None 84 | 85 | 86 | # test with: python -m unittest 87 | # really basic tests.. nothing to see. move along 88 | class Tester(unittest.TestCase): 89 | def testMatchSimple(self): 90 | text = 'password: "exampleonetimepass "' 91 | self.assertTrue(find_password(text)) 92 | 93 | def testMatchComplex(self): 94 | text = r'password: 0x74676D62646D736B646970766C66696B0A "anotherexamplepass\012"' 95 | self.assertTrue(find_password(text)) 96 | 97 | 98 | if __name__ == "__main__": 99 | print get_pass(argv[1], argv[2], argv[3]) 100 | -------------------------------------------------------------------------------- /contrib/store-pw-with-gpg/offlineimaprc.sample: -------------------------------------------------------------------------------- 1 | [general] 2 | # GPG quirks, leave unconfigured 3 | ui = ttyui 4 | # you can use any name as long as it matches the 'account1, 'account2' in the rest 5 | # of the file 6 | accounts = account1, account2 7 | # this is where the `gpg-pw.py` file is on disk 8 | pythonfile=~/where/is/the/file/gpg-pw.py 9 | fsync = False 10 | 11 | # you can call this any way you like 12 | [Account account1] 13 | localrepository = account1-local 14 | remoterepository = account1-remote 15 | # no need to touch this 16 | status_backend = sqlite 17 | 18 | [Account account2] 19 | localrepository = account2-local 20 | remoterepository = account2-remote 21 | status_backend = sqlite 22 | 23 | # thi sis a gmail account 24 | [Repository account1-local] 25 | type = Maildir 26 | # create with maildirmake or by hand by creating cur, new, tmp 27 | localfolders = ~/Mail/Mailboxes/account1 28 | # standard Gmail stuff 29 | nametrans = lambda folder: { 'drafts': '[Gmail]/Drafts', 30 | 'sent': '[Gmail]/Sent mail', 31 | 'flagged': '[Gmail]/Starred', 32 | 'trash': '[Gmail]/Trash', 33 | 'archive': '[Gmail]/All Mail' 34 | }.get(folder, folder) 35 | 36 | [Repository account1-remote] 37 | maxconnections = 1 38 | type = Gmail 39 | ssl=yes 40 | # for osx, you might need to download the certs by hand 41 | #sslcacertfile=~/Mail/certs.pem 42 | #sslcacertfile=~/Mail/imap.gmail.com.pem 43 | # sslcacertfile=/etc/ssl/cert.pem 44 | 45 | # or use Linux's standard certs 46 | sslcacertfile=/etc/ssl/certs/ca-certificates.crt 47 | # your account 48 | remoteuser = account1@gmail.com 49 | remotepasseval = get_pass(account="account1@gmail.com", server="imap.gmail.com", passwd_file="passwords-gmail.txt.gpg") 50 | realdelete = no 51 | createfolders = no 52 | nametrans = lambda folder: {'[Gmail]/Drafts': 'drafts', 53 | '[Gmail]/Sent Mail': 'sent', 54 | '[Gmail]/Starred': 'star', 55 | '[Gmail]/Trash': 'trash', 56 | '[Gmail]/All Mail': 'archive', 57 | }.get(folder, folder) 58 | folderfilter = lambda folder: folder not in ['[Gmail]/Trash', 59 | '[Gmail]/Spam', 60 | ] 61 | 62 | [Repository account2-remote] 63 | # copy the stanza above, change the 'account' parameter of get_pass, etc. 64 | -------------------------------------------------------------------------------- /contrib/store-pw-with-gpg/passwords-gmail.txt: -------------------------------------------------------------------------------- 1 | account1@gmail.com password1 2 | account2@gmail.com password2 3 | -------------------------------------------------------------------------------- /contrib/systemd/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Integrating OfflineIMAP into systemd 4 | author: Ben Boeckel 5 | date: 2015-03-22 6 | contributors: Abdo Roig-Maranges, benutzer193, Hugo Osvaldo Barrera 7 | updated: 2017-06-01 8 | --- 9 | 10 | 11 | 12 | 13 | ## Systemd units 14 | 15 | These unit files are meant to be used in the user session. You may drop them 16 | into `/etc/systemd/user` or `${XDG_DATA_HOME}/systemd/user` followed by 17 | `systemctl --user daemon-reload` to have systemd aware of the unit files. 18 | 19 | These files are meant to be triggered either manually using `systemctl --user 20 | start offlineimap.service` or by enabling the timer unit using `systemctl --user 21 | enable offlineimap-oneshot.timer`. Additionally, specific accounts may be 22 | triggered by using `offlineimap@myaccount.timer` or 23 | `offlineimap-oneshot@myaccount.service`. 24 | 25 | If the defaults provided by these units doesn't suit your setup, any of the 26 | values may be overridden by using `systemctl --user edit offlineimap.service`. 27 | This'll prevent having to copy-and-edit the original file. 28 | -------------------------------------------------------------------------------- /contrib/systemd/offlineimap-oneshot.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Offlineimap Service (oneshot) 3 | Documentation=man:offlineimap(1) 4 | 5 | [Service] 6 | Type=oneshot 7 | ExecStart=/usr/bin/offlineimap -o -u basic 8 | # Give 120 seconds for offlineimap to gracefully stop before hard killing it: 9 | TimeoutStopSec=120 10 | 11 | [Install] 12 | WantedBy=mail.target 13 | -------------------------------------------------------------------------------- /contrib/systemd/offlineimap-oneshot.timer: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Offlineimap Query Timer 3 | 4 | [Timer] 5 | OnBootSec=1m 6 | OnUnitInactiveSec=15m 7 | 8 | [Install] 9 | WantedBy=default.target 10 | -------------------------------------------------------------------------------- /contrib/systemd/offlineimap-oneshot@.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Offlineimap Service for account %i (oneshot) 3 | Documentation=man:offlineimap(1) 4 | 5 | [Service] 6 | Type=oneshot 7 | ExecStart=/usr/bin/offlineimap -o -a %i -u basic 8 | # Give 120 seconds for offlineimap to gracefully stop before hard killing it. 9 | TimeoutStopSec=120 10 | 11 | [Install] 12 | WantedBy=default.target 13 | -------------------------------------------------------------------------------- /contrib/systemd/offlineimap-oneshot@.timer: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Offlineimap Query Timer for account %i 3 | 4 | [Timer] 5 | OnBootSec=1m 6 | OnUnitInactiveSec=15m 7 | 8 | [Install] 9 | WantedBy=default.target 10 | -------------------------------------------------------------------------------- /contrib/systemd/offlineimap.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Offlineimap Service 3 | Documentation=man:offlineimap(1) 4 | 5 | [Service] 6 | ExecStart=/usr/bin/offlineimap -u basic 7 | Restart=on-failure 8 | RestartSec=60 9 | 10 | [Install] 11 | WantedBy=default.target 12 | -------------------------------------------------------------------------------- /contrib/systemd/offlineimap@.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Offlineimap Service for account %i 3 | Documentation=man:offlineimap(1) 4 | 5 | [Service] 6 | ExecStart=/usr/bin/offlineimap -a %i -u basic 7 | Restart=on-failure 8 | RestartSec=60 9 | 10 | [Install] 11 | WantedBy=default.target 12 | -------------------------------------------------------------------------------- /contrib/tested-by.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | """ 4 | 5 | Put into Public Domain, by Nicolas Sebrecht. 6 | 7 | Manage the feedbacks of the testers for the release notes. 8 | 9 | """ 10 | 11 | from os import system 12 | import argparse 13 | 14 | from helpers import CACHEDIR, EDITOR, Testers, User, Git 15 | 16 | 17 | class App(object): 18 | def __init__(self): 19 | self.args = None 20 | self.testers = Testers() 21 | self.feedbacks = None 22 | 23 | 24 | def _getTestersByFeedback(self): 25 | if self.feedbacks is not None: 26 | return self.feedbacks 27 | 28 | feedbackOk = [] 29 | feedbackNo = [] 30 | 31 | for tester in self.testers.get(): 32 | if tester.getFeedback() is True: 33 | feedbackOk.append(tester) 34 | else: 35 | feedbackNo.append(tester) 36 | 37 | for array in [feedbackOk, feedbackNo]: 38 | array.sort(key=lambda t: t.getName()) 39 | 40 | self.feedbacks = feedbackOk + feedbackNo 41 | 42 | def parseArgs(self): 43 | parser = argparse.ArgumentParser(description='Manage the feedbacks.') 44 | 45 | parser.add_argument('--add', '-a', dest='add_tester', 46 | help='Add tester') 47 | parser.add_argument('--delete', '-d', dest='delete_tester', 48 | type=int, 49 | help='Delete tester NUMBER') 50 | parser.add_argument('--list', '-l', dest='list_all_testers', 51 | action='store_true', 52 | help='List the testers') 53 | parser.add_argument('--switchFeedback', '-s', dest='switch_feedback', 54 | action='store_true', 55 | help='Switch the feedback of a tester') 56 | 57 | self.args = parser.parse_args() 58 | 59 | def run(self): 60 | if self.args.list_all_testers is True: 61 | self.listTesters() 62 | if self.args.switch_feedback is True: 63 | self.switchFeedback() 64 | elif self.args.add_tester: 65 | self.addTester(self.args.add_tester) 66 | elif type(self.args.delete_tester) == int: 67 | self.deleteTester(self.args.delete_tester) 68 | 69 | def addTester(self, strTester): 70 | try: 71 | splitted = strTester.split('<') 72 | name = splitted[0].strip() 73 | email = "<{}".format(splitted[1]).strip() 74 | except Exception as e: 75 | print(e) 76 | print("expected format is: 'Firstname Lastname '") 77 | exit(2) 78 | self.testers.add(name, email) 79 | self.testers.write() 80 | 81 | def deleteTester(self, number): 82 | self.listTesters() 83 | removed = self.feedbacks.pop(number) 84 | self.testers.remove(removed) 85 | 86 | print("New list:") 87 | self.feedbacks = None 88 | self.listTesters() 89 | print("Removed: {}".format(removed)) 90 | ans = User.request("Save on disk? (s/Q)").lower() 91 | if ans in ['s']: 92 | self.testers.write() 93 | 94 | 95 | def listTesters(self): 96 | self._getTestersByFeedback() 97 | 98 | count = 0 99 | for tester in self.feedbacks: 100 | feedback = "ok" 101 | if tester.getFeedback() is not True: 102 | feedback = "no" 103 | print("{:02d} - {} {}: {}".format( 104 | count, tester.getName(), tester.getEmail(), feedback 105 | ) 106 | ) 107 | count += 1 108 | 109 | def switchFeedback(self): 110 | self._getTestersByFeedback() 111 | msg = "Switch tester: [/s/q]" 112 | 113 | self.listTesters() 114 | number = User.request(msg) 115 | while number.lower() not in ['s', 'save', 'q', 'quit']: 116 | if number == '': 117 | continue 118 | try: 119 | number = int(number) 120 | self.feedbacks[number].switchFeedback() 121 | except (ValueError, IndexError) as e: 122 | print(e) 123 | exit(1) 124 | finally: 125 | self.listTesters() 126 | number = User.request(msg) 127 | if number in ['s', 'save']: 128 | self.testers.write() 129 | self.listTesters() 130 | 131 | def reset(self): 132 | self.testers.reset() 133 | self.testers.write() 134 | 135 | #def updateMailaliases(self): 136 | 137 | if __name__ == '__main__': 138 | Git.chdirToRepositoryTopLevel() 139 | 140 | app = App() 141 | app.parseArgs() 142 | app.run() 143 | -------------------------------------------------------------------------------- /contrib/upcoming.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | """ 4 | 5 | Put into Public Domain, by Nicolas Sebrecht. 6 | 7 | Produce the "upcoming release" notes. 8 | 9 | """ 10 | 11 | from os import system 12 | 13 | from helpers import ( 14 | MAILING_LIST, CACHEDIR, EDITOR, Testers, Git, OfflineimapInfo, User 15 | ) 16 | 17 | 18 | 19 | UPCOMING_FILE = "{}/upcoming.txt".format(CACHEDIR) 20 | UPCOMING_HEADER = "{}/upcoming-header.txt".format(CACHEDIR) 21 | 22 | # Header is like: 23 | # 24 | #Message-Id: <{messageId}> 25 | #Date: {date} 26 | #From: {name} <{email}> 27 | #To: {mailinglist} 28 | #Cc: {ccList} 29 | #Subject: [ANNOUNCE] upcoming offlineimap v{expectedVersion} 30 | # 31 | ## Notes 32 | # 33 | #I think it's time for a new release. 34 | # 35 | #I aim to make the new release in one week, approximately. If you'd like more 36 | #time, please let me know. ,-) 37 | # 38 | #Please, send me a mail to confirm it works for you. This will be written in the 39 | #release notes and the git logs. 40 | # 41 | # 42 | ## Authors 43 | # 44 | 45 | 46 | if __name__ == '__main__': 47 | offlineimapInfo = OfflineimapInfo() 48 | 49 | print("Will read headers from {}".format(UPCOMING_HEADER)) 50 | Git.chdirToRepositoryTopLevel() 51 | oVersion = offlineimapInfo.getVersion() 52 | ccList = Testers.listTestersInTeam() 53 | authors = Git.getAuthorsList(oVersion) 54 | for author in authors: 55 | email = author.getEmail() 56 | if email not in ccList: 57 | ccList.append(email) 58 | 59 | with open(UPCOMING_FILE, 'w') as upcoming, \ 60 | open(UPCOMING_HEADER, 'r') as fd_header: 61 | header = {} 62 | 63 | header['messageId'] = Git.buildMessageId() 64 | header['date'] = Git.buildDate() 65 | header['name'], header['email'] = Git.getLocalUser() 66 | header['mailinglist'] = MAILING_LIST 67 | header['expectedVersion'] = User.request("Expected new version?") 68 | header['ccList'] = ", ".join(ccList) 69 | 70 | upcoming.write(fd_header.read().format(**header).lstrip()) 71 | upcoming.write(Git.getShortlog(oVersion)) 72 | 73 | upcoming.write("\n\n# Diffstat\n\n") 74 | upcoming.write(Git.getDiffstat(oVersion)) 75 | upcoming.write("\n\n\n-- \n{}\n".format(Git.getLocalUser()[0])) 76 | 77 | system("{} {}".format(EDITOR, UPCOMING_FILE)) 78 | print("{} written".format(UPCOMING_FILE)) 79 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # This program is free software under the terms of the GNU General Public 2 | # License. See the COPYING file which must come with this package. 3 | 4 | SOURCES = $(wildcard *.rst) 5 | HTML_TARGETS = $(patsubst %.rst,%.html,$(SOURCES)) 6 | 7 | RM = rm 8 | RST2HTML=`type rst2html >/dev/null 2>&1 && echo rst2html || echo rst2html.py` 9 | RST2MAN=`type rst2man >/dev/null 2>&1 && echo rst2man || echo rst2man.py` 10 | SPHINXBUILD = sphinx-build 11 | 12 | docs: man api 13 | 14 | html: $(HTML_TARGETS) 15 | 16 | $(HTML_TARGETS): %.html : %.rst 17 | $(RST2HTML) $? $@ 18 | 19 | manhtml: offlineimap.html offlineimapui.html 20 | 21 | offlineimap.html: offlineimap.txt offlineimap.known_issues.txt 22 | a2x -v -d manpage -D manhtml -f xhtml $< 23 | 24 | offlineimapui.html: offlineimapui.txt 25 | a2x -v -d manpage -D manhtml -f xhtml $< 26 | 27 | 28 | man: offlineimap.1 offlineimapui.7 29 | 30 | offlineimap.1: offlineimap.txt offlineimap.known_issues.txt 31 | a2x -v -d manpage -f manpage $< 32 | 33 | offlineimapui.7: offlineimapui.txt 34 | a2x -v -d manpage -f manpage $< 35 | 36 | api: 37 | $(SPHINXBUILD) -b html -d html/doctrees doc-src html 38 | 39 | websitedoc: 40 | ./website-doc.sh releases 41 | ./website-doc.sh api 42 | ./website-doc.sh html 43 | ./website-doc.sh contrib 44 | 45 | clean: 46 | $(RM) -f $(HTML_TARGETS) 47 | $(RM) -f offlineimap.1 48 | $(RM) -f offlineimap.7 49 | $(RM) -f manhtml/* 50 | $(RM) -rf html/* 51 | -find . -name '*.html' -exec rm -f {} \; 52 | 53 | .PHONY: clean doc 54 | -------------------------------------------------------------------------------- /docs/build-uploads.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # vim: expandtab ts=2 : 4 | 5 | WEBSITE_UPLOADS='./website/_uploads' 6 | 7 | while true 8 | do 9 | test -d .git && break 10 | cd .. 11 | done 12 | 13 | set -e 14 | 15 | echo "make clean" 16 | make clean >/dev/null 17 | echo "make targz" 18 | make targz >/dev/null 19 | 20 | # Defined in the root Makefile. 21 | version="$(./offlineimap.py --version)" 22 | abbrev="$(git log --format='%h' HEAD~1..)" 23 | targz="../offlineimap-v${version}-${abbrev}.tar.gz" 24 | 25 | filename="offlineimap-v${version}.tar.gz" 26 | 27 | mv -v "$targz" "${WEBSITE_UPLOADS}/${filename}" 28 | cd "$WEBSITE_UPLOADS" 29 | for digest in sha1 sha256 sha512 30 | do 31 | target="${filename}.${digest}" 32 | echo "Adding digest ${WEBSITE_UPLOADS}/${target}" 33 | "${digest}sum" "$filename" > "$target" 34 | done 35 | -------------------------------------------------------------------------------- /docs/doc-src/API.rst: -------------------------------------------------------------------------------- 1 | .. OfflineImap API documentation 2 | 3 | .. currentmodule:: offlineimap 4 | 5 | .. _API docs: 6 | 7 | :mod:`offlineimap's` API documentation 8 | ====================================== 9 | 10 | Within :mod:`offlineimap`, the classes :class:`OfflineImap` provides the 11 | high-level functionality. The rest of the classes should usually not needed to 12 | be touched by the user. Email repositories are represented by a 13 | :class:`offlineimap.repository.Base.BaseRepository` or derivatives (see 14 | :mod:`offlineimap.repository` for details). A folder within a repository is 15 | represented by a :class:`offlineimap.folder.Base.BaseFolder` or any derivative 16 | from :mod:`offlineimap.folder`. 17 | 18 | This page contains the main API overview of OfflineImap |release|. 19 | 20 | OfflineImap can be imported as:: 21 | 22 | from offlineimap import OfflineImap 23 | 24 | 25 | :mod:`offlineimap` -- The OfflineImap module 26 | ============================================= 27 | 28 | .. module:: offlineimap 29 | 30 | .. autoclass:: offlineimap.OfflineImap(cmdline_opts = None) 31 | :members: 32 | :inherited-members: 33 | :undoc-members: 34 | :private-members: 35 | 36 | 37 | :class:`offlineimap.account` 38 | ============================ 39 | 40 | An :class:`accounts.Account` connects two email repositories that are to be 41 | synced. It comes in two flavors, normal and syncable. 42 | 43 | .. autoclass:: offlineimap.accounts.Account 44 | 45 | .. autoclass:: offlineimap.accounts.SyncableAccount 46 | :members: 47 | :inherited-members: 48 | 49 | .. autodata:: ui 50 | 51 | Contains the current :mod:`offlineimap.ui`, and can be used for logging etc. 52 | 53 | :exc:`OfflineImapError` -- A Notmuch execution error 54 | -------------------------------------------------------- 55 | 56 | .. autoexception:: offlineimap.error.OfflineImapError 57 | :members: 58 | 59 | This exception inherits directly from :exc:`Exception` and is raised 60 | on errors during the offlineimap execution. It has an attribute 61 | `severity` that denotes the severity level of the error. 62 | 63 | 64 | :mod:`offlineimap.globals` -- module with global variables 65 | ========================================================== 66 | 67 | Module `offlineimap.globals` provides the read-only storage 68 | for the global variables. 69 | 70 | All exported module attributes can be set manually, but this practice 71 | is highly discouraged and shouldn't be used. 72 | However, attributes of all stored variables can only be read, write 73 | access to them is denied. 74 | 75 | Currently, we have only :attr:`options` attribute that holds 76 | command-line options as returned by OptionParser. 77 | The value of :attr:`options` must be set by :func:`set_options` 78 | prior to its first use. 79 | 80 | .. automodule:: offlineimap.globals 81 | :members: 82 | 83 | .. data:: options 84 | 85 | You can access the values of stored options using the usual 86 | syntax, offlineimap.globals.options. 87 | -------------------------------------------------------------------------------- /docs/doc-src/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # pyDNS documentation build configuration file, created by 4 | # sphinx-quickstart on Tue Feb 2 10:00:47 2010. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | sys.path.insert(0, os.path.abspath('../..')) 20 | 21 | from offlineimap import __version__, __author__, __copyright__ 22 | # -- General configuration ----------------------------------------------------- 23 | 24 | # Add any Sphinx extension module names here, as strings. They can be extensions 25 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 26 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 27 | 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'sphinx.ext.viewcode'] 28 | autoclass_content = "both" 29 | 30 | # Add any paths that contain templates here, relative to this directory. 31 | templates_path = ['_templates'] 32 | 33 | # The suffix of source filenames. 34 | source_suffix = '.rst' 35 | 36 | # The encoding of source files. 37 | #source_encoding = 'utf-8' 38 | 39 | # The master toctree document. 40 | master_doc = 'index' 41 | 42 | # General information about the project. 43 | project = u'OfflineIMAP' 44 | copyright = __copyright__ 45 | 46 | # The version info for the project you're documenting, acts as replacement for 47 | # |version| and |release|, also used in various other places throughout the 48 | # built documents. 49 | # 50 | # The short X.Y version. 51 | version = __version__ 52 | # The full version, including alpha/beta/rc tags. 53 | release = __version__ 54 | 55 | # The language for content autogenerated by Sphinx. Refer to documentation 56 | # for a list of supported languages. 57 | #language = None 58 | 59 | # There are two options for replacing |today|: either, you set today to some 60 | # non-false value, then it is used: 61 | #today = '' 62 | # Else, today_fmt is used as the format for a strftime call. 63 | #today_fmt = '%B %d, %Y' 64 | 65 | # List of documents that shouldn't be included in the build. 66 | #unused_docs = [] 67 | 68 | # List of directories, relative to source directory, that shouldn't be searched 69 | # for source files. 70 | exclude_trees = [] 71 | 72 | # The reST default role (used for this markup: `text`) to use for all documents. 73 | #default_role = None 74 | 75 | # If true, '()' will be appended to :func: etc. cross-reference text. 76 | #add_function_parentheses = True 77 | 78 | # If true, the current module name will be prepended to all description 79 | # unit titles (such as .. function::). 80 | add_module_names = False 81 | 82 | # If true, sectionauthor and moduleauthor directives will be shown in the 83 | # output. They are ignored by default. 84 | #show_authors = False 85 | 86 | # The name of the Pygments (syntax highlighting) style to use. 87 | pygments_style = 'sphinx' 88 | 89 | # A list of ignored prefixes for module index sorting. 90 | #modindex_common_prefix = [] 91 | 92 | 93 | # -- Options for HTML output --------------------------------------------------- 94 | 95 | # The theme to use for HTML and HTML Help pages. Major themes that come with 96 | # Sphinx are currently 'default' and 'sphinxdoc'. 97 | html_theme = 'default' 98 | #html_style = '' 99 | # Theme options are theme-specific and customize the look and feel of a theme 100 | # further. For a list of options available for each theme, see the 101 | # documentation. 102 | #html_theme_options = {} 103 | 104 | # Add any paths that contain custom themes here, relative to this directory. 105 | #html_theme_path = [] 106 | 107 | # The name for this set of Sphinx documents. If None, it defaults to 108 | # " v documentation". 109 | #html_title = None 110 | 111 | # A shorter title for the navigation bar. Default is the same as html_title. 112 | #html_short_title = None 113 | 114 | # The name of an image file (relative to this directory) to place at the top 115 | # of the sidebar. 116 | #html_logo = None 117 | 118 | # The name of an image file (within the static path) to use as favicon of the 119 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 120 | # pixels large. 121 | #html_favicon = None 122 | 123 | # Add any paths that contain custom static files (such as style sheets) here, 124 | # relative to this directory. They are copied after the builtin static files, 125 | # so a file named "default.css" will overwrite the builtin "default.css". 126 | #html_static_path = ['html'] 127 | 128 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 129 | # using the given strftime format. 130 | #html_last_updated_fmt = '%b %d, %Y' 131 | 132 | # If true, SmartyPants will be used to convert quotes and dashes to 133 | # typographically correct entities. 134 | #html_use_smartypants = True 135 | 136 | # Custom sidebar templates, maps document names to template names. 137 | #html_sidebars = {} 138 | 139 | # Additional templates that should be rendered to pages, maps page names to 140 | # template names. 141 | #html_additional_pages = {} 142 | 143 | # If false, no module index is generated. 144 | html_use_modindex = False 145 | 146 | # If false, no index is generated. 147 | #html_use_index = True 148 | 149 | # If true, the index is split into individual pages for each letter. 150 | #html_split_index = False 151 | 152 | # If true, links to the reST sources are added to the pages. 153 | #html_show_sourcelink = True 154 | 155 | # If true, an OpenSearch description file will be output, and all pages will 156 | # contain a tag referring to it. The value of this option must be the 157 | # base URL from which the finished HTML is served. 158 | #html_use_opensearch = '' 159 | 160 | # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). 161 | #html_file_suffix = '' 162 | 163 | # Output file base name for HTML help builder. 164 | htmlhelp_basename = 'dev-doc' 165 | 166 | 167 | # -- Options for LaTeX output -------------------------------------------------- 168 | 169 | # The paper size ('letter' or 'a4'). 170 | #latex_paper_size = 'letter' 171 | 172 | # The font size ('10pt', '11pt' or '12pt'). 173 | #latex_font_size = '10pt' 174 | 175 | # Grouping the document tree into LaTeX files. List of tuples 176 | # (source start file, target name, title, author, documentclass [howto/manual]). 177 | latex_documents = [ 178 | ('index', 'offlineimap.tex', u'OfflineIMAP Documentation', 179 | u'OfflineIMAP contributors', 'manual'), 180 | ] 181 | 182 | # The name of an image file (relative to this directory) to place at the top of 183 | # the title page. 184 | #latex_logo = None 185 | 186 | # For "manual" documents, if this is true, then toplevel headings are parts, 187 | # not chapters. 188 | #latex_use_parts = False 189 | 190 | # Additional stuff for the LaTeX preamble. 191 | #latex_preamble = '' 192 | 193 | # Documents to append as an appendix to all manuals. 194 | #latex_appendices = [] 195 | 196 | # If false, no module index is generated. 197 | #latex_use_modindex = True 198 | 199 | 200 | # Example configuration for intersphinx: refer to the Python standard library. 201 | intersphinx_mapping = {'http://docs.python.org/': None} 202 | -------------------------------------------------------------------------------- /docs/doc-src/dco.rst: -------------------------------------------------------------------------------- 1 | .. _dco 2 | 3 | Developer's Certificate of Origin 4 | ================================= 5 | 6 | v1.1:: 7 | 8 | By making a contribution to this project, I certify that: 9 | 10 | (a) The contribution was created in whole or in part by me and I 11 | have the right to submit it under the open source license 12 | indicated in the file; or 13 | 14 | (b) The contribution is based upon previous work that, to the best 15 | of my knowledge, is covered under an appropriate open source 16 | license and I have the right under that license to submit that 17 | work with modifications, whether created in whole or in part 18 | by me, under the same open source license (unless I am 19 | permitted to submit under a different license), as indicated 20 | in the file; or 21 | 22 | (c) The contribution was provided directly to me by some other 23 | person who certified (a), (b) or (c) and I have not modified 24 | it. 25 | 26 | (d) I understand and agree that this project and the contribution 27 | are public and that a record of the contribution (including all 28 | personal information I submit with it, including my sign-off) is 29 | maintained indefinitely and may be redistributed consistent with 30 | this project or the open source license(s) involved. 31 | 32 | 33 | Then, you just add a line saying:: 34 | 35 | Signed-off-by: Random J Developer 36 | 37 | This line can be automatically added by git if you run the git-commit command 38 | with the ``-s`` option. Signing can made be afterword with ``--amend -s``. 39 | 40 | Notice that you can place your own ``Signed-off-by:`` line when forwarding 41 | somebody else's patch with the above rules for D-C-O. Indeed you are encouraged 42 | to do so. Do not forget to place an in-body ``From:`` line at the beginning to 43 | properly attribute the change to its true author (see above). 44 | 45 | Also notice that a real name is used in the ``Signed-off-by:`` line. Please 46 | don't hide your real name. 47 | 48 | If you like, you can put extra tags at the end: 49 | 50 | Reported-by 51 | is used to to credit someone who found the bug that the patch attempts to fix. 52 | 53 | Acked-by 54 | says that the person who is more familiar with the area the patch attempts to 55 | modify liked the patch. 56 | 57 | Reviewed-by 58 | unlike the other tags, can only be offered by the reviewer and means that she 59 | is completely satisfied that the patch is ready for application. It is 60 | usually offered only after a detailed review. 61 | 62 | Tested-by 63 | is used to indicate that the person applied the patch and found it to have the 64 | desired effect. 65 | 66 | You can also create your own tag or use one that's in common usage such as 67 | ``Thanks-to:``, ``Based-on-patch-by:``, or ``Mentored-by:``. 68 | 69 | 70 | -------------------------------------------------------------------------------- /docs/doc-src/index.rst: -------------------------------------------------------------------------------- 1 | .. OfflineImap documentation master file 2 | .. _OfflineIMAP: http://www.offlineimap.org 3 | 4 | 5 | Welcome to OfflineIMAP's developer documentation 6 | ================================================ 7 | 8 | **License** 9 | :doc:`dco` (dco) 10 | 11 | **Documented APIs** 12 | 13 | .. toctree:: 14 | API 15 | repository 16 | ui 17 | 18 | 19 | .. moduleauthor:: John Goerzen, and many others. See AUTHORS and the git history for a full list. 20 | 21 | :License: This module is covered under the GNU GPL v2 (or later). 22 | -------------------------------------------------------------------------------- /docs/doc-src/repository.rst: -------------------------------------------------------------------------------- 1 | .. currentmodule:: offlineimap.repository 2 | 3 | :mod:`offlineimap.repository` -- Email repositories 4 | ------------------------------------------------------------ 5 | 6 | A derivative of class 7 | :class:`Base.BaseRepository` represents an email 8 | repository depending on the type of storage, possible options are: 9 | 10 | * :class:`IMAPRepository`, 11 | * :class:`MappedIMAPRepository` 12 | * :class:`GmailRepository`, 13 | * :class:`MaildirRepository`, or 14 | * :class:`LocalStatusRepository`. 15 | 16 | Which class you need depends on your account 17 | configuration. The helper class :class:`offlineimap.repository.Repository` is 18 | an *autoloader*, that returns the correct class depending 19 | on your configuration. So when you want to instanciate a new 20 | :mod:`offlineimap.repository`, you will mostly do it through this class. 21 | 22 | .. autoclass:: offlineimap.repository.Repository 23 | :members: 24 | :inherited-members: 25 | 26 | 27 | 28 | :mod:`offlineimap.repository.Base.BaseRepository` -- Representation of a mail repository 29 | ------------------------------------------------------------------------------------------ 30 | .. autoclass:: offlineimap.repository.Base.BaseRepository 31 | :members: 32 | :inherited-members: 33 | :undoc-members: 34 | 35 | .. .. note:: :meth:`foo` 36 | .. .. attribute:: Database.MODE 37 | 38 | Defines constants that are used as the mode in which to open a database. 39 | 40 | MODE.READ_ONLY 41 | Open the database in read-only mode 42 | 43 | MODE.READ_WRITE 44 | Open the database in read-write mode 45 | 46 | .. autoclass:: offlineimap.repository.IMAPRepository 47 | .. autoclass:: offlineimap.repository.MappedIMAPRepository 48 | .. autoclass:: offlineimap.repository.GmailRepository 49 | .. autoclass:: offlineimap.repository.MaildirRepository 50 | .. autoclass:: offlineimap.repository.LocalStatusRepository 51 | 52 | :mod:`offlineimap.folder` -- Basic representation of a local or remote Mail folder 53 | --------------------------------------------------------------------------------------------------------- 54 | 55 | .. autoclass:: offlineimap.folder.Base.BaseFolder 56 | :members: 57 | :inherited-members: 58 | :undoc-members: 59 | 60 | .. .. attribute:: Database.MODE 61 | 62 | Defines constants that are used as the mode in which to open a database. 63 | 64 | MODE.READ_ONLY 65 | Open the database in read-only mode 66 | 67 | MODE.READ_WRITE 68 | Open the database in read-write mode 69 | -------------------------------------------------------------------------------- /docs/doc-src/ui.rst: -------------------------------------------------------------------------------- 1 | :mod:`offlineimap.ui` -- A flexible logging system 2 | -------------------------------------------------------- 3 | 4 | .. currentmodule:: offlineimap.ui 5 | 6 | OfflineImap has various ui systems, that can be selected. They offer various 7 | functionalities. They must implement all functions that the 8 | :class:`offlineimap.ui.UIBase` offers. Early on, the ui must be set using 9 | :meth:`getglobalui` 10 | 11 | .. automethod:: offlineimap.ui.setglobalui 12 | .. automethod:: offlineimap.ui.getglobalui 13 | 14 | Base UI plugin 15 | ^^^^^^^^^^^^^^^^^^^^^^^^^^ 16 | 17 | .. autoclass:: offlineimap.ui.UIBase.UIBase 18 | :members: 19 | :inherited-members: 20 | 21 | .. .. note:: :meth:`foo` 22 | .. .. attribute:: Database.MODE 23 | 24 | Defines constants that are used as the mode in which to open a database. 25 | 26 | MODE.READ_ONLY 27 | Open the database in read-only mode 28 | 29 | MODE.READ_WRITE 30 | Open the database in read-write mode 31 | -------------------------------------------------------------------------------- /docs/manhtml/.lock: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicolas33/offlineimap/ba4ecea9e4e4f14e91f3cbd0dc7c54182b232994/docs/manhtml/.lock -------------------------------------------------------------------------------- /docs/offlineimap.known_issues.txt: -------------------------------------------------------------------------------- 1 | 2 | * Deletions. 3 | + 4 | While in usual run the deletions are propagated. To prevent from data loss, 5 | removing a folder makes offlineimap re-sync the folder. However, propagating the 6 | removal of the whole content of a folder can happen in the two following cases: 7 | 8 | - The whole content of a folder is deleted but the folder directory still 9 | exists. 10 | 11 | - The parent directory of the folder was deleted. 12 | 13 | * SSL3 write pending. 14 | + 15 | Users enabling SSL may hit a bug about "SSL3 write pending". If so, the 16 | account(s) will stay unsynchronised from the time the bug appeared. Running 17 | OfflineIMAP again can help. We are still working on this bug. Patches or 18 | detailed bug reports would be appreciated. Please check you're running the 19 | last stable version and send us a report to the mailing list including the 20 | full log. 21 | 22 | * IDLE support is incomplete and experimental. Bugs may be encountered. 23 | 24 | - No hook exists for "run after an IDLE response". 25 | + 26 | Email will show up, but may not be processed until the next refresh cycle. 27 | 28 | - nametrans may not be supported correctly. 29 | 30 | - IMAP IDLE <-> IMAP IDLE doesn't work yet. 31 | 32 | - IDLE might stop syncing on a system suspend/resume. 33 | 34 | - IDLE may only work "once" per refresh. 35 | + 36 | If you encounter this bug, please send a report to the list! 37 | 38 | * Maildir support in Windows drive. 39 | + 40 | Maildir uses colon character (:) in message file names. Colon is however 41 | forbidden character in windows drives. There are several workarounds for that 42 | situation: 43 | 44 | . Enable file name character translation in windows registry (not tested). 45 | - 46 | 47 | . Use cygwin managed mount (not tested). 48 | - not available anymore since cygwin 1.7 49 | 50 | . Use "maildir-windows-compatible = yes" account OfflineIMAP configuration. 51 | - That makes OfflineIMAP to use exclamation mark (!) instead of colon for 52 | storing messages. Such files can be written to windows partitions. But 53 | you will probably loose compatibility with other programs trying to 54 | read the same Maildir. 55 | + 56 | - Exclamation mark was chosen because of the note in 57 | http://docs.python.org/library/mailbox.html 58 | + 59 | - If you have some messages already stored without this option, you will 60 | have to re-sync them again 61 | 62 | * OfflineIMAP confused after system suspend. 63 | + 64 | When resuming a suspended session, OfflineIMAP does not cleanly handles the 65 | broken socket(s) if socktimeout option is not set. 66 | You should enable this option with a value like 10. 67 | 68 | * OfflineIMAP confused when mails change while in a sync. 69 | + 70 | When OfflineIMAP is syncing, some events happening since the invocation on 71 | remote or local side are badly handled. OfflineIMAP won't track for changes 72 | during the sync. 73 | 74 | 75 | * Sharing a maildir with multiple IMAP servers. 76 | + 77 | Generally a word of caution mixing IMAP repositories on the same Maildir root. 78 | You have to be careful that you *never* use the same maildir folder for 2 IMAP 79 | servers. In the best case, the folder MD5 will be different, and you will get 80 | a loop where it will upload your mails to both servers in turn (infinitely!) 81 | as it thinks you have placed new mails in the local Maildir. In the worst 82 | case, the MD5 is the same (likely) and mail UIDs overlap (likely too!) and it 83 | will fail to sync some mails as it thinks they are already existent. 84 | + 85 | I would create a new local Maildir Repository for the Personal Gmail and 86 | use a different root to be on the safe side here. You could e.g. use 87 | 88 | `~/mail/Pro' as Maildir root for the ProGmail and 89 | `~/mail/Personal' as root for the personal one. 90 | + 91 | If you then point your local mutt, or whatever MUA you use to `~/mail/' 92 | as root, it should still recognize all folders. 93 | 94 | 95 | * Edge cases with maxage causing too many messages to be synced. 96 | + 97 | All messages from at most maxage days ago (+/- a few hours, depending on 98 | timezones) are synced, but there are cases in which older messages can also be 99 | synced. This happens when a message's UID is significantly higher than those of 100 | other messages with similar dates, e.g. when messages are added to the local 101 | folder behind offlineimap's back, causing them to get assigned a new UID, or 102 | when offlineimap first syncs a pre-existing Maildir. In the latter case, it 103 | could appear as if a noticeable and random subset of old messages are synced. 104 | 105 | * Offlineimap hangs. 106 | + 107 | When having unexpected hangs it's advised to set `singlethreadperfolder' to 108 | 'yes', especially when in IMAP/IMAP mode (no maildir). 109 | 110 | * Passwords in netrc. 111 | + 112 | Offlineimap doesn't know how to retrieve passwords when more than one account is 113 | stored in the netrc file. See 114 | . 115 | 116 | * XOAUTH2 117 | + 118 | XOAUTH2 might be a bit tricky to set up. Make sure you've followed the step to 119 | step guide in 'offlineimap.conf'. The known bugs about Gmail are tracked at 120 | . 121 | + 122 | Sometimes, you might hit one of the following error: 123 | 124 | - [imap]: xoauth2handler: response "{u'error': u'invalid_grant'}" 125 | - oauth2handler got: {u'error': u'invalid_grant'} 126 | 127 | + 128 | In such case, we had reports that generating a new refresh token from the same 129 | client ID and secret can help. 130 | + 131 | .Google documentation on "invalid_grant" 132 | ---- 133 | When you try to use a refresh token, the following returns you an 134 | invalid_grant error: 135 | 136 | - Your server's clock is not in sync with network time protocol - NTP. 137 | - The refresh token limit has been exceeded. 138 | ---- 139 | + 140 | .Token expiration 141 | ---- 142 | It is possible that a granted token might no longer work. A token might stop 143 | working for one of these reasons: 144 | 145 | - The user has revoked access. 146 | - The token has not been used for six months. 147 | - The user changed passwords and the token contains Gmail scopes. 148 | - The user account has exceeded a certain number of token requests. 149 | 150 | There is currently a limit of 50 refresh tokens per user account per client. If 151 | the limit is reached, creating a new token automatically invalidates the oldest 152 | token without warning. This limit does not apply to service accounts. 153 | ---- 154 | + 155 | See 156 | and 157 | to know more. 158 | 159 | * "does not have message with UID" with Microsoft servers 160 | + 161 | `ERROR: IMAP server 'Server ### Remote' does not have a message with UID 'xxx'` 162 | + 163 | Microsoft IMAP servers are not compliant with the RFC. It is currently required 164 | to folderfilter some faulting folders. See 165 | http://www.offlineimap.org/doc/FAQ.html#exchange-and-office365 for a detailed 166 | list. 167 | 168 | -------------------------------------------------------------------------------- /docs/offlineimapui.txt: -------------------------------------------------------------------------------- 1 | 2 | offlineimapui(7) 3 | ================ 4 | 5 | NAME 6 | ---- 7 | offlineimapui - The User Interfaces 8 | 9 | DESCRIPTION 10 | ----------- 11 | 12 | OfflineIMAP comes with different UIs, each aiming its own purpose. 13 | 14 | 15 | TTYUI 16 | ------ 17 | 18 | TTYUI interface is for people running in terminals. It prints out basic 19 | status messages and is generally friendly to use on a console or xterm. 20 | 21 | 22 | Basic 23 | ------ 24 | 25 | Basic is designed for situations in which OfflineIMAP will be run non-attended 26 | and the status of its execution will be logged. 27 | 28 | This user interface is not capable of reading a password from the keyboard; 29 | account passwords must be specified using one of the configuration file 30 | options. For example, it will not print periodic sleep announcements and tends 31 | to be a tad less verbose, in general. 32 | 33 | 34 | Blinkenlights 35 | ------------- 36 | 37 | Blinkenlights is an interface designed to be sleek, fun to watch, and 38 | informative of the overall picture of what OfflineIMAP is doing. 39 | 40 | Blinkenlights contains a row of "LEDs" with command buttons and a log. The 41 | log shows more detail about what is happening and is color-coded to match the 42 | color of the lights. 43 | 44 | Each light in the Blinkenlights interface represents a thread of execution -- 45 | that is, a particular task that OfflineIMAP is performing right now. The 46 | colors indicate what task the particular thread is performing, and are as 47 | follows: 48 | 49 | * Black 50 | 51 | indicates that this light's thread has terminated; it will light up again 52 | later when new threads start up. So, black indicates no activity. 53 | 54 | * Red (Meaning 1) 55 | 56 | is the color of the main program's thread, which basically does nothing but 57 | monitor the others. It might remind you of HAL 9000 in 2001. 58 | 59 | * Gray 60 | 61 | indicates that the thread is establishing a new connection to the IMAP 62 | server. 63 | 64 | * Purple 65 | 66 | is the color of an account synchronization thread that is monitoring the 67 | progress of the folders in that account (not generating any I/O). 68 | 69 | * Cyan 70 | 71 | indicates that the thread is syncing a folder. 72 | 73 | * Green 74 | 75 | means that a folder's message list is being loaded. 76 | 77 | * Blue 78 | 79 | is the color of a message synchronization controller thread. 80 | 81 | * Orange 82 | 83 | indicates that an actual message is being copied. (We use fuchsia for fake 84 | messages.) 85 | 86 | * Red (meaning 2) 87 | 88 | indicates that a message is being deleted. 89 | 90 | * Yellow / bright orange 91 | 92 | indicates that message flags are being added. 93 | 94 | * Pink / bright red 95 | 96 | indicates that message flags are being removed. 97 | 98 | * Red / Black Flashing 99 | 100 | corresponds to the countdown timer that runs between synchronizations. 101 | 102 | 103 | The name of this interfaces derives from a bit of computer history. Eric 104 | Raymond's Jargon File defines blinkenlights, in part, as: 105 | 106 | Front-panel diagnostic lights on a computer, esp. a dinosaur. Now that 107 | dinosaurs are rare, this term usually refers to status lights on a modem, 108 | network hub, or the like. 109 | 110 | This term derives from the last word of the famous blackletter-Gothic sign in 111 | mangled pseudo-German that once graced about half the computer rooms in the 112 | English-speaking world. One version ran in its entirety as follows: 113 | 114 | ACHTUNG! ALLES LOOKENSPEEPERS! 115 | 116 | Das computermachine ist nicht fuer gefingerpoken und mittengrabben. 117 | Ist easy schnappen der springenwerk, blowenfusen und poppencorken 118 | mit spitzensparken. Ist nicht fuer gewerken bei das dumpkopfen. 119 | Das rubbernecken sichtseeren keepen das cotten-pickenen hans in das 120 | pockets muss; relaxen und watchen das blinkenlichten. 121 | 122 | 123 | Quiet 124 | ----- 125 | 126 | It will output nothing except errors and serious warnings. Like Basic, this 127 | user interface is not capable of reading a password from the keyboard; account 128 | passwords must be specified using one of the configuration file options. 129 | 130 | 131 | Syslog 132 | ------ 133 | 134 | Syslog is designed for situations where OfflineIMAP is run as a daemon (e.g., 135 | as a systemd --user service), but errors should be forwarded to the system log. 136 | Like Basic, this user interface is not capable of reading a password from the 137 | keyboard; account passwords must be specified using one of the configuration 138 | file options. 139 | 140 | 141 | MachineUI 142 | --------- 143 | 144 | MachineUI generates output in a machine-parsable format. It is designed 145 | for other programs that will interface to OfflineIMAP. 146 | 147 | 148 | See Also 149 | -------- 150 | 151 | offlineimap(1) 152 | -------------------------------------------------------------------------------- /docs/rfcs/README.md: -------------------------------------------------------------------------------- 1 | 2 | All RFCs related to IMAP. 3 | 4 | TODO: Add a brief introduction here to introduce the most important RFCs. 5 | -------------------------------------------------------------------------------- /docs/rfcs/rfc1733.models_in_IMAP4.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Network Working Group M. Crispin 8 | Request for Comments: 1733 University of Washington 9 | Category: Informational December 1994 10 | 11 | 12 | DISTRIBUTED ELECTRONIC MAIL MODELS IN IMAP4 13 | 14 | 15 | Status of this Memo 16 | 17 | This memo provides information for the Internet community. This memo 18 | does not specify an Internet standard of any kind. Distribution of 19 | this memo is unlimited. 20 | 21 | 22 | Distributed Electronic Mail Models 23 | 24 | There are three fundamental models of client/server email: offline, 25 | online, and disconnected use. IMAP4 can be used in any one of these 26 | three models. 27 | 28 | The offline model is the most familiar form of client/server email 29 | today, and is used by protocols such as POP-3 (RFC 1225) and UUCP. 30 | In this model, a client application periodically connects to a 31 | server. It downloads all the pending messages to the client machine 32 | and deletes these from the server. Thereafter, all mail processing 33 | is local to the client. This model is store-and-forward; it moves 34 | mail on demand from an intermediate server (maildrop) to a single 35 | destination machine. 36 | 37 | The online model is most commonly used with remote filesystem 38 | protocols such as NFS. In this model, a client application 39 | manipulates mailbox data on a server machine. A connection to the 40 | server is maintained throughout the session. No mailbox data are 41 | kept on the client; the client retrieves data from the server as is 42 | needed. IMAP4 introduces a form of the online model that requires 43 | considerably less network bandwidth than a remote filesystem 44 | protocol, and provides the opportunity for using the server for CPU 45 | or I/O intensive functions such as parsing and searching. 46 | 47 | The disconnected use model is a hybrid of the offline and online 48 | models, and is used by protocols such as PCMAIL (RFC 1056). In this 49 | model, a client user downloads some set of messages from the server, 50 | manipulates them offline, then at some later time uploads the 51 | changes. The server remains the authoritative repository of the 52 | messages. The problems of synchronization (particularly when 53 | multiple clients are involved) are handled through the means of 54 | unique identifiers for each message. 55 | 56 | 57 | 58 | Crispin [Page 1] 59 | 60 | RFC 1733 IMAP4 - Model December 1994 61 | 62 | 63 | Each of these models have their own strengths and weaknesses: 64 | 65 | Feature Offline Online Disc 66 | ------- ------- ------ ---- 67 | Can use multiple clients NO YES YES 68 | Minimum use of server connect time YES NO YES 69 | Minimum use of server resources YES NO NO 70 | Minimum use of client disk resources NO YES NO 71 | Multiple remote mailboxes NO YES YES 72 | Fast startup NO YES NO 73 | Mail processing when not online YES NO YES 74 | 75 | Although IMAP4 has its origins as a protocol designed to accommodate 76 | the online model, it can support the other two models as well. This 77 | makes possible the creation of clients that can be used in any of the 78 | three models. For example, a user may wish to switch between the 79 | online and disconnected models on a regular basis (e.g. owing to 80 | travel). 81 | 82 | IMAP4 is designed to transmit message data on demand, and to provide 83 | the facilities necessary for a client to decide what data it needs at 84 | any particular time. There is generally no need to do a wholesale 85 | transfer of an entire mailbox or even of the complete text of a 86 | message. This makes a difference in situations where the mailbox is 87 | large, or when the link to the server is slow. 88 | 89 | More specifically, IMAP4 supports server-based RFC 822 and MIME 90 | processing. With this information, it is possible for a client to 91 | determine in advance whether it wishes to retrieve a particular 92 | message or part of a message. For example, a user connected to an 93 | IMAP4 server via a dialup link can determine that a message has a 94 | 2000 byte text segment and a 40 megabyte video segment, and elect to 95 | fetch only the text segment. 96 | 97 | In IMAP4, the client/server relationship lasts only for the duration 98 | of the TCP connection. There is no registration of clients. Except 99 | for any unique identifiers used in disconnected use operation, the 100 | client initially has no knowledge of mailbox state and learns it from 101 | the IMAP4 server when a mailbox is selected. This initial transfer 102 | is minimal; the client requests additional state data as it needs. 103 | 104 | As noted above, the choice for the location of mailbox data depends 105 | upon the model chosen. The location of message state (e.g. whether 106 | or not a message has been read or answered) is also determined by the 107 | model, and is not necessarily the same as the location of the mailbox 108 | data. For example, in the online model message state can be co- 109 | located with mailbox data; it can also be located elsewhere (on the 110 | client or on a third agent) using unique identifiers to achieve 111 | 112 | 113 | 114 | Crispin [Page 2] 115 | 116 | RFC 1733 IMAP4 - Model December 1994 117 | 118 | 119 | common reference across sessions. The latter is particularly useful 120 | with a server that exports public data such as netnews and does not 121 | maintain per-user state. 122 | 123 | The IMAP4 protocol provides the generality to implement these 124 | different models. This is done by means of server and (especially) 125 | client configuration, and not by requiring changes to the protocol or 126 | the implementation of the protocol. 127 | 128 | 129 | Security Considerations 130 | 131 | Security issues are not discussed in this memo. 132 | 133 | 134 | Author's Address: 135 | 136 | Mark R. Crispin 137 | Networks and Distributed Computing, JE-30 138 | University of Washington 139 | Seattle, WA 98195 140 | 141 | Phone: (206) 543-5762 142 | 143 | EMail: MRC@CAC.Washington.EDU 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | Crispin [Page 3] 171 | 172 | -------------------------------------------------------------------------------- /docs/rfcs/rfc2061.compatibility_IMAP4-IMAP2bis.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Network Working Group M. Crispin 8 | Request for Comments: 2061 University of Washington 9 | Category: Informational December 1996 10 | 11 | 12 | IMAP4 COMPATIBILITY WITH IMAP2BIS 13 | 14 | Status of this Memo 15 | 16 | This memo provides information for the Internet community. This memo 17 | does not specify an Internet standard of any kind. Distribution of 18 | this memo is unlimited. 19 | 20 | Introduction 21 | 22 | The Internet Message Access Protocol (IMAP) has been through several 23 | revisions and variants in its 10-year history. Many of these are 24 | either extinct or extremely rare; in particular, several undocumented 25 | variants and the variants described in RFC 1064, RFC 1176, and RFC 26 | 1203 fall into this category. 27 | 28 | One variant, IMAP2bis, is at the time of this writing very common and 29 | has been widely distributed with the Pine mailer. Unfortunately, 30 | there is no definite document describing IMAP2bis. This document is 31 | intended to be read along with RFC 1176 and the most recent IMAP4 32 | specification (RFC 2060) to assist implementors in creating an IMAP4 33 | implementation to interoperate with implementations that conform to 34 | earlier specifications. Nothing in this document is required by the 35 | IMAP4 specification; implementors must decide for themselves whether 36 | they want their implementation to fail if it encounters old software. 37 | 38 | At the time of this writing, IMAP4 has been updated from the version 39 | described in RFC 1730. An implementor who wishes to interoperate 40 | with both RFC 1730 and RFC 2060 should refer to both documents. 41 | 42 | This information is not complete; it reflects current knowledge of 43 | server and client implementations as well as "folklore" acquired in 44 | the evolution of the protocol. It is NOT a description of how to 45 | interoperate with all variants of IMAP, but rather with the old 46 | variant that is most likely to be encountered. For detailed 47 | information on interoperating with other old variants, refer to RFC 48 | 1732. 49 | 50 | IMAP4 client interoperability with IMAP2bis servers 51 | 52 | A quick way to check whether a server implementation supports the 53 | IMAP4 specification is to try the CAPABILITY command. An OK response 54 | will indicate which variant(s) of IMAP4 are supported by the server. 55 | 56 | 57 | 58 | Crispin Informational [Page 1] 59 | 60 | RFC 2061 IMAP4 Compatibility December 1996 61 | 62 | 63 | If the client does not find any of its known variant in the response, 64 | it should treat the server as IMAP2bis. A BAD response indicates an 65 | IMAP2bis or older server. 66 | 67 | Most IMAP4 facilities are in IMAP2bis. The following exceptions 68 | exist: 69 | 70 | CAPABILITY command 71 | The absense of this command indicates IMAP2bis (or older). 72 | 73 | AUTHENTICATE command. 74 | Use the LOGIN command. 75 | 76 | LSUB, SUBSCRIBE, and UNSUBSCRIBE commands 77 | No direct functional equivalent. IMAP2bis had a concept 78 | called "bboards" which is not in IMAP4. RFC 1176 supported 79 | these with the BBOARD and FIND BBOARDS commands. IMAP2bis 80 | augmented these with the FIND ALL.BBOARDS, SUBSCRIBE BBOARD, 81 | and UNSUBSCRIBE BBOARD commands. It is recommended that 82 | none of these commands be implemented in new software, 83 | including servers that support old clients. 84 | 85 | LIST command 86 | Use the command FIND ALL.MAILBOXES, which has a similar syn- 87 | tax and response to the FIND MAILBOXES command described in 88 | RFC 1176. The FIND MAILBOXES command is unlikely to produce 89 | useful information. 90 | 91 | * in a sequence 92 | Use the number of messages in the mailbox from the EXISTS 93 | unsolicited response. 94 | 95 | SEARCH extensions (character set, additional criteria) 96 | Reformulate the search request using only the RFC 1176 syn- 97 | tax. This may entail doing multiple searches to achieve the 98 | desired results. 99 | 100 | BODYSTRUCTURE fetch data item 101 | Use the non-extensible BODY data item. 102 | 103 | body sections HEADER, TEXT, MIME, HEADER.FIELDS, HEADER.FIELDS.NOT 104 | Use body section numbers only. 105 | 106 | BODY.PEEK[section] 107 | Use BODY[section] and manually clear the \Seen flag as 108 | necessary. 109 | 110 | 111 | 112 | 113 | 114 | Crispin Informational [Page 2] 115 | 116 | RFC 2061 IMAP4 Compatibility December 1996 117 | 118 | 119 | FLAGS.SILENT, +FLAGS.SILENT, and -FLAGS.SILENT store data items 120 | Use the corresponding non-SILENT versions and ignore the 121 | untagged FETCH responses which come back. 122 | 123 | UID fetch data item and the UID commands 124 | No functional equivalent. 125 | 126 | CLOSE command 127 | No functional equivalent. 128 | 129 | 130 | In IMAP2bis, the TRYCREATE special information token is sent as a 131 | separate unsolicited OK response instead of inside the NO response. 132 | 133 | IMAP2bis is ambiguous about whether or not flags or internal dates 134 | are preserved on COPY. It is impossible to know what behavior is 135 | supported by the server. 136 | 137 | IMAP4 server interoperability with IMAP2bis clients 138 | 139 | The only interoperability problem between an IMAP4 server and a 140 | well-written IMAP2bis client is an incompatibility with the use of 141 | "\" in quoted strings. This is best avoided by using literals 142 | instead of quoted strings if "\" or <"> is embedded in the string. 143 | 144 | Security Considerations 145 | 146 | Security issues are not discussed in this memo. 147 | 148 | Author's Address 149 | 150 | Mark R. Crispin 151 | Networks and Distributed Computing 152 | University of Washington 153 | 4545 15th Aveneue NE 154 | Seattle, WA 98105-4527 155 | 156 | Phone: (206) 543-5762 157 | EMail: MRC@CAC.Washington.EDU 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | Crispin Informational [Page 3] 171 | 172 | -------------------------------------------------------------------------------- /docs/rfcs/rfc2087.IMAP4_QUOTA_extension.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Network Working Group J. Myers 8 | Request for Comments: 2087 Carnegie Mellon 9 | Category: Standards Track January 1997 10 | 11 | 12 | IMAP4 QUOTA extension 13 | 14 | Status of this Memo 15 | 16 | This document specifies an Internet standards track protocol for the 17 | Internet community, and requests discussion and suggestions for 18 | improvements. Please refer to the current edition of the "Internet 19 | Official Protocol Standards" (STD 1) for the standardization state 20 | and status of this protocol. Distribution of this memo is unlimited. 21 | 22 | 1. Abstract 23 | 24 | The QUOTA extension of the Internet Message Access Protocol [IMAP4] 25 | permits administrative limits on resource usage (quotas) to be 26 | manipulated through the IMAP protocol. 27 | 28 | Table of Contents 29 | 30 | 1. Abstract........................................... 1 31 | 2. Conventions Used in this Document.................. 1 32 | 3. Introduction and Overview.......................... 2 33 | 4. Commands........................................... 2 34 | 4.1. SETQUOTA Command................................... 2 35 | 4.2. GETQUOTA Command................................... 2 36 | 4.3. GETQUOTAROOT Command............................... 3 37 | 5. Responses.......................................... 3 38 | 5.1. QUOTA Response..................................... 3 39 | 5.2. QUOTAROOT Response................................. 4 40 | 6. Formal syntax...................................... 4 41 | 7. References......................................... 5 42 | 8. Security Considerations............................ 5 43 | 9. Author's Address................................... 5 44 | 45 | 46 | 2. Conventions Used in this Document 47 | 48 | In examples, "C:" and "S:" indicate lines sent by the client and 49 | server respectively. 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | Myers Standards Track [Page 1] 59 | 60 | RFC 2087 QUOTA January 1997 61 | 62 | 63 | 3. Introduction and Overview 64 | 65 | The QUOTA extension is present in any IMAP4 implementation which 66 | returns "QUOTA" as one of the supported capabilities to the 67 | CAPABILITY command. 68 | 69 | An IMAP4 server which supports the QUOTA capability may support 70 | limits on any number of resources. Each resource has an atom name 71 | and an implementation-defined interpretation which evaluates to an 72 | integer. Examples of such resources are: 73 | 74 | Name Interpretation 75 | 76 | STORAGE Sum of messages' RFC822.SIZE, in units of 1024 octets 77 | MESSAGE Number of messages 78 | 79 | 80 | Each mailbox has zero or more implementation-defined named "quota 81 | roots". Each quota root has zero or more resource limits. All 82 | mailboxes that share the same named quota root share the resource 83 | limits of the quota root. 84 | 85 | Quota root names do not necessarily have to match the names of 86 | existing mailboxes. 87 | 88 | 4. Commands 89 | 90 | 4.1. SETQUOTA Command 91 | 92 | Arguments: quota root 93 | list of resource limits 94 | 95 | Data: untagged responses: QUOTA 96 | 97 | Result: OK - setquota completed 98 | NO - setquota error: can't set that data 99 | BAD - command unknown or arguments invalid 100 | 101 | The SETQUOTA command takes the name of a mailbox quota root and a 102 | list of resource limits. The resource limits for the named quota root 103 | are changed to be the specified limits. Any previous resource limits 104 | for the named quota root are discarded. 105 | 106 | If the named quota root did not previously exist, an implementation 107 | may optionally create it and change the quota roots for any number of 108 | existing mailboxes in an implementation-defined manner. 109 | 110 | 111 | 112 | 113 | 114 | Myers Standards Track [Page 2] 115 | 116 | RFC 2087 QUOTA January 1997 117 | 118 | 119 | Example: C: A001 SETQUOTA "" (STORAGE 512) 120 | S: * QUOTA "" (STORAGE 10 512) 121 | S: A001 OK Setquota completed 122 | 123 | 4.2. GETQUOTA Command 124 | 125 | Arguments: quota root 126 | 127 | Data: untagged responses: QUOTA 128 | 129 | Result: OK - getquota completed 130 | NO - getquota error: no such quota root, permission 131 | denied 132 | BAD - command unknown or arguments invalid 133 | 134 | The GETQUOTA command takes the name of a quota root and returns the 135 | quota root's resource usage and limits in an untagged QUOTA response. 136 | 137 | Example: C: A003 GETQUOTA "" 138 | S: * QUOTA "" (STORAGE 10 512) 139 | S: A003 OK Getquota completed 140 | 141 | 4.3. GETQUOTAROOT Command 142 | 143 | Arguments: mailbox name 144 | 145 | Data: untagged responses: QUOTAROOT, QUOTA 146 | 147 | Result: OK - getquota completed 148 | NO - getquota error: no such mailbox, permission denied 149 | BAD - command unknown or arguments invalid 150 | 151 | The GETQUOTAROOT command takes the name of a mailbox and returns the 152 | list of quota roots for the mailbox in an untagged QUOTAROOT 153 | response. For each listed quota root, it also returns the quota 154 | root's resource usage and limits in an untagged QUOTA response. 155 | 156 | Example: C: A003 GETQUOTAROOT INBOX 157 | S: * QUOTAROOT INBOX "" 158 | S: * QUOTA "" (STORAGE 10 512) 159 | S: A003 OK Getquota completed 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | Myers Standards Track [Page 3] 171 | 172 | RFC 2087 QUOTA January 1997 173 | 174 | 175 | 5. Responses 176 | 177 | 5.1. QUOTA Response 178 | 179 | Data: quota root name 180 | list of resource names, usages, and limits 181 | 182 | This response occurs as a result of a GETQUOTA or GETQUOTAROOT 183 | command. The first string is the name of the quota root for which 184 | this quota applies. 185 | 186 | The name is followed by a S-expression format list of the resource 187 | usage and limits of the quota root. The list contains zero or 188 | more triplets. Each triplet conatins a resource name, the current 189 | usage of the resource, and the resource limit. 190 | 191 | Resources not named in the list are not limited in the quota root. 192 | Thus, an empty list means there are no administrative resource 193 | limits in the quota root. 194 | 195 | Example: S: * QUOTA "" (STORAGE 10 512) 196 | 197 | 5.2. QUOTAROOT Response 198 | 199 | Data: mailbox name 200 | zero or more quota root names 201 | 202 | This response occurs as a result of a GETQUOTAROOT command. The 203 | first string is the mailbox and the remaining strings are the 204 | names of the quota roots for the mailbox. 205 | 206 | Example: S: * QUOTAROOT INBOX "" 207 | S: * QUOTAROOT comp.mail.mime 208 | 209 | 6. Formal syntax 210 | 211 | The following syntax specification uses the augmented Backus-Naur 212 | Form (BNF) notation as specified in RFC 822 with one exception; the 213 | delimiter used with the "#" construct is a single space (SP) and not 214 | one or more commas. 215 | 216 | Except as noted otherwise, all alphabetic characters are case- 217 | insensitive. The use of upper or lower case characters to define 218 | token strings is for editorial clarity only. Implementations MUST 219 | accept these strings in a case-insensitive fashion. 220 | 221 | 222 | 223 | 224 | 225 | 226 | Myers Standards Track [Page 4] 227 | 228 | RFC 2087 QUOTA January 1997 229 | 230 | 231 | getquota ::= "GETQUOTA" SP astring 232 | 233 | getquotaroot ::= "GETQUOTAROOT" SP astring 234 | 235 | quota_list ::= "(" #quota_resource ")" 236 | 237 | quota_resource ::= atom SP number SP number 238 | 239 | quota_response ::= "QUOTA" SP astring SP quota_list 240 | 241 | quotaroot_response 242 | ::= "QUOTAROOT" SP astring *(SP astring) 243 | 244 | setquota ::= "SETQUOTA" SP astring SP setquota_list 245 | 246 | setquota_list ::= "(" 0#setquota_resource ")" 247 | 248 | setquota_resource ::= atom SP number 249 | 250 | 7. References 251 | 252 | [IMAP4] Crispin, M., "Internet Message Access Protocol - Version 4", 253 | RFC 1730, University of Washington, December 1994. 254 | 255 | [RFC-822] Crocker, D., "Standard for the Format of ARPA Internet 256 | Text Messages", STD 11, RFC 822. 257 | 258 | 8. Security Considerations 259 | 260 | Implementors should be careful to make sure the implementation of 261 | these commands does not violate the site's security policy. The 262 | resource usage of other users is likely to be considered confidential 263 | information and should not be divulged to unauthorized persons. 264 | 265 | 9. Author's Address 266 | 267 | John G. Myers 268 | Carnegie-Mellon University 269 | 5000 Forbes Ave. 270 | Pittsburgh PA, 15213-3890 271 | 272 | EMail: jgm+@cmu.edu 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | Myers Standards Track [Page 5] 283 | 284 | -------------------------------------------------------------------------------- /docs/rfcs/rfc2088.IMAP4_non_synchronizing_literals.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Network Working Group J. Myers 8 | Request for Comments: 2088 Carnegie Mellon 9 | Cateogry: Standards Track January 1997 10 | 11 | 12 | IMAP4 non-synchronizing literals 13 | 14 | Status of this Memo 15 | 16 | This document specifies an Internet standards track protocol for the 17 | Internet community, and requests discussion and suggestions for 18 | improvements. Please refer to the current edition of the "Internet 19 | Official Protocol Standards" (STD 1) for the standardization state 20 | and status of this protocol. Distribution of this memo is unlimited. 21 | 22 | 1. Abstract 23 | 24 | The Internet Message Access Protocol [IMAP4] contains the "literal" 25 | syntactic construct for communicating strings. When sending a 26 | literal from client to server, IMAP4 requires the client to wait for 27 | the server to send a command continuation request between sending the 28 | octet count and the string data. This document specifies an 29 | alternate form of literal which does not require this network round 30 | trip. 31 | 32 | 2. Conventions Used in this Document 33 | 34 | In examples, "C:" and "S:" indicate lines sent by the client and 35 | server respectively. 36 | 37 | 3. Specification 38 | 39 | The non-synchronizing literal is added an alternate form of literal, 40 | and may appear in communication from client to server instead of the 41 | IMAP4 form of literal. The IMAP4 form of literal, used in 42 | communication from client to server, is referred to as a 43 | synchronizing literal. 44 | 45 | Non-synchronizing literals may be used with any IMAP4 server 46 | implementation which returns "LITERAL+" as one of the supported 47 | capabilities to the CAPABILITY command. If the server does not 48 | advertise the LITERAL+ capability, the client must use synchronizing 49 | literals instead. 50 | 51 | The non-synchronizing literal is distinguished from the original 52 | synchronizing literal by having a plus ('+') between the octet count 53 | and the closing brace ('}'). The server does not generate a command 54 | continuation request in response to a non-synchronizing literal, and 55 | 56 | 57 | 58 | Myers Standards Track [Page 1] 59 | 60 | RFC 2088 LITERAL January 1997 61 | 62 | 63 | clients are not required to wait before sending the octets of a non- 64 | synchronizing literal. 65 | 66 | The protocol receiver of an IMAP4 server must check the end of every 67 | received line for an open brace ('{') followed by an octet count, a 68 | plus ('+'), and a close brace ('}') immediately preceeding the CRLF. 69 | If it finds this sequence, it is the octet count of a non- 70 | synchronizing literal and the server MUST treat the specified number 71 | of following octets and the following line as part of the same 72 | command. A server MAY still process commands and reject errors on a 73 | line-by-line basis, as long as it checks for non-synchronizing 74 | literals at the end of each line. 75 | 76 | Example: C: A001 LOGIN {11+} 77 | C: FRED FOOBAR {7+} 78 | C: fat man 79 | S: A001 OK LOGIN completed 80 | 81 | 4. Formal Syntax 82 | 83 | The following syntax specification uses the augmented Backus-Naur 84 | Form (BNF) notation as specified in [RFC-822] as modified by [IMAP4]. 85 | Non-terminals referenced but not defined below are as defined by 86 | [IMAP4]. 87 | 88 | literal ::= "{" number ["+"] "}" CRLF *CHAR8 89 | ;; Number represents the number of CHAR8 octets 90 | 91 | 6. References 92 | 93 | [IMAP4] Crispin, M., "Internet Message Access Protocol - Version 4", 94 | draft-crispin-imap-base-XX.txt, University of Washington, April 1996. 95 | 96 | [RFC-822] Crocker, D., "Standard for the Format of ARPA Internet Text 97 | Messages", STD 11, RFC 822. 98 | 99 | 7. Security Considerations 100 | 101 | There are no known security issues with this extension. 102 | 103 | 8. Author's Address 104 | 105 | John G. Myers 106 | Carnegie-Mellon University 107 | 5000 Forbes Ave. 108 | Pittsburgh PA, 15213-3890 109 | 110 | Email: jgm+@cmu.edu 111 | 112 | 113 | 114 | Myers Standards Track [Page 2] 115 | 116 | -------------------------------------------------------------------------------- /docs/rfcs/rfc2177.IMAP4_IDLE_command.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Network Working Group B. Leiba 8 | Request for Comments: 2177 IBM T.J. Watson Research Center 9 | Category: Standards Track June 1997 10 | 11 | 12 | IMAP4 IDLE command 13 | 14 | Status of this Memo 15 | 16 | This document specifies an Internet standards track protocol for the 17 | Internet community, and requests discussion and suggestions for 18 | improvements. Please refer to the current edition of the "Internet 19 | Official Protocol Standards" (STD 1) for the standardization state 20 | and status of this protocol. Distribution of this memo is unlimited. 21 | 22 | 1. Abstract 23 | 24 | The Internet Message Access Protocol [IMAP4] requires a client to 25 | poll the server for changes to the selected mailbox (new mail, 26 | deletions). It's often more desirable to have the server transmit 27 | updates to the client in real time. This allows a user to see new 28 | mail immediately. It also helps some real-time applications based on 29 | IMAP, which might otherwise need to poll extremely often (such as 30 | every few seconds). (While the spec actually does allow a server to 31 | push EXISTS responses aysynchronously, a client can't expect this 32 | behaviour and must poll.) 33 | 34 | This document specifies the syntax of an IDLE command, which will 35 | allow a client to tell the server that it's ready to accept such 36 | real-time updates. 37 | 38 | 2. Conventions Used in this Document 39 | 40 | In examples, "C:" and "S:" indicate lines sent by the client and 41 | server respectively. 42 | 43 | The key words "MUST", "MUST NOT", "SHOULD", "SHOULD NOT", and "MAY" 44 | in this document are to be interpreted as described in RFC 2060 45 | [IMAP4]. 46 | 47 | 3. Specification 48 | 49 | IDLE Command 50 | 51 | Arguments: none 52 | 53 | Responses: continuation data will be requested; the client sends 54 | the continuation data "DONE" to end the command 55 | 56 | 57 | 58 | Leiba Standards Track [Page 1] 59 | 60 | RFC 2177 IMAP4 IDLE command June 1997 61 | 62 | 63 | 64 | Result: OK - IDLE completed after client sent "DONE" 65 | NO - failure: the server will not allow the IDLE 66 | command at this time 67 | BAD - command unknown or arguments invalid 68 | 69 | The IDLE command may be used with any IMAP4 server implementation 70 | that returns "IDLE" as one of the supported capabilities to the 71 | CAPABILITY command. If the server does not advertise the IDLE 72 | capability, the client MUST NOT use the IDLE command and must poll 73 | for mailbox updates. In particular, the client MUST continue to be 74 | able to accept unsolicited untagged responses to ANY command, as 75 | specified in the base IMAP specification. 76 | 77 | The IDLE command is sent from the client to the server when the 78 | client is ready to accept unsolicited mailbox update messages. The 79 | server requests a response to the IDLE command using the continuation 80 | ("+") response. The IDLE command remains active until the client 81 | responds to the continuation, and as long as an IDLE command is 82 | active, the server is now free to send untagged EXISTS, EXPUNGE, and 83 | other messages at any time. 84 | 85 | The IDLE command is terminated by the receipt of a "DONE" 86 | continuation from the client; such response satisfies the server's 87 | continuation request. At that point, the server MAY send any 88 | remaining queued untagged responses and then MUST immediately send 89 | the tagged response to the IDLE command and prepare to process other 90 | commands. As in the base specification, the processing of any new 91 | command may cause the sending of unsolicited untagged responses, 92 | subject to the ambiguity limitations. The client MUST NOT send a 93 | command while the server is waiting for the DONE, since the server 94 | will not be able to distinguish a command from a continuation. 95 | 96 | The server MAY consider a client inactive if it has an IDLE command 97 | running, and if such a server has an inactivity timeout it MAY log 98 | the client off implicitly at the end of its timeout period. Because 99 | of that, clients using IDLE are advised to terminate the IDLE and 100 | re-issue it at least every 29 minutes to avoid being logged off. 101 | This still allows a client to receive immediate mailbox updates even 102 | though it need only "poll" at half hour intervals. 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | Leiba Standards Track [Page 2] 115 | 116 | RFC 2177 IMAP4 IDLE command June 1997 117 | 118 | 119 | Example: C: A001 SELECT INBOX 120 | S: * FLAGS (Deleted Seen) 121 | S: * 3 EXISTS 122 | S: * 0 RECENT 123 | S: * OK [UIDVALIDITY 1] 124 | S: A001 OK SELECT completed 125 | C: A002 IDLE 126 | S: + idling 127 | ...time passes; new mail arrives... 128 | S: * 4 EXISTS 129 | C: DONE 130 | S: A002 OK IDLE terminated 131 | ...another client expunges message 2 now... 132 | C: A003 FETCH 4 ALL 133 | S: * 4 FETCH (...) 134 | S: A003 OK FETCH completed 135 | C: A004 IDLE 136 | S: * 2 EXPUNGE 137 | S: * 3 EXISTS 138 | S: + idling 139 | ...time passes; another client expunges message 3... 140 | S: * 3 EXPUNGE 141 | S: * 2 EXISTS 142 | ...time passes; new mail arrives... 143 | S: * 3 EXISTS 144 | C: DONE 145 | S: A004 OK IDLE terminated 146 | C: A005 FETCH 3 ALL 147 | S: * 3 FETCH (...) 148 | S: A005 OK FETCH completed 149 | C: A006 IDLE 150 | 151 | 4. Formal Syntax 152 | 153 | The following syntax specification uses the augmented Backus-Naur 154 | Form (BNF) notation as specified in [RFC-822] as modified by [IMAP4]. 155 | Non-terminals referenced but not defined below are as defined by 156 | [IMAP4]. 157 | 158 | command_auth ::= append / create / delete / examine / list / lsub / 159 | rename / select / status / subscribe / unsubscribe 160 | / idle 161 | ;; Valid only in Authenticated or Selected state 162 | 163 | idle ::= "IDLE" CRLF "DONE" 164 | 165 | 166 | 167 | 168 | 169 | 170 | Leiba Standards Track [Page 3] 171 | 172 | RFC 2177 IMAP4 IDLE command June 1997 173 | 174 | 175 | 5. References 176 | 177 | [IMAP4] Crispin, M., "Internet Message Access Protocol - Version 178 | 4rev1", RFC 2060, December 1996. 179 | 180 | 6. Security Considerations 181 | 182 | There are no known security issues with this extension. 183 | 184 | 7. Author's Address 185 | 186 | Barry Leiba 187 | IBM T.J. Watson Research Center 188 | 30 Saw Mill River Road 189 | Hawthorne, NY 10532 190 | 191 | Email: leiba@watson.ibm.com 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | Leiba Standards Track [Page 4] 227 | 228 | -------------------------------------------------------------------------------- /docs/rfcs/rfc3691.IMAP_UNSELECT_command.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Network Working Group A. Melnikov 8 | Request for Comments: 3691 Isode Ltd. 9 | Category: Standards Track February 2004 10 | 11 | 12 | Internet Message Access Protocol (IMAP) UNSELECT command 13 | 14 | Status of this Memo 15 | 16 | This document specifies an Internet standards track protocol for the 17 | Internet community, and requests discussion and suggestions for 18 | improvements. Please refer to the current edition of the "Internet 19 | Official Protocol Standards" (STD 1) for the standardization state 20 | and status of this protocol. Distribution of this memo is unlimited. 21 | 22 | Copyright Notice 23 | 24 | Copyright (C) The Internet Society (2004). All Rights Reserved. 25 | 26 | Abstract 27 | 28 | This document defines an UNSELECT command that can be used to close 29 | the current mailbox in an Internet Message Access Protocol - version 30 | 4 (IMAP4) session without expunging it. Certain types of IMAP 31 | clients need to release resources associated with the selected 32 | mailbox without selecting a different mailbox. While IMAP4 provides 33 | this functionality (via a SELECT command with a nonexistent mailbox 34 | name or reselecting the same mailbox with EXAMINE command), a more 35 | clean solution is desirable. 36 | 37 | Table of Contents 38 | 39 | 1. Introduction . . . . . . . . . . . . . . . . . . . . . . . . . 2 40 | 2. UNSELECT command . . . . . . . . . . . . . . . . . . . . . . . 2 41 | 3. Security Considerations. . . . . . . . . . . . . . . . . . . . 3 42 | 4. Formal Syntax. . . . . . . . . . . . . . . . . . . . . . . . . 3 43 | 5. IANA Considerations. . . . . . . . . . . . . . . . . . . . . . 3 44 | 6. Acknowledgments. . . . . . . . . . . . . . . . . . . . . . . . 3 45 | 7. Normative References . . . . . . . . . . . . . . . . . . . . . 4 46 | 8. Author's Address . . . . . . . . . . . . . . . . . . . . . . . 4 47 | 9. Full Copyright Statement . . . . . . . . . . . . . . . . . . . 5 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | Melnikov Standards Track [Page 1] 59 | 60 | RFC 3691 IMAP UNSELECT command February 2004 61 | 62 | 63 | 1. Introduction 64 | 65 | Certain types of IMAP clients need to release resources associated 66 | with the selected mailbox without selecting a different mailbox. 67 | While [IMAP4] provides this functionality (via a SELECT command with 68 | a nonexistent mailbox name or reselecting the same mailbox with 69 | EXAMINE command), a more clean solution is desirable. 70 | 71 | [IMAP4] defines the CLOSE command that closes the selected mailbox as 72 | well as permanently removes all messages with the \Deleted flag set. 73 | 74 | However [IMAP4] lacks a command that simply closes the mailbox 75 | without expunging it. This document defines the UNSELECT command for 76 | this purpose. 77 | 78 | A server which supports this extension indicates this with a 79 | capability name of "UNSELECT". 80 | 81 | "C:" and "S:" in examples show lines sent by the client and server 82 | respectively. 83 | 84 | The keywords "MUST", "MUST NOT", "SHOULD", "SHOULD NOT", and "MAY" in 85 | this document when typed in uppercase are to be interpreted as 86 | defined in "Key words for use in RFCs to Indicate Requirement Levels" 87 | [KEYWORDS]. 88 | 89 | 2. UNSELECT Command 90 | 91 | Arguments: none 92 | 93 | Responses: no specific responses for this command 94 | 95 | Result: OK - unselect completed, now in authenticated state 96 | BAD - no mailbox selected, or argument supplied but 97 | none permitted 98 | 99 | The UNSELECT command frees server's resources associated with the 100 | selected mailbox and returns the server to the authenticated 101 | state. This command performs the same actions as CLOSE, except 102 | that no messages are permanently removed from the currently 103 | selected mailbox. 104 | 105 | Example: C: A341 UNSELECT 106 | S: A341 OK Unselect completed 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | Melnikov Standards Track [Page 2] 115 | 116 | RFC 3691 IMAP UNSELECT command February 2004 117 | 118 | 119 | 3. Security Considerations 120 | 121 | It is believed that this extension doesn't raise any additional 122 | security concerns not already discussed in [IMAP4]. 123 | 124 | 4. Formal Syntax 125 | 126 | The following syntax specification uses the Augmented Backus-Naur 127 | Form (ABNF) notation as specified in [ABNF]. Non-terminals 128 | referenced but not defined below are as defined by [IMAP4]. 129 | 130 | Except as noted otherwise, all alphabetic characters are case- 131 | insensitive. The use of upper or lower case characters to define 132 | token strings is for editorial clarity only. Implementations MUST 133 | accept these strings in a case-insensitive fashion. 134 | 135 | command-select /= "UNSELECT" 136 | 137 | 5. IANA Considerations 138 | 139 | IMAP4 capabilities are registered by publishing a standards track or 140 | IESG approved experimental RFC. The registry is currently located 141 | at: 142 | 143 | http://www.iana.org/assignments/imap4-capabilities 144 | 145 | This document defines the UNSELECT IMAP capabilities. IANA has added 146 | this capability to the registry. 147 | 148 | 6. Acknowledgments 149 | 150 | UNSELECT command was originally implemented by Tim Showalter in Cyrus 151 | IMAP server. 152 | 153 | Also, the author of the document would like to thank Vladimir Butenko 154 | and Mark Crispin for reminding that UNSELECT has to be documented. 155 | Also thanks to Simon Josefsson for pointing out that there are 156 | multiple ways to implement UNSELECT. 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | Melnikov Standards Track [Page 3] 171 | 172 | RFC 3691 IMAP UNSELECT command February 2004 173 | 174 | 175 | 7. Normative References 176 | 177 | [KEYWORDS] Bradner, S., "Key words for use in RFCs to Indicate 178 | Requirement Levels", BCP 14, RFC 2119, March 1997. 179 | 180 | [IMAP4] Crispin, M., "Internet Message Access Protocol - Version 181 | 4rev1", RFC 3501, March 2003. 182 | 183 | [ABNF] Crocker, D., Ed. and P. Overell, "Augmented BNF for Syntax 184 | Specifications: ABNF", RFC 2234, November 1997. 185 | 186 | 8. Author's Address 187 | 188 | Alexey Melnikov 189 | Isode Limited 190 | 5 Castle Business Village 191 | Hampton, Middlesex TW12 2BX 192 | 193 | EMail: Alexey.Melnikov@isode.com 194 | URI: http://www.melnikov.ca/ 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | Melnikov Standards Track [Page 4] 227 | 228 | RFC 3691 IMAP UNSELECT command February 2004 229 | 230 | 231 | 9. Full Copyright Statement 232 | 233 | Copyright (C) The Internet Society (2004). This document is subject 234 | to the rights, licenses and restrictions contained in BCP 78 and 235 | except as set forth therein, the authors retain all their rights. 236 | 237 | This document and the information contained herein are provided on an 238 | "AS IS" basis and THE CONTRIBUTOR, THE ORGANIZATION HE/SHE 239 | REPRESENTS OR IS SPONSORED BY (IF ANY), THE INTERNET SOCIETY AND THE 240 | INTERNET ENGINEERING TASK FORCE DISCLAIM ALL WARRANTIES, EXPRESS OR 241 | IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTY THAT THE USE OF 242 | THE INFORMATION HEREIN WILL NOT INFRINGE ANY RIGHTS OR ANY IMPLIED 243 | WARRANTIES OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. 244 | 245 | Intellectual Property 246 | 247 | The IETF takes no position regarding the validity or scope of any 248 | Intellectual Property Rights or other rights that might be claimed 249 | to pertain to the implementation or use of the technology 250 | described in this document or the extent to which any license 251 | under such rights might or might not be available; nor does it 252 | represent that it has made any independent effort to identify any 253 | such rights. Information on the procedures with respect to 254 | rights in RFC documents can be found in BCP 78 and BCP 79. 255 | 256 | Copies of IPR disclosures made to the IETF Secretariat and any 257 | assurances of licenses to be made available, or the result of an 258 | attempt made to obtain a general license or permission for the use 259 | of such proprietary rights by implementers or users of this 260 | specification can be obtained from the IETF on-line IPR repository 261 | at http://www.ietf.org/ipr. 262 | 263 | The IETF invites any interested party to bring to its attention 264 | any copyrights, patents or patent applications, or other 265 | proprietary rights that may cover technology that may be required 266 | to implement this standard. Please address the information to the 267 | IETF at ietf-ipr@ietf.org. 268 | 269 | Acknowledgement 270 | 271 | Funding for the RFC Editor function is currently provided by the 272 | Internet Society. 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | Melnikov Standards Track [Page 5] 283 | 284 | -------------------------------------------------------------------------------- /docs/website-doc.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # vim: expandtab ts=2 : 4 | 5 | ARGS=$* 6 | 7 | SPHINXBUILD=sphinx-build 8 | TMPDIR='/tmp/offlineimap-sphinx-doctrees' 9 | WEBSITE='./website' 10 | DOCBASE="${WEBSITE}/_doc" 11 | DESTBASE="${DOCBASE}/versions" 12 | VERSIONS_YML="${WEBSITE}/_data/versions.yml" 13 | ANNOUNCES_YML="${WEBSITE}/_data/announces.yml" 14 | ANNOUNCES_YML_LIMIT=31 15 | ANNOUNCES_YML_TMP="${ANNOUNCES_YML}.tmp" 16 | CONTRIB_YML="${WEBSITE}/_data/contribs.yml" 17 | CONTRIB="${DOCBASE}/contrib" 18 | HEADER="# DO NOT EDIT MANUALLY: it is generated by a script (website-doc.sh)." 19 | 20 | 21 | function fix_pwd () { 22 | cd "$(git rev-parse --show-toplevel)" || \ 23 | exit 2 "cannot determine the root of the repository" 24 | test -d "$DESTBASE" || exit 1 25 | } 26 | 27 | fix_pwd 28 | version="v$(./offlineimap.py --version)" 29 | 30 | 31 | 32 | # 33 | # Add the doc for the contrib files. 34 | # 35 | function contrib () { 36 | echo $HEADER > "$CONTRIB_YML" 37 | # systemd 38 | cp -afv "./contrib/systemd/README.md" "${CONTRIB}/systemd.md" 39 | echo "- {filename: 'systemd', linkname: 'Integrate with systemd'}" >> "$CONTRIB_YML" 40 | } 41 | 42 | 43 | 44 | # 45 | # Build the sphinx documentation. 46 | # 47 | function api () { 48 | # Build the doc with sphinx. 49 | dest="${DESTBASE}/${version}" 50 | echo "Cleaning target directory: $dest" 51 | rm -rf "$dest" 52 | $SPHINXBUILD -b html -d "$TMPDIR" ./docs/doc-src "$dest" 53 | 54 | # Build the JSON definitions for Jekyll. 55 | # This let know the website about the available APIs documentations. 56 | echo "Building Jekyll data: $VERSIONS_YML" 57 | # Erase previous content. 58 | echo > "$VERSIONS_YML" <> "$VERSIONS_YML" 69 | } 70 | 71 | 72 | 73 | # 74 | # Return title from release entry. 75 | # $1: full release title 76 | # 77 | function parse_releases_get_link () { 78 | echo $1 | sed -r -e 's,^### (OfflineIMAP.*)\),\1,' \ 79 | | tr '[:upper:]' '[:lower:]' \ 80 | | sed -r -e 's,[\.("],,g' \ 81 | | sed -r -e 's, ,-,g' 82 | } 83 | 84 | # 85 | # Return version from release entry. 86 | # $1: full release title 87 | # 88 | function parse_releases_get_version () { 89 | echo $1 | sed -r -e 's,^### [a-Z]+ (v[^ ]+).*,\1,' 90 | } 91 | 92 | # 93 | # Return date from release entry. 94 | # $1: full release title 95 | # 96 | function parse_releases_get_date () { 97 | echo $1 | sed -r -e 's,.*\(([0-9]+-[0-9]+-[0-9]+).*,\1,' 98 | } 99 | 100 | # 101 | # Make Changelog public and save links to them as JSON. 102 | # 103 | function releases () { 104 | # Copy the Changelogs. 105 | for foo in ./Changelog.md ./Changelog.maint.md 106 | do 107 | cp -afv "$foo" "$DOCBASE" 108 | done 109 | 110 | # Build the announces JSON list. Format is JSON: 111 | # - {version: '', link: ''} 112 | # - ... 113 | echo "$HEADER" > "$ANNOUNCES_YML" 114 | # Announces for the mainline. 115 | grep -E '^### OfflineIMAP' ./Changelog.md | while read title 116 | do 117 | link="$(parse_releases_get_link "$title")" 118 | v="$(parse_releases_get_version "$title")" 119 | d="$(parse_releases_get_date "$title")" 120 | echo "- {date: '${d}', version: '${v}', link: 'Changelog.html#${link}'}" 121 | done | tee -a "$ANNOUNCES_YML_TMP" 122 | # Announces for the maintenance releases. 123 | grep -E '^### OfflineIMAP' ./Changelog.maint.md | while read title 124 | do 125 | link="$(parse_releases_get_link "$title")" 126 | v="$(parse_releases_get_version "$title")" 127 | d="$(parse_releases_get_date "$title")" 128 | echo "- {date: '${d}', version: '${v}', link: 'Changelog.maint.html#${link}'}" 129 | done | tee -a "$ANNOUNCES_YML_TMP" 130 | sort -nr "$ANNOUNCES_YML_TMP" | head -n $ANNOUNCES_YML_LIMIT >> "$ANNOUNCES_YML" 131 | rm -f "$ANNOUNCES_YML_TMP" 132 | } 133 | 134 | function manhtml () { 135 | set -e 136 | 137 | cd ./docs 138 | make manhtml 139 | cd .. 140 | cp -afv ./docs/manhtml/* "$DOCBASE" 141 | } 142 | 143 | 144 | exit_code=0 145 | test "n$ARGS" = 'n' && ARGS='usage' # no option passed 146 | for arg in $ARGS 147 | do 148 | # PWD was fixed at the very beginning. 149 | case "n$arg" in 150 | "nreleases") 151 | releases 152 | ;; 153 | "napi") 154 | api 155 | ;; 156 | "nhtml") 157 | manhtml 158 | ;; 159 | "ncontrib") 160 | contrib 161 | ;; 162 | "nusage") 163 | echo "Usage: website-doc.sh " 164 | ;; 165 | *) 166 | echo "unkown option $arg" 167 | exit_code=$(( $exit_code + 1 )) 168 | ;; 169 | esac 170 | done 171 | 172 | exit $exit_code 173 | -------------------------------------------------------------------------------- /offlineimap.conf.minimal: -------------------------------------------------------------------------------- 1 | # Sample minimal config file. Copy this to ~/.offlineimaprc and edit to 2 | # get started fast. 3 | 4 | [general] 5 | accounts = Test 6 | 7 | [Account Test] 8 | localrepository = Local 9 | remoterepository = Remote 10 | 11 | [Repository Local] 12 | type = Maildir 13 | localfolders = ~/Test 14 | 15 | [Repository Remote] 16 | type = IMAP 17 | remotehost = examplehost 18 | remoteuser = jgoerzen 19 | -------------------------------------------------------------------------------- /offlineimap.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | # Startup from single-user installation 3 | # Copyright (C) 2002-2018 John Goerzen & contributors 4 | # 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program; if not, write to the Free Software 17 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 18 | 19 | import os 20 | import sys 21 | 22 | from offlineimap import OfflineImap 23 | 24 | oi = OfflineImap() 25 | oi.run() 26 | -------------------------------------------------------------------------------- /offlineimap/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ['OfflineImap'] 2 | 3 | __productname__ = 'OfflineIMAP' 4 | # Expecting trailing "-rcN" or "" for stable releases. 5 | __version__ = "7.3.0" 6 | __copyright__ = "Copyright 2002-2019 John Goerzen & contributors" 7 | __author__ = "John Goerzen" 8 | __author_email__= "offlineimap-project@lists.alioth.debian.org" 9 | __description__ = "Disconnected Universal IMAP Mail Synchronization/Reader Support" 10 | __license__ = "Licensed under the GNU GPL v2 or any later version (with an OpenSSL exception)" 11 | __bigcopyright__ = """%(__productname__)s %(__version__)s 12 | %(__license__)s""" % locals() 13 | __homepage__ = "http://www.offlineimap.org" 14 | 15 | banner = __bigcopyright__ 16 | 17 | from offlineimap.error import OfflineImapError 18 | # put this last, so we don't run into circular dependencies using 19 | # e.g. offlineimap.__version__. 20 | from offlineimap.init import OfflineImap 21 | -------------------------------------------------------------------------------- /offlineimap/emailutil.py: -------------------------------------------------------------------------------- 1 | # Some useful functions to extract data out of emails 2 | # Copyright (C) 2002-2015 John Goerzen & contributors 3 | # 4 | # This program is free software; you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 2 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 17 | 18 | import email 19 | from email.parser import Parser as MailParser 20 | 21 | def get_message_date(content, header='Date'): 22 | """Parses mail and returns resulting timestamp. 23 | 24 | :param header: the header to extract date from; 25 | :returns: timestamp or `None` in the case of failure. 26 | """ 27 | 28 | message = MailParser().parsestr(content, True) 29 | dateheader = message.get(header) 30 | # parsedate_tz returns a 10-tuple that can be passed to mktime_tz 31 | # Will be None if missing or not in a valid format. Note that 32 | # indexes 6, 7, and 8 of the result tuple are not usable. 33 | datetuple = email.utils.parsedate_tz(dateheader) 34 | if datetuple is None: 35 | return None 36 | return email.utils.mktime_tz(datetuple) 37 | -------------------------------------------------------------------------------- /offlineimap/error.py: -------------------------------------------------------------------------------- 1 | class OfflineImapError(Exception): 2 | """An Error during offlineimap synchronization""" 3 | 4 | class ERROR: 5 | """Severity level of an Exception 6 | 7 | * **MESSAGE**: Abort the current message, but continue with folder 8 | * **FOLDER_RETRY**: Error syncing folder, but do retry 9 | * **FOLDER**: Abort folder sync, but continue with next folder 10 | * **REPO**: Abort repository sync, continue with next account 11 | * **CRITICAL**: Immediately exit offlineimap 12 | """ 13 | 14 | MESSAGE, FOLDER_RETRY, FOLDER, REPO, CRITICAL = 0, 10, 15, 20, 30 15 | 16 | def __init__(self, reason, severity, errcode=None): 17 | """ 18 | :param reason: Human readable string suitable for logging 19 | 20 | :param severity: denoting which operations should be 21 | aborted. E.g. a ERROR.MESSAGE can occur on a faulty 22 | message, but a ERROR.REPO occurs when the server is 23 | offline. 24 | 25 | :param errcode: optional number denoting a predefined error 26 | situation (which let's us exit with a predefined exit 27 | value). So far, no errcodes have been defined yet. 28 | 29 | :type severity: OfflineImapError.ERROR value""" 30 | 31 | self.errcode = errcode 32 | self.severity = severity 33 | 34 | # 'reason' is stored in the Exception().args tuple. 35 | super(OfflineImapError, self).__init__(reason) 36 | 37 | @property 38 | def reason(self): 39 | return self.args[0] 40 | -------------------------------------------------------------------------------- /offlineimap/folder/__init__.py: -------------------------------------------------------------------------------- 1 | from . import Base, Gmail, IMAP, Maildir, LocalStatus, UIDMaps 2 | 3 | -------------------------------------------------------------------------------- /offlineimap/globals.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013-2016 Eygene A. Ryabinkin & contributors. 2 | # 3 | # Module that holds various global objects. 4 | 5 | from offlineimap.utils import const 6 | 7 | # Holds command-line options for OfflineIMAP. 8 | options = const.ConstProxy() 9 | 10 | def set_options(source): 11 | """Sets the source for options variable.""" 12 | 13 | options.set_source(source) 14 | -------------------------------------------------------------------------------- /offlineimap/localeval.py: -------------------------------------------------------------------------------- 1 | """Eval python code with global namespace of a python source file.""" 2 | 3 | # Copyright (C) 2002-2016 John Goerzen & contributors 4 | # 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program; if not, write to the Free Software 17 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 18 | 19 | import imp 20 | try: 21 | import errno 22 | except: 23 | pass 24 | 25 | class LocalEval(object): 26 | """Here is a powerfull but very dangerous option, of course.""" 27 | 28 | def __init__(self, path=None): 29 | self.namespace = {} 30 | 31 | if path is not None: 32 | # FIXME: limit opening files owned by current user with rights set 33 | # to fixed mode 644. 34 | foo = open(path, 'r') 35 | module = imp.load_module( 36 | '', 37 | foo, 38 | path, 39 | ('', 'r', imp.PY_SOURCE)) 40 | for attr in dir(module): 41 | self.namespace[attr] = getattr(module, attr) 42 | 43 | def eval(self, text, namespace=None): 44 | names = {} 45 | names.update(self.namespace) 46 | if namespace is not None: 47 | names.update(namespace) 48 | return eval(text, names) 49 | -------------------------------------------------------------------------------- /offlineimap/repository/Gmail.py: -------------------------------------------------------------------------------- 1 | # Gmail IMAP repository support 2 | # Copyright (C) 2008-2016 Riccardo Murri & 3 | # contributors 4 | # 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program; if not, write to the Free Software 17 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 18 | 19 | from offlineimap.repository.IMAP import IMAPRepository 20 | from offlineimap import folder, OfflineImapError 21 | 22 | 23 | class GmailRepository(IMAPRepository): 24 | """Gmail IMAP repository. 25 | 26 | This class just has default settings for GMail's IMAP service. So 27 | you can do 'type = Gmail' instead of 'type = IMAP' and skip 28 | specifying the hostname, port etc. See 29 | http://mail.google.com/support/bin/answer.py?answer=78799&topic=12814 30 | for the values we use.""" 31 | def __init__(self, reposname, account): 32 | """Initialize a GmailRepository object.""" 33 | IMAPRepository.__init__(self, reposname, account) 34 | 35 | def gethost(self): 36 | """Return the server name to connect to. 37 | 38 | We first check the usual IMAP settings, and then fall back to 39 | imap.gmail.com if nothing is specified.""" 40 | try: 41 | return super(GmailRepository, self).gethost() 42 | except OfflineImapError: 43 | # Nothing was configured, cache and return hardcoded 44 | # one. See the parent class (IMAPRepository) for how this 45 | # cache is used. 46 | self._host = "imap.gmail.com" 47 | return self._host 48 | 49 | def getoauth2_request_url(self): 50 | """Return the OAuth URL to request tokens from. 51 | 52 | We first check the usual OAuth settings, and then fall back to 53 | https://accounts.google.com/o/oauth2/token if nothing is 54 | specified.""" 55 | 56 | url = super(GmailRepository, self).getoauth2_request_url() 57 | if url is None: 58 | # Nothing was configured, cache and return hardcoded one. 59 | self.setoauth2_request_url("https://accounts.google.com/o/oauth2/token") 60 | else: 61 | self.setoauth2_request_url(url) 62 | return self.oauth2_request_url 63 | 64 | def getport(self): 65 | """Return the port number to connect to. 66 | 67 | This Gmail implementation first checks for the usual IMAP settings 68 | and falls back to 993 if nothing is specified.""" 69 | 70 | port = super(GmailRepository, self).getport() 71 | 72 | if port is None: 73 | return 993 74 | else: 75 | return port 76 | 77 | def getssl(self): 78 | ssl = self.getconfboolean('ssl', None) 79 | 80 | if ssl is None: 81 | # Nothing was configured, return our default setting for 82 | # GMail. Maybe this should look more similar to gethost & 83 | # we could just rely on the global "ssl = yes" default. 84 | return True 85 | else: 86 | return ssl 87 | 88 | def getpreauthtunnel(self): 89 | return None 90 | 91 | def getfolder(self, foldername, decode=True): 92 | return self.getfoldertype()(self.imapserver, foldername, 93 | self, decode) 94 | 95 | def getfoldertype(self): 96 | return folder.Gmail.GmailFolder 97 | 98 | def gettrashfolder(self, foldername): 99 | # Where deleted mail should be moved 100 | return self.getconf('trashfolder', '[Gmail]/Trash') 101 | 102 | def getspamfolder(self): 103 | # Depending on the IMAP settings (Settings -> Forwarding and 104 | # POP/IMAP -> IMAP Access -> "When I mark a message in IMAP as 105 | # deleted") GMail might also deletes messages upon EXPUNGE in 106 | # the Spam folder. 107 | return self.getconf('spamfolder', '[Gmail]/Spam') 108 | -------------------------------------------------------------------------------- /offlineimap/repository/GmailMaildir.py: -------------------------------------------------------------------------------- 1 | # Maildir repository support 2 | # Copyright (C) 2002-2015 John Goerzen & contributors 3 | # 4 | # 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program; if not, write to the Free Software 17 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 18 | 19 | from offlineimap.repository.Maildir import MaildirRepository 20 | from offlineimap.folder.GmailMaildir import GmailMaildirFolder 21 | 22 | class GmailMaildirRepository(MaildirRepository): 23 | def __init__(self, reposname, account): 24 | """Initialize a MaildirRepository object. Takes a path name 25 | to the directory holding all the Maildir directories.""" 26 | 27 | super(GmailMaildirRepository, self).__init__(reposname, account) 28 | 29 | def getfoldertype(self): 30 | return GmailMaildirFolder 31 | -------------------------------------------------------------------------------- /offlineimap/repository/LocalStatus.py: -------------------------------------------------------------------------------- 1 | # Local status cache repository support 2 | # Copyright (C) 2002-2017 John Goerzen & contributors 3 | # 4 | # This program is free software; you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 2 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 17 | 18 | import os 19 | 20 | from offlineimap.folder.LocalStatus import LocalStatusFolder 21 | from offlineimap.folder.LocalStatusSQLite import LocalStatusSQLiteFolder 22 | from offlineimap.repository.Base import BaseRepository 23 | from offlineimap.error import OfflineImapError 24 | 25 | 26 | class LocalStatusRepository(BaseRepository): 27 | def __init__(self, reposname, account): 28 | BaseRepository.__init__(self, reposname, account) 29 | 30 | # class and root for all backends. 31 | self.backends = {} 32 | self.backends['sqlite'] = { 33 | 'class': LocalStatusSQLiteFolder, 34 | 'root': os.path.join(account.getaccountmeta(), 'LocalStatus-sqlite') 35 | } 36 | self.backends['plain'] = { 37 | 'class': LocalStatusFolder, 38 | 'root': os.path.join(account.getaccountmeta(), 'LocalStatus') 39 | } 40 | 41 | if self.account.getconf('status_backend', None) is not None: 42 | raise OfflineImapError( 43 | "the 'status_backend' configuration option is not supported" 44 | " anymore; please, remove this configuration option.", 45 | OfflineImapError.ERROR.REPO 46 | ) 47 | # Set class and root for sqlite. 48 | self.setup_backend('sqlite') 49 | 50 | if not os.path.exists(self.root): 51 | os.mkdir(self.root, 0o700) 52 | 53 | # self._folders is a dict of name:LocalStatusFolders(). 54 | self._folders = {} 55 | 56 | def _instanciatefolder(self, foldername): 57 | return self.LocalStatusFolderClass(foldername, self) # Instanciate. 58 | 59 | def setup_backend(self, backend): 60 | if backend in self.backends.keys(): 61 | self._backend = backend 62 | self.root = self.backends[backend]['root'] 63 | self.LocalStatusFolderClass = self.backends[backend]['class'] 64 | 65 | def import_other_backend(self, folder): 66 | for bk, dic in self.backends.items(): 67 | # Skip folder's own type. 68 | if dic['class'] == type(folder): 69 | continue 70 | 71 | repobk = LocalStatusRepository(self.name, self.account) 72 | repobk.setup_backend(bk) # Fake the backend. 73 | folderbk = dic['class'](folder.name, repobk) 74 | 75 | # If backend contains data, import it to folder. 76 | if not folderbk.isnewfolder(): 77 | self.ui._msg("Migrating LocalStatus cache from %s to %s " 78 | "status folder for %s:%s"% 79 | (bk, self._backend, self.name, folder.name)) 80 | 81 | folderbk.cachemessagelist() 82 | folder.messagelist = folderbk.messagelist 83 | folder.saveall() 84 | break 85 | 86 | def getsep(self): 87 | return '.' 88 | 89 | def makefolder(self, foldername): 90 | """Create a LocalStatus Folder.""" 91 | 92 | if self.account.dryrun: 93 | return # Bail out in dry-run mode. 94 | 95 | # Create an empty StatusFolder. 96 | folder = self._instanciatefolder(foldername) 97 | # First delete any existing data to make sure we won't consider obsolete 98 | # data. This might happen if the user removed the folder (maildir) and 99 | # it is re-created afterwards. 100 | folder.purge() 101 | folder.openfiles() 102 | folder.save() 103 | folder.closefiles() 104 | 105 | # Invalidate the cache. 106 | self.forgetfolders() 107 | 108 | def getfolder(self, foldername): 109 | """Return the Folder() object for a foldername. 110 | 111 | Caller must call closefiles() on the folder when done.""" 112 | 113 | if foldername in self._folders: 114 | return self._folders[foldername] 115 | 116 | folder = self._instanciatefolder(foldername) 117 | 118 | # If folder is empty, try to import data from an other backend. 119 | if folder.isnewfolder(): 120 | self.import_other_backend(folder) 121 | 122 | self._folders[foldername] = folder 123 | return folder 124 | 125 | def getfolders(self): 126 | """Returns a list of all cached folders. 127 | 128 | Does nothing for this backend. We mangle the folder file names 129 | (see getfolderfilename) so we can not derive folder names from 130 | the file names that we have available. TODO: need to store a 131 | list of folder names somehow?""" 132 | 133 | pass 134 | 135 | def forgetfolders(self): 136 | """Forgets the cached list of folders, if any. Useful to run 137 | after a sync run.""" 138 | 139 | self._folders = {} 140 | -------------------------------------------------------------------------------- /offlineimap/repository/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2002-2016 John Goerzen & contributors. 2 | # 3 | # This program is free software; you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation; either version 2 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software 15 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 16 | 17 | import six 18 | from sys import exc_info 19 | 20 | try: 21 | from configparser import NoSectionError 22 | except ImportError: #python2 23 | from ConfigParser import NoSectionError 24 | 25 | from offlineimap.repository.IMAP import IMAPRepository, MappedIMAPRepository 26 | from offlineimap.repository.Gmail import GmailRepository 27 | from offlineimap.repository.Maildir import MaildirRepository 28 | from offlineimap.repository.GmailMaildir import GmailMaildirRepository 29 | from offlineimap.repository.LocalStatus import LocalStatusRepository 30 | from offlineimap.error import OfflineImapError 31 | 32 | 33 | class Repository(object): 34 | """Abstract class that returns the correct Repository type 35 | instance based on 'account' and 'reqtype', e.g. a 36 | class:`ImapRepository` instance.""" 37 | 38 | def __new__(cls, account, reqtype): 39 | """ 40 | :param account: :class:`Account` 41 | :param regtype: 'remote', 'local', or 'status'""" 42 | 43 | if reqtype == 'remote': 44 | name = account.getconf('remoterepository') 45 | # We don't support Maildirs on the remote side. 46 | typemap = {'IMAP': IMAPRepository, 47 | 'Gmail': GmailRepository} 48 | 49 | elif reqtype == 'local': 50 | name = account.getconf('localrepository') 51 | typemap = {'IMAP': MappedIMAPRepository, 52 | 'Maildir': MaildirRepository, 53 | 'GmailMaildir': GmailMaildirRepository} 54 | 55 | elif reqtype == 'status': 56 | # create and return a LocalStatusRepository. 57 | name = account.getconf('localrepository') 58 | return LocalStatusRepository(name, account) 59 | 60 | else: 61 | errstr = "Repository type %s not supported" % reqtype 62 | raise OfflineImapError(errstr, OfflineImapError.ERROR.REPO) 63 | 64 | # Get repository type. 65 | config = account.getconfig() 66 | try: 67 | repostype = config.get('Repository ' + name, 'type').strip() 68 | except NoSectionError as e: 69 | errstr = ("Could not find section '%s' in configuration. Required " 70 | "for account '%s'." % ('Repository %s' % name, account)) 71 | six.reraise(OfflineImapError, 72 | OfflineImapError(errstr, OfflineImapError.ERROR.REPO), 73 | exc_info()[2]) 74 | 75 | try: 76 | repo = typemap[repostype] 77 | except KeyError: 78 | errstr = "'%s' repository not supported for '%s' repositories."% \ 79 | (repostype, reqtype) 80 | six.reraise(OfflineImapError, 81 | OfflineImapError(errstr, OfflineImapError.ERROR.REPO), 82 | exc_info()[2]) 83 | 84 | return repo(name, account) 85 | 86 | def __init__(self, account, reqtype): 87 | """Load the correct Repository type and return that. The 88 | __init__ of the corresponding Repository class will be 89 | executed instead of this stub 90 | 91 | :param account: :class:`Account` 92 | :param regtype: 'remote', 'local', or 'status' 93 | """ 94 | pass 95 | -------------------------------------------------------------------------------- /offlineimap/threadutil.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2002-2016 John Goerzen & contributors 2 | # Thread support module 3 | # 4 | # This program is free software; you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 2 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 17 | 18 | from threading import Lock, Thread, BoundedSemaphore, currentThread 19 | try: 20 | from Queue import Queue, Empty 21 | except ImportError: # python3 22 | from queue import Queue, Empty 23 | import traceback 24 | import os.path 25 | from offlineimap.ui import getglobalui 26 | 27 | 28 | STOP_MONITOR = 'STOP_MONITOR' 29 | 30 | ###################################################################### 31 | # General utilities 32 | ###################################################################### 33 | 34 | def semaphorereset(semaphore, originalstate): 35 | """Block until `semaphore` gets back to its original state, ie all acquired 36 | resources have been released.""" 37 | 38 | for i in range(originalstate): 39 | semaphore.acquire() 40 | # Now release these. 41 | for i in range(originalstate): 42 | semaphore.release() 43 | 44 | class accountThreads(object): 45 | """Store the list of all threads in the software so it can be used to find out 46 | what's running and what's not.""" 47 | 48 | def __init__(self): 49 | self.lock = Lock() 50 | self.list = [] 51 | 52 | def add(self, thread): 53 | with self.lock: 54 | self.list.append(thread) 55 | 56 | def remove(self, thread): 57 | with self.lock: 58 | self.list.remove(thread) 59 | 60 | def pop(self): 61 | with self.lock: 62 | if len(self.list) < 1: 63 | return None 64 | return self.list.pop() 65 | 66 | def wait(self): 67 | while True: 68 | thread = self.pop() 69 | if thread is None: 70 | break 71 | thread.join() 72 | 73 | 74 | ###################################################################### 75 | # Exit-notify threads 76 | ###################################################################### 77 | 78 | exitedThreads = Queue() 79 | 80 | def monitor(): 81 | """An infinite "monitoring" loop watching for finished ExitNotifyThread's. 82 | 83 | This one is supposed to run in the main thread. 84 | :param callback: the function to call when a thread terminated. That 85 | function is called with a single argument -- the 86 | ExitNotifyThread that has terminated. The monitor will 87 | not continue to monitor for other threads until 88 | 'callback' returns, so if it intends to perform long 89 | calculations, it should start a new thread itself -- but 90 | NOT an ExitNotifyThread, or else an infinite loop 91 | may result. 92 | Furthermore, the monitor will hold the lock all the 93 | while the other thread is waiting. 94 | :type callback: a callable function 95 | """ 96 | 97 | global exitedThreads 98 | ui = getglobalui() 99 | 100 | while True: 101 | # Loop forever and call 'callback' for each thread that exited 102 | try: 103 | # We need a timeout in the get() call, so that ctrl-c can throw a 104 | # SIGINT (http://bugs.python.org/issue1360). A timeout with empty 105 | # Queue will raise `Empty`. 106 | # 107 | # ExitNotifyThread add themselves to the exitedThreads queue once 108 | # they are done (normally or with exception). 109 | thread = exitedThreads.get(True, 60) 110 | # Request to abort when callback returns True. 111 | 112 | if thread.exit_exception is not None: 113 | if isinstance(thread.exit_exception, SystemExit): 114 | # Bring a SystemExit into the main thread. 115 | # Do not send it back to UI layer right now. 116 | # Maybe later send it to ui.terminate? 117 | raise SystemExit 118 | ui.threadException(thread) # Expected to terminate the program. 119 | # Should never hit this line. 120 | raise AssertionError("thread has 'exit_exception' set to" 121 | " '%s' [%s] but this value is unexpected" 122 | " and the ui did not stop the program."% 123 | (repr(thread.exit_exception), type(thread.exit_exception))) 124 | 125 | # Only the monitor thread has this exit message set. 126 | elif thread.exit_message == STOP_MONITOR: 127 | break # Exit the loop here. 128 | else: 129 | ui.threadExited(thread) 130 | except Empty: 131 | pass 132 | 133 | 134 | class ExitNotifyThread(Thread): 135 | """This class is designed to alert a "monitor" to the fact that a 136 | thread has exited and to provide for the ability for it to find out 137 | why. All instances are made daemon threads (setDaemon(True), so we 138 | bail out when the mainloop dies. 139 | 140 | The thread can set instance variables self.exit_message for a human 141 | readable reason of the thread exit. 142 | 143 | There is one instance of this class at runtime. The main thread waits for 144 | the monitor to end.""" 145 | 146 | def __init__(self, *args, **kwargs): 147 | super(ExitNotifyThread, self).__init__(*args, **kwargs) 148 | # These are all child threads that are supposed to go away when 149 | # the main thread is killed. 150 | self.setDaemon(True) 151 | self.exit_message = None 152 | self._exit_exc = None 153 | self._exit_stacktrace = None 154 | 155 | def run(self): 156 | """Allow profiling of a run and store exceptions.""" 157 | 158 | global exitedThreads 159 | try: 160 | Thread.run(self) 161 | except Exception as e: 162 | # Thread exited with Exception, store it 163 | tb = traceback.format_exc() 164 | self.set_exit_exception(e, tb) 165 | 166 | exitedThreads.put(self, True) 167 | 168 | def set_exit_exception(self, exc, st=None): 169 | """Sets Exception and stacktrace of a thread, so that other 170 | threads can query its exit status""" 171 | 172 | self._exit_exc = exc 173 | self._exit_stacktrace = st 174 | 175 | @property 176 | def exit_exception(self): 177 | """Returns the cause of the exit, one of: 178 | Exception() -- the thread aborted with this exception 179 | None -- normal termination.""" 180 | 181 | return self._exit_exc 182 | 183 | @property 184 | def exit_stacktrace(self): 185 | """Returns a string representing the stack trace if set""" 186 | 187 | return self._exit_stacktrace 188 | 189 | 190 | ###################################################################### 191 | # Instance-limited threads 192 | ###################################################################### 193 | 194 | limitedNamespaces = {} 195 | 196 | def initInstanceLimit(limitNamespace, instancemax): 197 | """Initialize the instance-limited thread implementation. 198 | 199 | Run up to intancemax threads for the given limitNamespace. This allows to 200 | honor maxsyncaccounts and maxconnections.""" 201 | 202 | global limitedNamespaces 203 | 204 | if not limitNamespace in limitedNamespaces: 205 | limitedNamespaces[limitNamespace] = BoundedSemaphore(instancemax) 206 | 207 | 208 | class InstanceLimitedThread(ExitNotifyThread): 209 | def __init__(self, limitNamespace, *args, **kwargs): 210 | self.limitNamespace = limitNamespace 211 | super(InstanceLimitedThread, self).__init__(*args, **kwargs) 212 | 213 | def start(self): 214 | global limitedNamespaces 215 | 216 | # Will block until the semaphore has free slots. 217 | limitedNamespaces[self.limitNamespace].acquire() 218 | ExitNotifyThread.start(self) 219 | 220 | def run(self): 221 | global limitedNamespaces 222 | 223 | try: 224 | ExitNotifyThread.run(self) 225 | finally: 226 | if limitedNamespaces and limitedNamespaces[self.limitNamespace]: 227 | limitedNamespaces[self.limitNamespace].release() 228 | -------------------------------------------------------------------------------- /offlineimap/ui/Machine.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2007-2018 John Goerzen & contributors. 2 | # 3 | # This program is free software; you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation; either version 2 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software 15 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 16 | try: 17 | from urllib import urlencode 18 | except ImportError: # python3 19 | from urllib.parse import urlencode 20 | import sys 21 | import time 22 | import logging 23 | from threading import currentThread 24 | 25 | import offlineimap 26 | from offlineimap.ui.UIBase import UIBase 27 | 28 | protocol = '7.2.0' 29 | 30 | class MachineLogFormatter(logging.Formatter): 31 | """urlencodes any outputted line, to avoid multi-line output""" 32 | 33 | def format(s, record): 34 | # Mapping of log levels to historic tag names 35 | severity_map = { 36 | 'info': 'msg', 37 | 'warning': 'warn', 38 | } 39 | line = super(MachineLogFormatter, s).format(record) 40 | severity = record.levelname.lower() 41 | if severity in severity_map: 42 | severity = severity_map[severity] 43 | if hasattr(record, "machineui"): 44 | command = record.machineui["command"] 45 | whoami = record.machineui["id"] 46 | else: 47 | command = "" 48 | whoami = currentThread().getName() 49 | 50 | prefix = "%s:%s"% (command, urlencode([('', whoami)])[1:]) 51 | return "%s:%s:%s"% (severity, prefix, urlencode([('', line)])[1:]) 52 | 53 | 54 | class MachineUI(UIBase): 55 | def __init__(s, config, loglevel=logging.INFO): 56 | super(MachineUI, s).__init__(config, loglevel) 57 | s._log_con_handler.createLock() 58 | """lock needed to block on password input""" 59 | # Set up the formatter that urlencodes the strings... 60 | s._log_con_handler.setFormatter(MachineLogFormatter()) 61 | 62 | # Arguments: 63 | # - handler: must be method from s.logger that reflects 64 | # the severity of the passed message 65 | # - command: command that produced this message 66 | # - msg: the message itself 67 | def _printData(s, handler, command, msg): 68 | handler(msg, 69 | extra = { 70 | 'machineui': { 71 | 'command': command, 72 | 'id': currentThread().getName(), 73 | } 74 | }) 75 | 76 | def _msg(s, msg): 77 | s._printData(s.logger.info, '_display', msg) 78 | 79 | def warn(s, msg, minor=0): 80 | # TODO, remove and cleanup the unused minor stuff 81 | s._printData(s.logger.warning, '', msg) 82 | 83 | def registerthread(s, account): 84 | super(MachineUI, s).registerthread(account) 85 | s._printData(s.logger.info, 'registerthread', account) 86 | 87 | def unregisterthread(s, thread): 88 | UIBase.unregisterthread(s, thread) 89 | s._printData(s.logger.info, 'unregisterthread', thread.getName()) 90 | 91 | def debugging(s, debugtype): 92 | s._printData(s.logger.debug, 'debugging', debugtype) 93 | 94 | def acct(s, accountname): 95 | s._printData(s.logger.info, 'acct', accountname) 96 | 97 | def acctdone(s, accountname): 98 | s._printData(s.logger.info, 'acctdone', accountname) 99 | 100 | def validityproblem(s, folder): 101 | s._printData(s.logger.warning, 'validityproblem', "%s\n%s\n%s\n%s"% 102 | (folder.getname(), folder.getrepository().getname(), 103 | folder.get_saveduidvalidity(), folder.get_uidvalidity())) 104 | 105 | def connecting(s, reposname, hostname, port): 106 | s._printData(s.logger.info, 'connecting', "%s\n%s\n%s"% (hostname, 107 | str(port), reposname)) 108 | 109 | def syncfolders(s, srcrepos, destrepos): 110 | s._printData(s.logger.info, 'syncfolders', "%s\n%s"% (s.getnicename(srcrepos), 111 | s.getnicename(destrepos))) 112 | 113 | def syncingfolder(s, srcrepos, srcfolder, destrepos, destfolder): 114 | s._printData(s.logger.info, 'syncingfolder', "%s\n%s\n%s\n%s\n"% 115 | (s.getnicename(srcrepos), srcfolder.getname(), 116 | s.getnicename(destrepos), destfolder.getname())) 117 | 118 | def loadmessagelist(s, repos, folder): 119 | s._printData(s.logger.info, 'loadmessagelist', "%s\n%s"% (s.getnicename(repos), 120 | folder.getvisiblename())) 121 | 122 | def messagelistloaded(s, repos, folder, count): 123 | s._printData(s.logger.info, 'messagelistloaded', "%s\n%s\n%d"% 124 | (s.getnicename(repos), folder.getname(), count)) 125 | 126 | def syncingmessages(s, sr, sf, dr, df): 127 | s._printData(s.logger.info, 'syncingmessages', "%s\n%s\n%s\n%s\n"% 128 | (s.getnicename(sr), sf.getname(), s.getnicename(dr), 129 | df.getname())) 130 | 131 | def ignorecopyingmessage(s, uid, srcfolder, destfolder): 132 | s._printData(s.logger.info, 'ignorecopyingmessage', "%d\n%s\n%s\n%s[%s]"% 133 | (uid, s.getnicename(srcfolder), srcfolder.getname(), 134 | s.getnicename(destfolder), destfolder)) 135 | 136 | def copyingmessage(s, uid, num, num_to_copy, srcfolder, destfolder): 137 | s._printData(s.logger.info, 'copyingmessage', "%d\n%s\n%s\n%s[%s]"% 138 | (uid, s.getnicename(srcfolder), srcfolder.getname(), 139 | s.getnicename(destfolder), destfolder)) 140 | 141 | def folderlist(s, list): 142 | return ("\f".join(["%s\t%s"% (s.getnicename(x), x.getname()) for x in list])) 143 | 144 | def uidlist(s, list): 145 | return ("\f".join([str(u) for u in list])) 146 | 147 | def deletingmessages(s, uidlist, destlist): 148 | ds = s.folderlist(destlist) 149 | s._printData(s.logger.info, 'deletingmessages', "%s\n%s"% (s.uidlist(uidlist), ds)) 150 | 151 | def addingflags(s, uidlist, flags, dest): 152 | s._printData(s.logger.info, "addingflags", "%s\n%s\n%s"% (s.uidlist(uidlist), 153 | "\f".join(flags), 154 | dest)) 155 | 156 | def deletingflags(s, uidlist, flags, dest): 157 | s._printData(s.logger.info, 'deletingflags', "%s\n%s\n%s"% (s.uidlist(uidlist), 158 | "\f".join(flags), 159 | dest)) 160 | 161 | def threadException(s, thread): 162 | s._printData(s.logger.warning, 'threadException', "%s\n%s"% 163 | (thread.getName(), s.getThreadExceptionString(thread))) 164 | s.delThreadDebugLog(thread) 165 | s.terminate(100) 166 | 167 | def terminate(s, exitstatus=0, errortitle='', errormsg=''): 168 | s._printData(s.logger.info, 'terminate', "%d\n%s\n%s"% (exitstatus, errortitle, errormsg)) 169 | sys.exit(exitstatus) 170 | 171 | def mainException(s): 172 | s._printData(s.logger.warning, 'mainException', s.getMainExceptionString()) 173 | 174 | def threadExited(s, thread): 175 | s._printData(s.logger.info, 'threadExited', thread.getName()) 176 | UIBase.threadExited(s, thread) 177 | 178 | def sleeping(s, sleepsecs, remainingsecs): 179 | s._printData(s.logger.info, 'sleeping', "%d\n%d"% (sleepsecs, remainingsecs)) 180 | if sleepsecs > 0: 181 | time.sleep(sleepsecs) 182 | return 0 183 | 184 | 185 | def getpass(s, username, config, errmsg=None): 186 | if errmsg: 187 | s._printData(s.logger.warning, 188 | 'getpasserror', "%s\n%s"% (username, errmsg), 189 | False) 190 | 191 | s._log_con_handler.acquire() # lock the console output 192 | try: 193 | s._printData(s.logger.info, 'getpass', username) 194 | return (sys.stdin.readline()[:-1]) 195 | finally: 196 | s._log_con_handler.release() 197 | 198 | def init_banner(s): 199 | s._printData(s.logger.info, 'protocol', protocol) 200 | s._printData(s.logger.info, 'initbanner', offlineimap.banner) 201 | 202 | def callhook(s, msg): 203 | s._printData(s.logger.info, 'callhook', msg) 204 | -------------------------------------------------------------------------------- /offlineimap/ui/Noninteractive.py: -------------------------------------------------------------------------------- 1 | # Noninteractive UI 2 | # Copyright (C) 2002-2016 John Goerzen & contributors. 3 | # 4 | # This program is free software; you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 2 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 17 | 18 | import logging 19 | 20 | import offlineimap 21 | from offlineimap.ui.UIBase import UIBase 22 | 23 | class Basic(UIBase): 24 | """'Basic' simply sets log level to INFO.""" 25 | 26 | def __init__(self, config, loglevel = logging.INFO): 27 | return super(Basic, self).__init__(config, loglevel) 28 | 29 | class Quiet(UIBase): 30 | """'Quiet' simply sets log level to WARNING""" 31 | def __init__(self, config, loglevel = logging.WARNING): 32 | return super(Quiet, self).__init__(config, loglevel) 33 | 34 | class Syslog(UIBase): 35 | """'Syslog' sets log level to INFO and outputs to syslog instead of stdout""" 36 | def __init__(self, config, loglevel = logging.INFO): 37 | return super(Syslog, self).__init__(config, loglevel) 38 | 39 | def setup_consolehandler(self): 40 | # create syslog handler 41 | ch = logging.handlers.SysLogHandler('/dev/log') 42 | # create formatter and add it to the handlers 43 | self.formatter = logging.Formatter("%(message)s") 44 | ch.setFormatter(self.formatter) 45 | # add the handlers to the logger 46 | self.logger.addHandler(ch) 47 | self.logger.info(offlineimap.banner) 48 | return ch 49 | 50 | def setup_sysloghandler(self): 51 | pass # Do not honor -s (log to syslog) CLI option. 52 | -------------------------------------------------------------------------------- /offlineimap/ui/TTY.py: -------------------------------------------------------------------------------- 1 | # TTY UI 2 | # Copyright (C) 2002-2018 John Goerzen & contributors 3 | # 4 | # This program is free software; you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 2 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 17 | 18 | import logging 19 | import sys 20 | import time 21 | from getpass import getpass 22 | 23 | from offlineimap import banner 24 | from offlineimap.ui.UIBase import UIBase 25 | 26 | 27 | class TTYFormatter(logging.Formatter): 28 | """Specific Formatter that adds thread information to the log output.""" 29 | 30 | def __init__(self, *args, **kwargs): 31 | #super() doesn't work in py2.6 as 'logging' uses old-style class 32 | logging.Formatter.__init__(self, *args, **kwargs) 33 | self._last_log_thread = None 34 | 35 | def format(self, record): 36 | """Override format to add thread information.""" 37 | 38 | #super() doesn't work in py2.6 as 'logging' uses old-style class 39 | log_str = logging.Formatter.format(self, record) 40 | # If msg comes from a different thread than our last, prepend 41 | # thread info. Most look like 'Account sync foo' or 'Folder 42 | # sync foo'. 43 | t_name = record.threadName 44 | if t_name == 'MainThread': 45 | return log_str # main thread doesn't get things prepended 46 | if t_name != self._last_log_thread: 47 | self._last_log_thread = t_name 48 | log_str = "%s:\n %s" % (t_name, log_str) 49 | else: 50 | log_str = " %s"% log_str 51 | return log_str 52 | 53 | 54 | class TTYUI(UIBase): 55 | def setup_consolehandler(self): 56 | """Backend specific console handler 57 | 58 | Sets up things and adds them to self.logger. 59 | :returns: The logging.Handler() for console output""" 60 | 61 | # create console handler with a higher log level 62 | ch = logging.StreamHandler() 63 | #ch.setLevel(logging.DEBUG) 64 | # create formatter and add it to the handlers 65 | self.formatter = TTYFormatter("%(message)s") 66 | ch.setFormatter(self.formatter) 67 | # add the handlers to the logger 68 | self.logger.addHandler(ch) 69 | self.logger.info(banner) 70 | # init lock for console output 71 | ch.createLock() 72 | return ch 73 | 74 | def isusable(self): 75 | """TTYUI is reported as usable when invoked on a terminal.""" 76 | 77 | return sys.stdout.isatty() and sys.stdin.isatty() 78 | 79 | def getpass(self, username, config, errmsg=None): 80 | """TTYUI backend is capable of querying the password.""" 81 | 82 | if errmsg: 83 | self.warn("%s: %s"% (username, errmsg)) 84 | self._log_con_handler.acquire() # lock the console output 85 | try: 86 | return getpass("Enter password for user '%s': " % username) 87 | finally: 88 | self._log_con_handler.release() 89 | 90 | def mainException(self): 91 | if isinstance(sys.exc_info()[1], KeyboardInterrupt): 92 | self.logger.warn("Timer interrupted at user request; program " 93 | "terminating.\n") 94 | self.terminate() 95 | else: 96 | UIBase.mainException(self) 97 | 98 | def sleeping(self, sleepsecs, remainingsecs): 99 | """Sleep for sleepsecs, display remainingsecs to go. 100 | 101 | Does nothing if sleepsecs <= 0. 102 | Display a message on the screen if we pass a full minute. 103 | 104 | This implementation in UIBase does not support this, but some 105 | implementations return 0 for successful sleep and 1 for an 106 | 'abort', ie a request to sync immediately.""" 107 | 108 | if sleepsecs > 0: 109 | if remainingsecs//60 != (remainingsecs-sleepsecs)//60: 110 | self.logger.info("Next refresh in %.1f minutes" % ( 111 | remainingsecs/60.0)) 112 | time.sleep(sleepsecs) 113 | return 0 114 | -------------------------------------------------------------------------------- /offlineimap/ui/__init__.py: -------------------------------------------------------------------------------- 1 | # UI module 2 | # Copyright (C) 2010-2011 Sebastian Spaeth & contributors 3 | # 4 | # This program is free software; you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 2 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 17 | 18 | from offlineimap.ui.UIBase import getglobalui, setglobalui 19 | from offlineimap.ui import TTY, Noninteractive, Machine 20 | 21 | UI_LIST = {'ttyui': TTY.TTYUI, 22 | 'basic': Noninteractive.Basic, 23 | 'quiet': Noninteractive.Quiet, 24 | 'syslog': Noninteractive.Syslog, 25 | 'machineui': Machine.MachineUI} 26 | 27 | #add Blinkenlights UI if it imports correctly (curses installed) 28 | try: 29 | from offlineimap.ui import Curses 30 | UI_LIST['blinkenlights'] = Curses.Blinkenlights 31 | except ImportError: 32 | pass 33 | -------------------------------------------------------------------------------- /offlineimap/ui/debuglock.py: -------------------------------------------------------------------------------- 1 | # Locking debugging code -- temporary 2 | # Copyright (C) 2003-2015 John Goerzen & contributors 3 | # 4 | # This program is free software; you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 2 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 17 | 18 | from threading import Lock, currentThread 19 | import traceback 20 | logfile = open("/tmp/logfile", "wt") 21 | loglock = Lock() 22 | 23 | class DebuggingLock: 24 | def __init__(self, name): 25 | self.lock = Lock() 26 | self.name = name 27 | 28 | def acquire(self, blocking = 1): 29 | self.print_tb("Acquire lock") 30 | self.lock.acquire(blocking) 31 | self.logmsg("===== %s: Thread %s acquired lock\n"% 32 | (self.name, currentThread().getName())) 33 | 34 | def release(self): 35 | self.print_tb("Release lock") 36 | self.lock.release() 37 | 38 | def logmsg(self, msg): 39 | loglock.acquire() 40 | logfile.write(msg + "\n") 41 | logfile.flush() 42 | loglock.release() 43 | 44 | def print_tb(self, msg): 45 | self.logmsg(".... %s: Thread %s attempting to %s\n"% \ 46 | (self.name, currentThread().getName(), msg) + \ 47 | "\n".join(traceback.format_list(traceback.extract_stack()))) 48 | 49 | 50 | -------------------------------------------------------------------------------- /offlineimap/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicolas33/offlineimap/ba4ecea9e4e4f14e91f3cbd0dc7c54182b232994/offlineimap/utils/__init__.py -------------------------------------------------------------------------------- /offlineimap/utils/const.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2013-2014 Eygene A. Ryabinkin and contributors 2 | # 3 | # Collection of classes that implement const-like behaviour 4 | # for various objects. 5 | 6 | import copy 7 | 8 | class ConstProxy(object): 9 | """Implements read-only access to a given object 10 | that can be attached to each instance only once.""" 11 | 12 | def __init__(self): 13 | self.__dict__['__source'] = None 14 | 15 | 16 | def __getattr__(self, name): 17 | src = self.__dict__['__source'] 18 | if src == None: 19 | raise ValueError("using non-initialized ConstProxy() object") 20 | return copy.deepcopy(getattr(src, name)) 21 | 22 | 23 | def __setattr__(self, name, value): 24 | raise AttributeError("tried to set '%s' to '%s' for constant object"% \ 25 | (name, value)) 26 | 27 | 28 | def __delattr__(self, name): 29 | raise RuntimeError("tried to delete field '%s' from constant object"% \ 30 | (name)) 31 | 32 | 33 | def set_source(self, source): 34 | """ Sets source object for this instance. """ 35 | if (self.__dict__['__source'] != None): 36 | raise ValueError("source object is already set") 37 | self.__dict__['__source'] = source 38 | -------------------------------------------------------------------------------- /offlineimap/utils/distro.py: -------------------------------------------------------------------------------- 1 | # Copyright 2006-2018 Eygene A. Ryabinkin & contributors. 2 | # 3 | # Module that supports distribution-specific functions. 4 | 5 | import platform 6 | import os 7 | 8 | 9 | # Each dictionary value is either string or some iterable. 10 | # 11 | # For the former we will just return the value, for an iterable 12 | # we will walk through the values and will return the first 13 | # one that corresponds to the existing file. 14 | __DEF_OS_LOCATIONS = { 15 | 'freebsd': '/usr/local/share/certs/ca-root-nss.crt', 16 | 'openbsd': '/etc/ssl/cert.pem', 17 | 'dragonfly': '/etc/ssl/cert.pem', 18 | 'darwin': [ 19 | # MacPorts, port curl-ca-bundle 20 | '/opt/local/share/curl/curl-ca-bundle.crt', 21 | # homebrew, package openssl 22 | '/usr/local/etc/openssl/cert.pem', 23 | ], 24 | 'linux-ubuntu': '/etc/ssl/certs/ca-certificates.crt', 25 | 'linux-debian': '/etc/ssl/certs/ca-certificates.crt', 26 | 'linux-gentoo': '/etc/ssl/certs/ca-certificates.crt', 27 | 'linux-fedora': '/etc/pki/tls/certs/ca-bundle.crt', 28 | 'linux-redhat': '/etc/pki/tls/certs/ca-bundle.crt', 29 | 'linux-suse': '/etc/ssl/ca-bundle.pem', 30 | 'linux-opensuse': '/etc/ssl/ca-bundle.pem', 31 | 'linux-arch': '/etc/ssl/certs/ca-certificates.crt', 32 | } 33 | 34 | 35 | def get_os_name(): 36 | """ 37 | Finds out OS name. For non-Linux system it will be just a plain 38 | OS name (like FreeBSD), for Linux it will be "linux-", 39 | where is the name of the distribution, as returned by 40 | the first component of platform.linux_distribution. 41 | 42 | Return value will be all-lowercase to avoid confusion about 43 | proper name capitalisation. 44 | 45 | """ 46 | OS = platform.system().lower() 47 | 48 | if OS.startswith('linux'): 49 | DISTRO = platform.linux_distribution()[0] 50 | if DISTRO: 51 | OS = OS + "-%s" % DISTRO.split()[0].lower() 52 | if os.path.exists('/etc/arch-release'): 53 | OS = "linux-arch" 54 | 55 | return OS 56 | 57 | def get_os_sslcertfile_searchpath(): 58 | """Returns search path for CA bundle for the current OS. 59 | 60 | We will return an iterable even if configuration has just 61 | a single value: it is easier for our callers to be sure 62 | that they can iterate over result. 63 | 64 | Returned value of None means that there is no search path 65 | at all. 66 | """ 67 | 68 | OS = get_os_name() 69 | 70 | l = None 71 | if OS in __DEF_OS_LOCATIONS: 72 | l = __DEF_OS_LOCATIONS[OS] 73 | if not hasattr(l, '__iter__'): 74 | l = (l, ) 75 | return l 76 | 77 | 78 | def get_os_sslcertfile(): 79 | """ 80 | Finds out the location for the distribution-specific 81 | CA certificate file bundle. 82 | 83 | Returns the location of the file or None if there is 84 | no known CA certificate file or all known locations 85 | correspond to non-existing filesystem objects. 86 | """ 87 | 88 | l = get_os_sslcertfile_searchpath() 89 | if l == None: 90 | return None 91 | 92 | for f in l: 93 | assert (type(f) == type("")) 94 | if os.path.exists(f) and \ 95 | (os.path.isfile(f) or os.path.islink(f)): 96 | return f 97 | 98 | return None 99 | -------------------------------------------------------------------------------- /offlineimap/utils/stacktrace.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013 Eygene A. Ryabinkin 2 | # Functions to perform stack tracing (for multithreaded programs 3 | # as well as for single-threaded ones). 4 | 5 | import sys 6 | import threading 7 | import traceback 8 | 9 | 10 | def dump(out): 11 | """ Dumps current stack trace into I/O object 'out' """ 12 | id2name = {} 13 | for th in threading.enumerate(): 14 | id2name[th.ident] = th.name 15 | n = 0 16 | for i, stack in sys._current_frames().items(): 17 | out.write ("\n# Thread #%d (id=%d), %s\n" % \ 18 | (n, i, id2name[i])) 19 | n = n + 1 20 | for f, lno, name, line in traceback.extract_stack (stack): 21 | out.write ('File: "%s", line %d, in %s' % \ 22 | (f, lno, name)) 23 | if line: 24 | out.write (" %s" % (line.strip())) 25 | out.write ("\n") 26 | -------------------------------------------------------------------------------- /offlineimap/virtual_imaplib2.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2016-2016 Nicolas Sebrecht & contributors 2 | # 3 | # This program is free software; you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation; either version 2 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software 15 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 16 | 17 | """ 18 | 19 | The virtual imaplib2 takes care to import the correct imaplib2 library. Any 20 | internal use of imaplib2 everywhere else in offlineimap must be done through 21 | this virtual_imaplib2 or we might go into troubles. 22 | 23 | """ 24 | 25 | DESC = None 26 | 27 | _SUPPORTED_RELEASE = 2 28 | _SUPPORTED_REVISION = 57 29 | 30 | try: 31 | # Try any imaplib2 in PYTHONPATH first. This allows both maintainers of 32 | # distributions and developers to not work with the bundled imaplib2. 33 | from imaplib2 import * 34 | import imaplib2 as imaplib 35 | 36 | if (int(imaplib.__release__) < _SUPPORTED_RELEASE or 37 | int(imaplib.__revision__) < _SUPPORTED_REVISION): 38 | raise ImportError("The provided imaplib2 version '%s' is not supported"% 39 | imaplib.__version__) 40 | DESC = "system" 41 | except (ImportError, NameError) as e: 42 | try: 43 | from offlineimap.bundled_imaplib2 import * 44 | import offlineimap.bundled_imaplib2 as imaplib 45 | 46 | DESC = "bundled" 47 | except: 48 | print("Error while trying to import system imaplib2: %s"% e) 49 | raise 50 | 51 | # Upstream won't expose those literals to avoid erasing them with "import *" in 52 | # case they exist. 53 | __version__ = imaplib.__version__ 54 | __release__ = imaplib.__release__ 55 | __revision__ = imaplib.__revision__ 56 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # Requirements 2 | six 3 | gssapi[kerberos] 4 | portalocker[cygwin] 5 | rfc6555 6 | -------------------------------------------------------------------------------- /scripts/get-repository.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Licence: this file is in the public domain. 4 | # 5 | # Download and configure the repositories of the website or wiki. 6 | 7 | repository=$1 8 | github_remote=$2 9 | 10 | # 11 | # TODO 12 | # 13 | final_note () { 14 | cat < 20 | $ cd ./$1 21 | $ git remote add myfork https://github.com//.git 22 | EOF 23 | } 24 | 25 | setup () { 26 | target_dir=$1 27 | remote_url=$2 28 | 29 | # Adjust $PWD if necessary. 30 | test -d scripts || cd .. 31 | if test ! -d scripts 32 | then 33 | echo "cannot figure the correct workdir..." 34 | exit 2 35 | fi 36 | 37 | if test -d $target_dir 38 | then 39 | echo "Directory '$target_dir' already exists..." 40 | exit 3 41 | fi 42 | 43 | git clone "${remote_url}.git" "$1" 44 | echo '' 45 | if test $? -gt 0 46 | then 47 | echo "Cannot fork $remote_url to $1" 48 | exit 4 49 | fi 50 | } 51 | 52 | configure_website () { 53 | renderer='./render.sh' 54 | 55 | echo "Found Github username: '$1'" 56 | echo "If it's wrong, please fix the script ./website/render.sh" 57 | 58 | cd ./website 59 | if test $? -eq 0 60 | then 61 | sed -r -i -e "s,{{USERNAME}},$1," "$renderer" 62 | cd .. 63 | else 64 | echo "ERROR: could not enter ./website. (?)" 65 | fi 66 | } 67 | 68 | configure_wiki () { 69 | : # noop 70 | } 71 | 72 | test n$github_remote = 'n' && github_remote='origin' 73 | 74 | # Get Github username. 75 | #offlineimap_url="$(git config --local --get remote.origin.url)" 76 | offlineimap_url="$(git config --local --get remote.nicolas33.url)" 77 | username=$(echo $offlineimap_url | sed -r -e 's,.*github.com.([^/]+)/.*,\1,') 78 | 79 | 80 | case n$repository in 81 | nwebsite) 82 | upstream=https://github.com/OfflineIMAP/offlineimap.github.io 83 | setup website "$upstream" 84 | configure_website "$username" 85 | final_note website "$upstream" 86 | ;; 87 | nwiki) 88 | upstream=https://github.com/OfflineIMAP/offlineimap.wiki 89 | setup wiki "$upstream" 90 | configure_wiki 91 | final_note wiki "$upstream" 92 | ;; 93 | *) 94 | cat <] 96 | 97 | : The name of the Git repository of YOUR fork at Github. 98 | Default: origin 99 | EOF 100 | exit 1 101 | ;; 102 | esac 103 | 104 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | 2 | [metadata] 3 | description-file = README.md 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # $Id: setup.py,v 1.1 2002/06/21 18:10:49 jgoerzen Exp $ 4 | 5 | # IMAP synchronization 6 | # Module: installer 7 | # COPYRIGHT # 8 | # Copyright (C) 2002 - 2018 John Goerzen & contributors 9 | # 10 | # This program is free software; you can redistribute it and/or modify 11 | # it under the terms of the GNU General Public License as published by 12 | # the Free Software Foundation; either version 2 of the License, or 13 | # (at your option) any later version. 14 | # 15 | # This program is distributed in the hope that it will be useful, 16 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | # GNU General Public License for more details. 19 | # 20 | # You should have received a copy of the GNU General Public License 21 | # along with this program; if not, write to the Free Software 22 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 23 | 24 | import os 25 | from distutils.core import setup, Command 26 | import offlineimap 27 | import logging 28 | from test.OLItest import TextTestRunner, TestLoader, OLITestLib 29 | 30 | class TestCommand(Command): 31 | """runs the OLI testsuite""" 32 | description = """Runs the test suite. In order to execute only a single 33 | test, you could also issue e.g. 'python -m unittest 34 | test.tests.test_01_basic.TestBasicFunctions.test_01_olistartup' on the 35 | command line.""" 36 | user_options = [] 37 | 38 | def initialize_options(self): 39 | pass 40 | 41 | def finalize_options(self): 42 | pass 43 | 44 | def run(self): 45 | logging.basicConfig(format='%(message)s') 46 | # set credentials and OfflineImap command to be executed: 47 | OLITestLib(cred_file='./test/credentials.conf', cmd='./offlineimap.py') 48 | suite = TestLoader().discover('./test/tests') 49 | #TODO: failfast does not seem to exist in python2.6? 50 | TextTestRunner(verbosity=2,failfast=True).run(suite) 51 | 52 | 53 | setup(name = "offlineimap", 54 | version = offlineimap.__version__, 55 | description = offlineimap.__description__, 56 | long_description = offlineimap.__description__, 57 | author = offlineimap.__author__, 58 | author_email = offlineimap.__author_email__, 59 | url = offlineimap.__homepage__, 60 | packages = ['offlineimap', 'offlineimap.folder', 61 | 'offlineimap.repository', 'offlineimap.ui', 62 | 'offlineimap.utils'], 63 | scripts = ['bin/offlineimap'], 64 | license = offlineimap.__copyright__ + \ 65 | ", Licensed under the GPL version 2", 66 | cmdclass = { 'test': TestCommand} 67 | ) 68 | 69 | -------------------------------------------------------------------------------- /snapcraft.yaml: -------------------------------------------------------------------------------- 1 | name: offlineimap 2 | version: git 3 | summary: OfflineIMAP 4 | description: | 5 | OfflineIMAP is software that downloads your email mailbox(es) as local 6 | Maildirs. OfflineIMAP will synchronize both sides via IMAP. 7 | 8 | grade: devel 9 | confinement: devmode 10 | 11 | apps: 12 | offlineimap: 13 | command: bin/offlineimap 14 | 15 | parts: 16 | offlineimap: 17 | plugin: python 18 | python-version: python2 19 | source: . 20 | -------------------------------------------------------------------------------- /test/.gitignore: -------------------------------------------------------------------------------- 1 | credentials.conf 2 | tmp_* 3 | *.pyc 4 | OLItest/*.pyc 5 | tests/*.pyc 6 | -------------------------------------------------------------------------------- /test/OLItest/__init__.py: -------------------------------------------------------------------------------- 1 | # OfflineImap test library 2 | # Copyright (C) 2012- Sebastian Spaeth & contributors 3 | # 4 | # This program is free software; you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 2 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 17 | 18 | __all__ = ['OLITestLib', 'TextTestRunner','TestLoader'] 19 | 20 | __productname__ = 'OfflineIMAP Test suite' 21 | __version__ = '0' 22 | __copyright__ = "Copyright 2012- Sebastian Spaeth & contributors" 23 | __author__ = 'Sebastian Spaeth' 24 | __author_email__= 'Sebastian@SSpaeth.de' 25 | __description__ = 'Moo' 26 | __license__ = "Licensed under the GNU GPL v2+ (v2 or any later version)" 27 | __homepage__ = "http://www.offlineimap.org" 28 | banner = """%(__productname__)s %(__version__)s 29 | %(__license__)s""" % locals() 30 | 31 | import unittest 32 | from unittest import TestLoader, TextTestRunner 33 | from .globals import default_conf 34 | from .TestRunner import OLITestLib 35 | -------------------------------------------------------------------------------- /test/OLItest/globals.py: -------------------------------------------------------------------------------- 1 | #Constants, that don't rely on anything else in the module 2 | # Copyright (C) 2012- Sebastian Spaeth & contributors 3 | # 4 | # This program is free software; you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 2 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 17 | try: 18 | from cStringIO import StringIO 19 | except ImportError: #python3 20 | from io import StringIO 21 | 22 | default_conf=StringIO("""[general] 23 | #will be set automatically 24 | metadata = 25 | accounts = test 26 | ui = quiet 27 | 28 | [Account test] 29 | localrepository = Maildir 30 | remoterepository = IMAP 31 | 32 | [Repository Maildir] 33 | Type = Maildir 34 | # will be set automatically during tests 35 | localfolders = 36 | 37 | [Repository IMAP] 38 | type=IMAP 39 | # Don't hammer the server with too many connection attempts: 40 | maxconnections=1 41 | folderfilter= lambda f: f.startswith('INBOX.OLItest') or f.startswith('INBOX/OLItest') 42 | """) 43 | -------------------------------------------------------------------------------- /test/README: -------------------------------------------------------------------------------- 1 | Documentation for the OfflineImap Test suite. 2 | 3 | How to run the tests 4 | ==================== 5 | 6 | - Copy the credentials.conf.sample to credentials.conf and insert 7 | credentials for an IMAP account and a Gmail account. Delete the Gmail 8 | section if you don't have a Gmail account. Do note, that the tests 9 | will change the account and upload/delete/modify it's contents and 10 | folder structure. So don't use a real used account here... 11 | 12 | - go to the top level dir (one above this one) and execute: 13 | 'python setup.py test' 14 | 15 | System requirements 16 | =================== 17 | 18 | This test suite depend on python>=2.7 to run out of the box. If you want to run this with python 2.6 you will need to install the backport from http://pypi.python.org/pypi/unittest2 instead. -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicolas33/offlineimap/ba4ecea9e4e4f14e91f3cbd0dc7c54182b232994/test/__init__.py -------------------------------------------------------------------------------- /test/credentials.conf.sample: -------------------------------------------------------------------------------- 1 | [Repository IMAP] 2 | type = IMAP 3 | remotehost = localhost 4 | ssl = no 5 | #sslcacertfile = 6 | #cert_fingerprint = 7 | remoteuser = user@domain 8 | remotepass = SeKr3t 9 | 10 | [Repository Gmail] 11 | type = Gmail 12 | remoteuser = user@domain 13 | remotepass = SeKr3t 14 | -------------------------------------------------------------------------------- /test/tests/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/tests/test_00_globals.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright 2013 Eygene A. Ryabinkin 3 | 4 | from offlineimap import globals 5 | import unittest 6 | 7 | class Opt: 8 | def __init__(self): 9 | self.one = "baz" 10 | self.two = 42 11 | self.three = True 12 | 13 | 14 | class TestOfflineimapGlobals(unittest.TestCase): 15 | 16 | @classmethod 17 | def setUpClass(klass): 18 | klass.o = Opt() 19 | globals.set_options (klass.o) 20 | 21 | def test_initial_state(self): 22 | for k in self.o.__dict__.keys(): 23 | self.assertTrue(getattr(self.o, k) == 24 | getattr(globals.options, k)) 25 | 26 | def test_object_changes(self): 27 | self.o.one = "one" 28 | self.o.two = 119 29 | self.o.three = False 30 | return self.test_initial_state() 31 | 32 | def test_modification(self): 33 | with self.assertRaises(AttributeError): 34 | globals.options.two = True 35 | 36 | def test_deletion(self): 37 | with self.assertRaises(RuntimeError): 38 | del globals.options.three 39 | 40 | def test_nonexistent_key(self): 41 | with self.assertRaises(AttributeError): 42 | a = globals.options.nosuchoption 43 | 44 | def test_double_init(self): 45 | with self.assertRaises(ValueError): 46 | globals.set_options (True) 47 | 48 | 49 | if __name__ == "__main__": 50 | suite = unittest.TestLoader().loadTestsFromTestCase(TestOfflineimapGlobals) 51 | unittest.TextTestRunner(verbosity=2).run(suite) 52 | -------------------------------------------------------------------------------- /test/tests/test_00_imaputil.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2012- Sebastian Spaeth & contributors 2 | # 3 | # This program is free software; you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation; either version 2 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software 15 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 16 | import unittest 17 | import logging 18 | 19 | from offlineimap import imaputil 20 | from offlineimap.ui import UI_LIST, setglobalui 21 | from offlineimap.CustomConfig import CustomConfigParser 22 | 23 | from test.OLItest import OLITestLib 24 | 25 | # Things need to be setup first, usually setup.py initializes everything. 26 | # but if e.g. called from command line, we take care of default values here: 27 | if not OLITestLib.cred_file: 28 | OLITestLib(cred_file='./test/credentials.conf', cmd='./offlineimap.py') 29 | 30 | def setUpModule(): 31 | logging.info("Set Up test module %s" % __name__) 32 | tdir = OLITestLib.create_test_dir(suffix=__name__) 33 | 34 | def tearDownModule(): 35 | logging.info("Tear Down test module") 36 | # comment out next line to keep testdir after test runs. TODO: make nicer 37 | OLITestLib.delete_test_dir() 38 | 39 | #Stuff that can be used 40 | #self.assertEqual(self.seq, range(10)) 41 | # should raise an exception for an immutable sequence 42 | #self.assertRaises(TypeError, random.shuffle, (1,2,3)) 43 | #self.assertTrue(element in self.seq) 44 | #self.assertFalse(element in self.seq) 45 | 46 | class TestInternalFunctions(unittest.TestCase): 47 | """While the other test files test OfflineImap as a program, these 48 | tests directly invoke internal helper functions to guarantee that 49 | they deliver results as expected""" 50 | 51 | @classmethod 52 | def setUpClass(cls): 53 | #This is run before all tests in this class 54 | config= OLITestLib.get_default_config() 55 | setglobalui(UI_LIST['quiet'](config)) 56 | 57 | def test_01_imapsplit(self): 58 | """Test imaputil.imapsplit()""" 59 | res = imaputil.imapsplit(b'(\\HasNoChildren) "." "INBOX.Sent"') 60 | self.assertEqual(res, [b'(\\HasNoChildren)', b'"."', b'"INBOX.Sent"']) 61 | 62 | res = imaputil.imapsplit(b'"mo\\" o" sdfsdf') 63 | self.assertEqual(res, [b'"mo\\" o"', b'sdfsdf']) 64 | 65 | def test_02_flagsplit(self): 66 | """Test imaputil.flagsplit()""" 67 | res = imaputil.flagsplit(b'(\\Draft \\Deleted)') 68 | self.assertEqual(res, [b'\\Draft', b'\\Deleted']) 69 | 70 | res = imaputil.flagsplit(b'(FLAGS (\\Seen Old) UID 4807)') 71 | self.assertEqual(res, [b'FLAGS', b'(\\Seen Old)', b'UID', b'4807']) 72 | 73 | def test_04_flags2hash(self): 74 | """Test imaputil.flags2hash()""" 75 | res = imaputil.flags2hash(b'(FLAGS (\\Seen Old) UID 4807)') 76 | self.assertEqual(res, {b'FLAGS': b'(\\Seen Old)', b'UID': b'4807'}) 77 | 78 | def test_05_flagsimap2maildir(self): 79 | """Test imaputil.flagsimap2maildir()""" 80 | res = imaputil.flagsimap2maildir(b'(\\Draft \\Deleted)') 81 | self.assertEqual(res, set(b'DT')) 82 | 83 | def test_06_flagsmaildir2imap(self): 84 | """Test imaputil.flagsmaildir2imap()""" 85 | res = imaputil.flagsmaildir2imap(set(b'DR')) 86 | self.assertEqual(res, b'(\\Answered \\Draft)') 87 | # test all possible flags 88 | res = imaputil.flagsmaildir2imap(set(b'SRFTD')) 89 | self.assertEqual(res, b'(\\Answered \\Deleted \\Draft \\Flagged \\Seen)') 90 | 91 | def test_07_uid_sequence(self): 92 | """Test imaputil.uid_sequence()""" 93 | res = imaputil.uid_sequence([1,2,3,4,5,10,12,13]) 94 | self.assertEqual(res, b'1:5,10,12:13') 95 | -------------------------------------------------------------------------------- /test/tests/test_01_basic.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2012- Sebastian Spaeth & contributors 2 | # 3 | # This program is free software; you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation; either version 2 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software 15 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 16 | import random 17 | import unittest 18 | import logging 19 | import os, sys 20 | from test.OLItest import OLITestLib 21 | 22 | # Things need to be setup first, usually setup.py initializes everything. 23 | # but if e.g. called from command line, we take care of default values here: 24 | if not OLITestLib.cred_file: 25 | OLITestLib(cred_file='./test/credentials.conf', cmd='./offlineimap.py') 26 | 27 | 28 | def setUpModule(): 29 | logging.info("Set Up test module %s" % __name__) 30 | tdir = OLITestLib.create_test_dir(suffix=__name__) 31 | 32 | def tearDownModule(): 33 | logging.info("Tear Down test module") 34 | OLITestLib.delete_test_dir() 35 | 36 | #Stuff that can be used 37 | #self.assertEqual(self.seq, range(10)) 38 | # should raise an exception for an immutable sequence 39 | #self.assertRaises(TypeError, random.shuffle, (1,2,3)) 40 | #self.assertTrue(element in self.seq) 41 | #self.assertFalse(element in self.seq) 42 | 43 | class TestBasicFunctions(unittest.TestCase): 44 | def setUp(self): 45 | OLITestLib.delete_remote_testfolders() 46 | 47 | def tearDown(self): 48 | OLITestLib.delete_remote_testfolders() 49 | 50 | def test_01_olistartup(self): 51 | """Tests if OLI can be invoked without exceptions 52 | 53 | Cleans existing remote test folders. Then syncs all "OLItest* 54 | (specified in the default config) to our local Maildir. The 55 | result should be 0 folders and 0 mails.""" 56 | code, res = OLITestLib.run_OLI() 57 | self.assertEqual(res, "") 58 | boxes, mails = OLITestLib.count_maildir_mails('') 59 | self.assertTrue((boxes, mails)==(0,0), msg="Expected 0 folders and 0 " 60 | "mails, but sync led to {0} folders and {1} mails".format( 61 | boxes, mails)) 62 | 63 | def test_02_createdir(self): 64 | """Create local 'OLItest 1', sync""" 65 | OLITestLib.delete_maildir('') #Delete all local maildir folders 66 | OLITestLib.create_maildir('INBOX.OLItest 1') 67 | code, res = OLITestLib.run_OLI() 68 | self.assertEqual(res, "") 69 | boxes, mails = OLITestLib.count_maildir_mails('') 70 | self.assertTrue((boxes, mails)==(1,0), msg="Expected 1 folders and 0 " 71 | "mails, but sync led to {0} folders and {1} mails".format( 72 | boxes, mails)) 73 | 74 | def test_03_createdir_quote(self): 75 | """Create local 'OLItest "1"' maildir, sync 76 | 77 | Folder names with quotes used to fail and have been fixed, so 78 | one is included here as a small challenge.""" 79 | OLITestLib.delete_maildir('') #Delete all local maildir folders 80 | OLITestLib.create_maildir('INBOX.OLItest "1"') 81 | code, res = OLITestLib.run_OLI() 82 | if 'unallowed folder' in res: 83 | raise unittest.SkipTest("remote server doesn't handle quote") 84 | self.assertEqual(res, "") 85 | boxes, mails = OLITestLib.count_maildir_mails('') 86 | self.assertTrue((boxes, mails)==(1,0), msg="Expected 1 folders and 0 " 87 | "mails, but sync led to {0} folders and {1} mails".format( 88 | boxes, mails)) 89 | 90 | def test_04_nametransmismatch(self): 91 | """Create mismatching remote and local nametrans rules 92 | 93 | This should raise an error.""" 94 | config = OLITestLib.get_default_config() 95 | config.set('Repository IMAP', 'nametrans', 96 | 'lambda f: f' ) 97 | config.set('Repository Maildir', 'nametrans', 98 | 'lambda f: f + "moo"' ) 99 | OLITestLib.write_config_file(config) 100 | code, res = OLITestLib.run_OLI() 101 | #logging.warn("%s %s "% (code, res)) 102 | # We expect an INFINITE FOLDER CREATION WARNING HERE.... 103 | mismatch = "ERROR: INFINITE FOLDER CREATION DETECTED!" in res 104 | self.assertEqual(mismatch, True, msg="Mismatching nametrans rules did " 105 | "NOT trigger an 'infinite folder generation' error. Output was:\n" 106 | "{0}".format(res)) 107 | # Write out default config file again 108 | OLITestLib.write_config_file() 109 | 110 | 111 | def test_05_createmail(self): 112 | """Create mail in OLItest 1, sync, wipe folder sync 113 | 114 | Currently, this will mean the folder will be recreated 115 | locally. At some point when remote folder deletion is 116 | implemented, this behavior will change.""" 117 | OLITestLib.delete_remote_testfolders() 118 | OLITestLib.delete_maildir('') #Delete all local maildir folders 119 | OLITestLib.create_maildir('INBOX.OLItest') 120 | OLITestLib.create_mail('INBOX.OLItest') 121 | code, res = OLITestLib.run_OLI() 122 | #logging.warn("%s %s "% (code, res)) 123 | self.assertEqual(res, "") 124 | boxes, mails = OLITestLib.count_maildir_mails('') 125 | self.assertTrue((boxes, mails)==(1,1), msg="Expected 1 folders and 1 " 126 | "mails, but sync led to {0} folders and {1} mails".format( 127 | boxes, mails)) 128 | # The local Mail should have been assigned a proper UID now, check! 129 | uids = OLITestLib.get_maildir_uids('INBOX.OLItest') 130 | self.assertFalse (None in uids, msg = "All mails should have been "+ \ 131 | "assigned the IMAP's UID number, but {0} messages had no valid ID "\ 132 | .format(len([None for x in uids if x==None]))) 133 | 134 | def test_06_createfolders(self): 135 | """Test if createfolders works as expected 136 | 137 | Create a local Maildir, then sync with remote "createfolders" 138 | disabled. Delete local Maildir and sync. We should have no new 139 | local maildir then. TODO: Rewrite this test to directly test 140 | and count the remote folders when the helper functions have 141 | been written""" 142 | config = OLITestLib.get_default_config() 143 | config.set('Repository IMAP', 'createfolders', 144 | 'False' ) 145 | OLITestLib.write_config_file(config) 146 | 147 | # delete all remote and local testfolders 148 | OLITestLib.delete_remote_testfolders() 149 | OLITestLib.delete_maildir('') 150 | OLITestLib.create_maildir('INBOX.OLItest') 151 | code, res = OLITestLib.run_OLI() 152 | #logging.warn("%s %s "% (code, res)) 153 | self.assertEqual(res, "") 154 | OLITestLib.delete_maildir('INBOX.OLItest') 155 | code, res = OLITestLib.run_OLI() 156 | self.assertEqual(res, "") 157 | boxes, mails = OLITestLib.count_maildir_mails('') 158 | self.assertTrue((boxes, mails)==(0,0), msg="Expected 0 folders and 0 " 159 | "mails, but sync led to {} folders and {} mails".format( 160 | boxes, mails)) 161 | -------------------------------------------------------------------------------- /test/tests/test_02_MappedIMAP.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2012- Sebastian Spaeth & contributors 2 | # 3 | # This program is free software; you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation; either version 2 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software 15 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 16 | import random 17 | import unittest 18 | import logging 19 | import os, sys 20 | from test.OLItest import OLITestLib 21 | 22 | # Things need to be setup first, usually setup.py initializes everything. 23 | # but if e.g. called from command line, we take care of default values here: 24 | if not OLITestLib.cred_file: 25 | OLITestLib(cred_file='./test/credentials.conf', cmd='./offlineimap.py') 26 | 27 | 28 | def setUpModule(): 29 | logging.info("Set Up test module %s" % __name__) 30 | tdir = OLITestLib.create_test_dir(suffix=__name__) 31 | 32 | def tearDownModule(): 33 | logging.info("Tear Down test module") 34 | OLITestLib.delete_test_dir() 35 | 36 | #Stuff that can be used 37 | #self.assertEqual(self.seq, range(10)) 38 | # should raise an exception for an immutable sequence 39 | #self.assertRaises(TypeError, random.shuffle, (1,2,3)) 40 | #self.assertTrue(element in self.seq) 41 | #self.assertFalse(element in self.seq) 42 | 43 | class TestBasicFunctions(unittest.TestCase): 44 | #@classmethod 45 | #def setUpClass(cls): 46 | #This is run before all tests in this class 47 | # cls._connection = createExpensiveConnectionObject() 48 | 49 | #@classmethod 50 | #This is run after all tests in this class 51 | #def tearDownClass(cls): 52 | # cls._connection.destroy() 53 | 54 | # This will be run before each test 55 | #def setUp(self): 56 | # self.seq = range(10) 57 | 58 | def test_01_MappedImap(self): 59 | """Tests if a MappedIMAP sync can be invoked without exceptions 60 | 61 | Cleans existing remote test folders. Then syncs all "OLItest* 62 | (specified in the default config) to our local IMAP (Gmail). The 63 | result should be 0 folders and 0 mails.""" 64 | pass #TODO 65 | #OLITestLib.delete_remote_testfolders() 66 | #code, res = OLITestLib.run_OLI() 67 | #self.assertEqual(res, "") 68 | #boxes, mails = OLITestLib.count_maildir_mails('') 69 | #self.assertTrue((boxes, mails)==(0,0), msg="Expected 0 folders and 0" 70 | # "mails, but sync led to {} folders and {} mails".format( 71 | # boxes, mails)) 72 | -------------------------------------------------------------------------------- /tests/.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | 3 | -------------------------------------------------------------------------------- /tests/create_conf_file.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python 2 | # Copyright 2018 Espace LLC/espacenetworks.com. Written by @chris001. 3 | # This must be run from the main directory of the offlineimap project. 4 | # Typically this script will be run by Travis to create the config files needed for running the automated tests. 5 | # python ./tests/create_conf_file.py 6 | # Input: Seven shell environment variables. 7 | # Output: it writes the config settings to "filename" (./oli-travis.conf) and "additionalfilename" (./test/credentials.conf). 8 | # "filename" is used by normal run of ./offlineimap -c ./oli-travis.conf , "additionalfilename" is used by "pytest". 9 | # They are the same conf file, copie to two different locations for convenience. 10 | 11 | import os 12 | import shutil 13 | try: 14 | import ConfigParser 15 | Config = ConfigParser.ConfigParser() 16 | except ImportError: 17 | import configparser 18 | Config = configparser.ConfigParser() 19 | 20 | filename = "./oli-travis.conf" 21 | additionalfilename = "./test/credentials.conf" # for the 'pytest' which automatically finds and runs the unittests. 22 | 23 | #TODO: detect OS we running on now, and set sslcacertfile location accordingly. 24 | sslcacertfile = "/etc/pki/tls/cert.pem" # CentOS 7 25 | sslcacertfile = "" # TODO: https://gist.github.com/1stvamp/2158128 Current Mac OSX now must download the cacertfile. 26 | sslcacertfile = "/etc/ssl/certs/ca-certificates.crt" # Ubuntu Trusty 14.04 (Travis linux test container 2018.) 27 | if os.environ["TRAVIS_OS_NAME"] == "osx": 28 | sslcacertfile = os.environ["OSX_BREW_SSLCACERTFILE"] 29 | 30 | # lets create that config file. 31 | cfgfile = open(filename,'w') 32 | 33 | # add the settings to the structure of the file, and lets write it out. 34 | sect = 'general' 35 | Config.add_section(sect) 36 | Config.set(sect,'accounts','Test') 37 | Config.set(sect,'maxsyncaccounts', '1') 38 | 39 | sect = 'Account Test' 40 | Config.add_section(sect) 41 | Config.set(sect,'localrepository','IMAP') # Outlook. 42 | Config.set(sect,'remoterepository', 'Gmail') 43 | 44 | ### "Repository IMAP" is hardcoded in test/OLItest/TestRunner.py it should dynamically get the Repository names but it doesn't. 45 | sect = 'Repository IMAP' # Outlook. 46 | Config.add_section(sect) 47 | Config.set(sect,'type','IMAP') 48 | Config.set(sect,'remotehost', 'imap-mail.outlook.com') 49 | Config.set(sect,'remoteport', '993') 50 | Config.set(sect,'auth_mechanisms', os.environ["OUTLOOK_AUTH"]) 51 | Config.set(sect,'ssl', 'True') 52 | #Config.set(sect,'tls_level', 'tls_compat') #Default is 'tls_compat'. 53 | #Config.set(sect,'ssl_version', 'tls1_2') # Leave this unset. Will auto select between tls1_1 and tls1_2 for tls_secure. 54 | Config.set(sect,'sslcacertfile', sslcacertfile) 55 | Config.set(sect,'remoteuser', os.environ["secure_outlook_email_address"]) 56 | Config.set(sect,'remotepass', os.environ["secure_outlook_email_pw"]) 57 | Config.set(sect,'createfolders', 'True') 58 | Config.set(sect,'folderfilter', 'lambda f: f not in ["Inbox", "[Gmail]/All Mail"]') #Capitalization of Inbox INBOX was causing runtime failure. 59 | #Config.set(sect,'folderfilter', 'lambda f: f not in ["[Gmail]/All Mail"]') 60 | 61 | 62 | ### "Repository Gmail" is also hardcoded into test/OLItest/TestRunner.py 63 | sect = 'Repository Gmail' 64 | Config.add_section(sect) 65 | Config.set(sect,'type', 'Gmail') 66 | Config.set(sect,'remoteport', '993') 67 | Config.set(sect,'auth_mechanisms', os.environ["GMAIL_AUTH"]) 68 | Config.set(sect,'oauth2_client_id', os.environ["secure_gmail_oauth2_client_id"]) 69 | Config.set(sect,'oauth2_client_secret', os.environ["secure_gmail_oauth2_client_secret"]) 70 | Config.set(sect,'oauth2_refresh_token', os.environ["secure_gmail_oauth2_refresh_token"]) 71 | Config.set(sect,'remoteuser', os.environ["secure_gmail_email_address"]) 72 | Config.set(sect,'ssl', 'True') 73 | #Config.set(sect,'tls_level', 'tls_compat') 74 | #Config.set(sect,'ssl_version', 'tls1_2') 75 | Config.set(sect,'sslcacertfile', sslcacertfile) 76 | Config.set(sect,'createfolders', 'True') 77 | Config.set(sect,'folderfilter', 'lambda f: f not in ["INBOX", "[Gmail]/All Mail"]') 78 | 79 | Config.write(cfgfile) 80 | cfgfile.close() 81 | 82 | shutil.copy(filename, additionalfilename) 83 | -------------------------------------------------------------------------------- /tests/requirements.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | pytest-cov 3 | coverage 4 | codecov 5 | --------------------------------------------------------------------------------