├── .coveragerc ├── .github ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .travis.yml ├── BACKERS.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.rst ├── COPYING ├── Changelog.maint.md ├── Changelog.md ├── Dockerfile ├── MAINTAINERS.rst ├── MANIFEST.in ├── Makefile ├── README.md ├── TODO.rst ├── bin └── offlineimap ├── contrib ├── README.md ├── helpers.py ├── internet-urllib3.py ├── nametrans_imap_to_utf8.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 ├── 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_utils.py │ └── stacktrace.py ├── pyproject.toml ├── requirements-certify.txt ├── requirements-cygwin.txt ├── requirements-kerberos.txt ├── requirements-keyring.txt ├── 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/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 | offlineimap.egg-info/ 14 | *.pyc 15 | offlineimap.1 16 | offlineimapui.7 17 | # Editors/IDEs 18 | tags 19 | # Generated conf files for Travis-CI tests. 20 | oli-travis.conf 21 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /BACKERS.md: -------------------------------------------------------------------------------- 1 | ### Many thanks to our wonderful Sponsors! Your generous support helps us maintain OfflineIMAP. 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | # SILVER sponsors 11 | 12 | [![silver-zeronet-logo]][silver-zeronet-link] 13 | 14 | ![Silver Sponsors][silver] 15 | 16 | # BRONZE sponsors 17 | 18 | ![Bronze Sponsors][bronze] 19 | 20 | 21 | [silver]: https://opencollective.com/offlineimap-organization/tiers/silver-sponsor.svg "Our Silver Sponsors" 22 | [silver-zeronet-logo]: https://github.com/OfflineIMAP/offlineimap.github.io/raw/master/assets/img/sponsors/zeronet.svg 23 | [silver-zeronet-link]: https://zeronet.co.nz/ "Zeronet - naturally fast internet" 24 | [bronze]: https://opencollective.com/offlineimap-organization/tiers/bronze-sponsor.svg "Our Bronze Sponsors" 25 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3 as base 2 | 3 | MAINTAINER Ben Yanke 4 | 5 | ############## 6 | # Main setup stage 7 | ############## 8 | 9 | # Copy in deps first, to improve build caching 10 | COPY requirements.txt /app-src/requirements.txt 11 | WORKDIR /app-src 12 | 13 | # Get kerberos deps before pip deps can be fetched 14 | #RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -qq -y krb5-user -y && rm -rf /var/lib/apt/lists/* 15 | 16 | # Get latest pip and dependencies 17 | RUN /usr/local/bin/python3 -m pip install --upgrade pip && pip install -r requirements.txt 18 | 19 | # Copy in rest of the code after deps are in place 20 | COPY . /app-src 21 | 22 | # Install the app 23 | RUN /usr/local/bin/python3 setup.py install 24 | 25 | ############## 26 | # Run tests in a throwaway stage 27 | # if tests are added later, run them here 28 | ############## 29 | #FROM base as test 30 | #WORKDIR /app-src 31 | #RUN /usr/local/bin/python3 setup.py test 32 | 33 | ############## 34 | # Throw away the test stage, revert back to base stage before push 35 | ############## 36 | 37 | FROM base 38 | WORKDIR /root 39 | CMD ["/usr/local/bin/offlineimap"] 40 | # reads from /root/.offlineimaprc by default - mount this in for running 41 | -------------------------------------------------------------------------------- /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 | - Rodolfo García Peñas (kix) 64 | - email: kix at kix.es 65 | - github: thekix 66 | - system: Linux 67 | 68 | 69 | Testers 70 | ======= 71 | 72 | - Abdó Roig-Maranges 73 | - Ben Boeckel 74 | - Chris Coleman 75 | - Darshit Shah 76 | - Eygene Ryabinkin 77 | - Igor Almeida 78 | - Ilias Tsitsimpis 79 | - "J" 80 | - Łukasz Żarnowiecki 81 | - Nicolas Sebrecht 82 | - Remi Locherer 83 | - Rodolfo García Peñas (kix) 84 | 85 | 86 | Maintainers 87 | =========== 88 | 89 | - Rodolfo García Peñas (kix) 90 | - Eygene Ryabinkin 91 | - Sebastian Spaeth 92 | - Nicolas Sebrecht 93 | - Chris Coleman 94 | 95 | 96 | Github 97 | ------ 98 | 99 | - Eygene Ryabinkin 100 | - Sebastian Spaeth 101 | - Nicolas Sebrecht 102 | 103 | 104 | Mailing List 105 | ------------ 106 | 107 | - Eygene Ryabinkin 108 | - Sebastian Spaeth 109 | - Nicolas Sebrecht 110 | 111 | 112 | Twitter 113 | ------- 114 | 115 | - Nicolas Sebrecht 116 | 117 | 118 | Pypi 119 | ---- 120 | 121 | - Nicolas Sebrecht 122 | - Sebastian Spaeth 123 | -------------------------------------------------------------------------------- /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]: https://github.com/OfflineIMAP/offlineimap 10 | [offlineimap3]: https://github.com/OfflineIMAP/offlineimap3 11 | [website]: https://www.offlineimap.org 12 | [wiki]: https://github.com/OfflineIMAP/offlineimap/wiki 13 | [blog]: https://www.offlineimap.org/posts.html 14 | 15 | Links: 16 | * Official github code repository 17 | * for Python 2: [offlineimap] 18 | * for Python 3: [offlineimap3] 19 | * Website: [website] 20 | * Wiki: [wiki] 21 | * Blog: [blog] 22 | 23 | # OfflineIMAP 24 | 25 | ***"Get the emails where you need them."*** 26 | 27 | 28 | ## Description 29 | 30 | OfflineIMAP is software that downloads your email mailbox(es) as **local 31 | Maildirs**. OfflineIMAP will synchronize both sides via *IMAP*. 32 | 33 | 34 | ## Why should I use OfflineIMAP? 35 | 36 | IMAP's main downside is that you have to **trust** your email provider to 37 | not lose your email. While certainly unlikely, it's not impossible. 38 | With OfflineIMAP, you can download your Mailboxes and make you own backups of 39 | your [Maildir](https://en.wikipedia.org/wiki/Maildir). 40 | 41 | This allows reading your email offline without the need for your mail 42 | reader (MUA) to support IMAP operations. Need an attachment from a 43 | message without internet connection? No problem, the message is still there. 44 | 45 | 46 | ## Project status and future 47 | 48 | OfflineIMAP, using Python 3, is based on OfflineIMAP for Python 2. 49 | Currently we are updating the source code. These changes should not affect 50 | the user (documentation, configuration files,... are the same) but some 51 | links or packages could refer to the Python 2 version. In that case, please 52 | open an issue. 53 | 54 | 55 | ## License 56 | 57 | GNU General Public License v2. 58 | 59 | 60 | ## Downloads 61 | 62 | You should first check if your distribution already packages OfflineIMAP for you. 63 | Downloads releases as [tarball or zipball](https://github.com/OfflineIMAP/offlineimap3/tags). 64 | 65 | If you are running Linux/BSD, you can install offlineimap with: 66 | 67 | - Debian and Ubuntu `apt install offlineimap3` 68 | - openSUSE `zypper install offlineimap` 69 | - Fedora `dnf install offlineimap` 70 | - FreeBSD `pkg search offlineimap3`, and install the python versioned package, `pkg install py311-offlineimap3` 71 | - Arch Linux: [`pacman -S offlineimap`](https://archlinux.org/packages/extra/any/offlineimap/), or through AUR package [offlineimap3-git](https://aur.archlinux.org/packages/offlineimap3-git/) 72 | - Docker image: `offlineimap/offlineimap:latest` 73 | (note: image not published yet, just an example) 74 | 75 | ## Feedbacks and contributions 76 | 77 | **The user discussions, development, announcements and all the exciting stuff take 78 | place on the mailing list.** While not mandatory to send emails, you can 79 | [subscribe here](http://lists.alioth.debian.org/mailman/listinfo/offlineimap-project). 80 | 81 | Bugs, issues and contributions can be requested to both the mailing list or the 82 | [official Github project][offlineimap3]. Provide the following information: 83 | - system/distribution (with version) 84 | - offlineimap version (`offlineimap -V`) 85 | - Python version 86 | - server name or domain 87 | - CLI options 88 | - Configuration file (offlineimaprc) 89 | - pythonfile (if any) 90 | - Logs, error 91 | - Steps to reproduce the error 92 | 93 | 94 | ## The community 95 | 96 | * OfflineIMAP's main site is the [project page at Github][offlineimap3]. 97 | * There is the [OfflineIMAP community's website][website]. 98 | * And finally, [the wiki][wiki]. 99 | 100 | 101 | ## Requirements & dependencies 102 | 103 | * Python v3+ 104 | * rfc6555 (required) 105 | * imaplib2 >= 3.5 106 | * keyring 107 | * gssapi (optional), for Kerberos authentication 108 | * portalocker (optional), if you need to run offlineimap in Cygwin for Windows 109 | 110 | ## Documentation 111 | 112 | All current and updated documentation is on the [community's website][website]. 113 | 114 | 115 | ### Read documentation locally 116 | 117 | You might want to read the documentation locally. Get the sources of the website. 118 | For the other documentation, run the appropriate make target: 119 | 120 | ```sh 121 | $ ./scripts/get-repository.sh website 122 | $ cd docs 123 | $ make html # Requires rst2html 124 | $ make man # Requires a2x (http://asciidoc.org) 125 | $ make api # Requires sphinx 126 | ``` 127 | -------------------------------------------------------------------------------- /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 python3 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/nametrans_imap_to_utf8.py: -------------------------------------------------------------------------------- 1 | """ 2 | convert_utf7_to_utf8 used in nametrans 3 | 4 | Main code: Rodolfo García Peñas (kix) @thekix 5 | Updated regex by @dnebauer 6 | 7 | Please, check https://github.com/OfflineIMAP/offlineimap3/issues/23 8 | for more info. 9 | """ 10 | import re 11 | 12 | 13 | def convert_utf7_to_utf8(str_imap): 14 | """ 15 | This function converts an IMAP_UTF-7 string object to UTF-8. 16 | It first replaces the ampersand (&) character with plus character (+) 17 | in the cases of UTF-7 character and then decode the string to utf-8. 18 | 19 | If the str_imap string is already UTF-8, return it. 20 | 21 | For example, "abc&AK4-D" is translated to "abc+AK4-D" 22 | and then, to "abc@D" 23 | 24 | Example code: 25 | my_string = "abc&AK4-D" 26 | print(convert_utf7_to_utf8(my_string)) 27 | 28 | Args: 29 | bytes_imap: IMAP UTF7 string 30 | 31 | Returns: UTF-8 string 32 | 33 | Source: https://github.com/OfflineIMAP/offlineimap3/issues/23 34 | 35 | """ 36 | try: 37 | str_utf7 = re.sub(r'&(\w{3}\-)', '+\\1', str_imap) 38 | str_utf8 = str_utf7.encode('utf-8').decode('utf_7') 39 | return str_utf8 40 | except UnicodeDecodeError: 41 | # error decoding because already utf-8, so return original string 42 | return str_imap 43 | 44 | -------------------------------------------------------------------------------- /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(): 18 | def __init__(self): 19 | self.args = None 20 | self.testers = Testers() 21 | self.feedbacks = None 22 | 23 | def _getTestersByFeedback(self): 24 | if self.feedbacks is not None: 25 | return self.feedbacks 26 | 27 | feedbackOk = [] 28 | feedbackNo = [] 29 | 30 | for tester in self.testers.get(): 31 | if tester.getFeedback() is True: 32 | feedbackOk.append(tester) 33 | else: 34 | feedbackNo.append(tester) 35 | 36 | for array in [feedbackOk, feedbackNo]: 37 | array.sort(key=lambda t: t.getName()) 38 | 39 | self.feedbacks = feedbackOk + feedbackNo 40 | 41 | def parseArgs(self): 42 | parser = argparse.ArgumentParser(description='Manage the feedbacks.') 43 | 44 | parser.add_argument('--add', '-a', dest='add_tester', 45 | help='Add tester') 46 | parser.add_argument('--delete', '-d', dest='delete_tester', 47 | type=int, 48 | help='Delete tester NUMBER') 49 | parser.add_argument('--list', '-l', dest='list_all_testers', 50 | action='store_true', 51 | help='List the testers') 52 | parser.add_argument('--switchFeedback', '-s', dest='switch_feedback', 53 | action='store_true', 54 | help='Switch the feedback of a tester') 55 | 56 | self.args = parser.parse_args() 57 | 58 | def run(self): 59 | if self.args.list_all_testers is True: 60 | self.listTesters() 61 | if self.args.switch_feedback is True: 62 | self.switchFeedback() 63 | elif self.args.add_tester: 64 | self.addTester(self.args.add_tester) 65 | elif type(self.args.delete_tester) == int: 66 | self.deleteTester(self.args.delete_tester) 67 | 68 | def addTester(self, strTester): 69 | try: 70 | splitted = strTester.split('<') 71 | name = splitted[0].strip() 72 | email = "<{}".format(splitted[1]).strip() 73 | except Exception as e: 74 | print(e) 75 | print("expected format is: 'Firstname Lastname '") 76 | exit(2) 77 | self.testers.add(name, email) 78 | self.testers.write() 79 | 80 | def deleteTester(self, number): 81 | self.listTesters() 82 | removed = self.feedbacks.pop(number) 83 | self.testers.remove(removed) 84 | 85 | print("New list:") 86 | self.feedbacks = None 87 | self.listTesters() 88 | print(("Removed: {}".format(removed))) 89 | ans = User.request("Save on disk? (s/Q)").lower() 90 | if ans in ['s']: 91 | self.testers.write() 92 | 93 | def listTesters(self): 94 | self._getTestersByFeedback() 95 | 96 | count = 0 97 | for tester in self.feedbacks: 98 | feedback = "ok" 99 | if tester.getFeedback() is not True: 100 | feedback = "no" 101 | print(("{:02d} - {} {}: {}".format( 102 | count, tester.getName(), tester.getEmail(), feedback 103 | ) 104 | )) 105 | count += 1 106 | 107 | def switchFeedback(self): 108 | self._getTestersByFeedback() 109 | msg = "Switch tester: [/s/q]" 110 | 111 | self.listTesters() 112 | number = User.request(msg) 113 | while number.lower() not in ['s', 'save', 'q', 'quit']: 114 | if number == '': 115 | continue 116 | try: 117 | number = int(number) 118 | self.feedbacks[number].switchFeedback() 119 | except (ValueError, IndexError) as e: 120 | print(e) 121 | exit(1) 122 | finally: 123 | self.listTesters() 124 | number = User.request(msg) 125 | if number in ['s', 'save']: 126 | self.testers.write() 127 | self.listTesters() 128 | 129 | def reset(self): 130 | self.testers.reset() 131 | self.testers.write() 132 | 133 | # def updateMailaliases(self): 134 | 135 | 136 | if __name__ == '__main__': 137 | Git.chdirToRepositoryTopLevel() 138 | 139 | app = App() 140 | app.parseArgs() 141 | app.run() 142 | -------------------------------------------------------------------------------- /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 | UPCOMING_FILE = "{}/upcoming.txt".format(CACHEDIR) 18 | UPCOMING_HEADER = "{}/upcoming-header.txt".format(CACHEDIR) 19 | 20 | # Header is like: 21 | # 22 | # Message-Id: <{messageId}> 23 | # Date: {date} 24 | # From: {name} <{email}> 25 | # To: {mailinglist} 26 | # Cc: {ccList} 27 | # Subject: [ANNOUNCE] upcoming offlineimap v{expectedVersion} 28 | # 29 | ## Notes 30 | # 31 | # I think it's time for a new release. 32 | # 33 | # I aim to make the new release in one week, approximately. If you'd like more 34 | # time, please let me know. ,-) 35 | # 36 | # Please, send me a mail to confirm it works for you. This will be written in the 37 | # release notes and the git logs. 38 | # 39 | # 40 | ## Authors 41 | # 42 | 43 | 44 | if __name__ == '__main__': 45 | offlineimapInfo = OfflineimapInfo() 46 | 47 | print("Will read headers from {}".format(UPCOMING_HEADER)) 48 | Git.chdirToRepositoryTopLevel() 49 | oVersion = offlineimapInfo.getVersion() 50 | ccList = Testers.listTestersInTeam() 51 | authors = Git.getAuthorsList(oVersion) 52 | for author in authors: 53 | email = author.getEmail() 54 | if email not in ccList: 55 | ccList.append(email) 56 | 57 | with open(UPCOMING_FILE, 'w') as upcoming, \ 58 | open(UPCOMING_HEADER, 'r') as fd_header: 59 | header = {} 60 | 61 | header['messageId'] = Git.buildMessageId() 62 | header['date'] = Git.buildDate() 63 | header['name'], header['email'] = Git.getLocalUser() 64 | header['mailinglist'] = MAILING_LIST 65 | header['expectedVersion'] = User.request("Expected new version?") 66 | header['ccList'] = ", ".join(ccList) 67 | 68 | upcoming.write(fd_header.read().format(**header).lstrip()) 69 | upcoming.write(Git.getShortlog(oVersion)) 70 | 71 | upcoming.write("\n\n# Diffstat\n\n") 72 | upcoming.write(Git.getDiffstat(oVersion)) 73 | upcoming.write("\n\n\n-- \n{}\n".format(Git.getLocalUser()[0])) 74 | 75 | system("{} {}".format(EDITOR, UPCOMING_FILE)) 76 | print("{} written".format(UPCOMING_FILE)) 77 | -------------------------------------------------------------------------------- /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/OfflineIMAP/offlineimap3/db347452273bb0f1b1a8ea952f6fb46cf95fedbf/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/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 python3 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 | from offlineimap import OfflineImap 20 | 21 | oi = OfflineImap() 22 | oi.run() 23 | -------------------------------------------------------------------------------- /offlineimap/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ['OfflineImap'] 2 | 3 | __productname__ = 'OfflineIMAP' 4 | # Expecting trailing "-rcN" or "" for stable releases. 5 | __version__ = "8.0.0" 6 | __copyright__ = "Copyright 2002-2021 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/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 | """ 2 | Folder Module of offlineimap 3 | """ 4 | from . import Base, Gmail, IMAP, Maildir, LocalStatus, UIDMaps 5 | -------------------------------------------------------------------------------- /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 | 11 | def set_options(source): 12 | """Sets the source for options variable.""" 13 | 14 | options.set_source(source) 15 | -------------------------------------------------------------------------------- /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 importlib.util 20 | 21 | 22 | class LocalEval: 23 | """Here is a powerfull but very dangerous option, of course.""" 24 | 25 | def __init__(self, path=None): 26 | self.namespace = {} 27 | 28 | if path is not None: 29 | # FIXME: limit opening files owned by current user with rights set 30 | # to fixed mode 644. 31 | importlib.machinery.SOURCE_SUFFIXES.append('') # empty string to allow any file 32 | spec = importlib.util.spec_from_file_location('', path) 33 | module = importlib.util.module_from_spec(spec) 34 | spec.loader.exec_module(module) 35 | for attr in dir(module): 36 | self.namespace[attr] = getattr(module, attr) 37 | 38 | def eval(self, text, namespace=None): 39 | names = {} 40 | names.update(self.namespace) 41 | if namespace is not None: 42 | names.update(namespace) 43 | return eval(text, names) 44 | -------------------------------------------------------------------------------- /offlineimap/repository/Gmail.py: -------------------------------------------------------------------------------- 1 | """ 2 | Gmail IMAP repository support 3 | Copyright (C) 2008-2016 Riccardo Murri & 4 | contributors 5 | 6 | This program is free software; you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation; either version 2 of the License, or 9 | (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program; if not, write to the Free Software 18 | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 19 | """ 20 | 21 | from offlineimap.repository.IMAP import IMAPRepository 22 | from offlineimap import folder, OfflineImapError 23 | 24 | 25 | class GmailRepository(IMAPRepository): 26 | """Gmail IMAP repository. 27 | 28 | This class just has default settings for GMail's IMAP service. So 29 | you can do 'type = Gmail' instead of 'type = IMAP' and skip 30 | specifying the hostname, port etc. See 31 | http://mail.google.com/support/bin/answer.py?answer=78799&topic=12814 32 | for the values we use.""" 33 | def __init__(self, reposname, account): 34 | """Initialize a GmailRepository object.""" 35 | IMAPRepository.__init__(self, reposname, account) 36 | 37 | def gethost(self): 38 | """Return the server name to connect to. 39 | 40 | We first check the usual IMAP settings, and then fall back to 41 | imap.gmail.com if nothing is specified.""" 42 | try: 43 | return super().gethost() 44 | except OfflineImapError: 45 | # Nothing was configured, cache and return hardcoded 46 | # one. See the parent class (IMAPRepository) for how this 47 | # cache is used. 48 | self._host = "imap.gmail.com" 49 | return self._host 50 | 51 | def getoauth2_request_url(self): 52 | """Return the OAuth URL to request tokens from. 53 | 54 | We first check the usual OAuth settings, and then fall back to 55 | https://accounts.google.com/o/oauth2/token if nothing is 56 | specified.""" 57 | 58 | url = super().getoauth2_request_url() 59 | if url is None: 60 | # Nothing was configured, use a hardcoded one. 61 | url = "https://accounts.google.com/o/oauth2/token" 62 | 63 | self.setoauth2_request_url(url) 64 | 65 | return self.oauth2_request_url 66 | 67 | def getport(self): 68 | """Return the port number to connect to. 69 | 70 | This Gmail implementation first checks for the usual IMAP settings 71 | and falls back to 993 if nothing is specified.""" 72 | 73 | port = super().getport() 74 | 75 | if port is not None: 76 | return port 77 | 78 | return 993 79 | 80 | def getssl(self): 81 | ssl = self.getconfboolean('ssl', None) 82 | 83 | if ssl is None: 84 | # Nothing was configured, return our default setting for 85 | # GMail. Maybe this should look more similar to gethost & 86 | # we could just rely on the global "ssl = yes" default. 87 | return True 88 | 89 | return ssl 90 | 91 | def getpreauthtunnel(self): 92 | return None 93 | 94 | def getfolder(self, foldername, decode=True): 95 | return self.getfoldertype()(self.imapserver, foldername, 96 | self, decode) 97 | 98 | def getfoldertype(self): 99 | return folder.Gmail.GmailFolder 100 | 101 | def gettrashfolder(self): 102 | """ 103 | Where deleted mail should be moved 104 | """ 105 | return self.getconf('trashfolder', '[Gmail]/Trash') 106 | 107 | def getspamfolder(self): 108 | """ 109 | Depending on the IMAP settings (Settings -> Forwarding and 110 | POP/IMAP -> IMAP Access -> "When I mark a message in IMAP as 111 | deleted") GMail might also deletes messages upon EXPUNGE in 112 | the Spam folder. 113 | """ 114 | return self.getconf('spamfolder', '[Gmail]/Spam') 115 | -------------------------------------------------------------------------------- /offlineimap/repository/GmailMaildir.py: -------------------------------------------------------------------------------- 1 | """ 2 | Maildir repository support 3 | Copyright (C) 2002-2015 John Goerzen & contributors 4 | 5 | 6 | This program is free software; you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation; either version 2 of the License, or 9 | (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program; if not, write to the Free Software 18 | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 19 | """ 20 | 21 | from offlineimap.repository.Maildir import MaildirRepository 22 | from offlineimap.folder.GmailMaildir import GmailMaildirFolder 23 | 24 | 25 | class GmailMaildirRepository(MaildirRepository): 26 | """ 27 | GMail Maildir Repository Class 28 | """ 29 | def getfoldertype(self): 30 | return GmailMaildirFolder 31 | -------------------------------------------------------------------------------- /offlineimap/repository/LocalStatus.py: -------------------------------------------------------------------------------- 1 | """ 2 | Local status cache repository support 3 | Copyright (C) 2002-2017 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 | 21 | from offlineimap.folder.LocalStatus import LocalStatusFolder 22 | from offlineimap.folder.LocalStatusSQLite import LocalStatusSQLiteFolder 23 | from offlineimap.repository.Base import BaseRepository 24 | from offlineimap.error import OfflineImapError 25 | 26 | 27 | class LocalStatusRepository(BaseRepository): 28 | """ 29 | Local Status Repository Class, child of Base Repository Class 30 | """ 31 | def __init__(self, reposname, account): 32 | BaseRepository.__init__(self, reposname, account) 33 | 34 | # class and root for all backends. 35 | self.backends = {} 36 | self.backends['sqlite'] = { 37 | 'class': LocalStatusSQLiteFolder, 38 | 'root': os.path.join(account.getaccountmeta(), 'LocalStatus-sqlite') 39 | } 40 | self.backends['plain'] = { 41 | 'class': LocalStatusFolder, 42 | 'root': os.path.join(account.getaccountmeta(), 'LocalStatus') 43 | } 44 | 45 | if self.account.getconf('status_backend', None) is not None: 46 | raise OfflineImapError( 47 | "the 'status_backend' configuration option is not supported" 48 | " anymore; please, remove this configuration option.", 49 | OfflineImapError.ERROR.REPO 50 | ) 51 | # Set class and root for sqlite. 52 | self.setup_backend('sqlite') 53 | 54 | if not os.path.exists(self.root): 55 | os.mkdir(self.root, 0o700) 56 | 57 | # self._folders is a dict of name:LocalStatusFolders(). 58 | self._folders = {} 59 | 60 | def _instanciatefolder(self, foldername): 61 | return self.LocalStatusFolderClass(foldername, self) # Instantiate. 62 | 63 | def setup_backend(self, backend): 64 | """ 65 | Setup the backend. 66 | 67 | Args: 68 | backend: backend to use 69 | 70 | Returns: None 71 | 72 | """ 73 | if backend in list(self.backends.keys()): 74 | self._backend = backend 75 | self.root = self.backends[backend]['root'] 76 | self.LocalStatusFolderClass = self.backends[backend]['class'] 77 | 78 | def import_other_backend(self, folder): 79 | """ 80 | Import other backend 81 | 82 | Args: 83 | folder: folder 84 | 85 | Returns: None 86 | 87 | """ 88 | for bkend, dic in list(self.backends.items()): 89 | # Skip folder's own type. 90 | if dic['class'] == type(folder): 91 | continue 92 | 93 | repobk = LocalStatusRepository(self.name, self.account) 94 | repobk.setup_backend(bkend) # Fake the backend. 95 | folderbk = dic['class'](folder.name, repobk) 96 | 97 | # If backend contains data, import it to folder. 98 | if not folderbk.isnewfolder(): 99 | self.ui._msg("Migrating LocalStatus cache from %s to %s " 100 | "status folder for %s:%s" % 101 | (bkend, self._backend, self.name, folder.name)) 102 | 103 | folderbk.cachemessagelist() 104 | folder.messagelist = folderbk.messagelist 105 | folder.saveall() 106 | break 107 | 108 | def getsep(self): 109 | return '.' 110 | 111 | def makefolder(self, foldername): 112 | """Create a LocalStatus Folder.""" 113 | 114 | if self.account.dryrun: 115 | return # Bail out in dry-run mode. 116 | 117 | # Create an empty StatusFolder. 118 | folder = self._instanciatefolder(foldername) 119 | # First delete any existing data to make sure we won't consider obsolete 120 | # data. This might happen if the user removed the folder (maildir) and 121 | # it is re-created afterwards. 122 | folder.purge() 123 | folder.openfiles() 124 | folder.save() 125 | folder.closefiles() 126 | 127 | # Invalidate the cache. 128 | self.forgetfolders() 129 | 130 | def getfolder(self, foldername): 131 | """Return the Folder() object for a foldername. 132 | 133 | Caller must call closefiles() on the folder when done.""" 134 | 135 | if foldername in self._folders: 136 | return self._folders[foldername] 137 | 138 | folder = self._instanciatefolder(foldername) 139 | 140 | # If folder is empty, try to import data from an other backend. 141 | if folder.isnewfolder(): 142 | self.import_other_backend(folder) 143 | 144 | self._folders[foldername] = folder 145 | return folder 146 | 147 | def getfolders(self): 148 | """Returns a list of all cached folders. 149 | 150 | Does nothing for this backend. We mangle the folder file names 151 | (see getfolderfilename) so we can not derive folder names from 152 | the file names that we have available. TODO: need to store a 153 | list of folder names somehow?""" 154 | 155 | def forgetfolders(self): 156 | """Forgets the cached list of folders, if any. Useful to run 157 | after a sync run.""" 158 | 159 | self._folders = {} 160 | -------------------------------------------------------------------------------- /offlineimap/repository/__init__.py: -------------------------------------------------------------------------------- 1 | """ 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 | from sys import exc_info 19 | from configparser import NoSectionError 20 | from offlineimap.repository.IMAP import IMAPRepository, MappedIMAPRepository 21 | from offlineimap.repository.Gmail import GmailRepository 22 | from offlineimap.repository.Maildir import MaildirRepository 23 | from offlineimap.repository.GmailMaildir import GmailMaildirRepository 24 | from offlineimap.repository.LocalStatus import LocalStatusRepository 25 | from offlineimap.error import OfflineImapError 26 | 27 | 28 | class Repository: 29 | """Abstract class that returns the correct Repository type 30 | instance based on 'account' and 'reqtype', e.g. a 31 | class:`ImapRepository` instance.""" 32 | 33 | def __new__(cls, account, reqtype): 34 | """ 35 | :param account: :class:`Account` 36 | :param reqtype: 'remote', 'local', or 'status'""" 37 | 38 | if reqtype == 'remote': 39 | name = account.getconf('remoterepository') 40 | # We don't support Maildirs on the remote side. 41 | typemap = {'IMAP': IMAPRepository, 42 | 'Gmail': GmailRepository} 43 | 44 | elif reqtype == 'local': 45 | name = account.getconf('localrepository') 46 | typemap = {'IMAP': MappedIMAPRepository, 47 | 'Maildir': MaildirRepository, 48 | 'GmailMaildir': GmailMaildirRepository} 49 | 50 | elif reqtype == 'status': 51 | # create and return a LocalStatusRepository. 52 | name = account.getconf('localrepository') 53 | return LocalStatusRepository(name, account) 54 | 55 | else: 56 | errstr = "Repository type %s not supported" % reqtype 57 | raise OfflineImapError(errstr, OfflineImapError.ERROR.REPO) 58 | 59 | # Get repository type. 60 | config = account.getconfig() 61 | try: 62 | repostype = config.get('Repository ' + name, 'type').strip() 63 | except NoSectionError as exc: 64 | errstr = ("Could not find section '%s' in configuration. Required " 65 | "for account '%s'." % ('Repository %s' % name, account)) 66 | raise OfflineImapError(errstr, OfflineImapError.ERROR.REPO, 67 | exc_info()[2]) from exc 68 | 69 | try: 70 | repo = typemap[repostype] 71 | except KeyError as exc: 72 | errstr = "'%s' repository not supported for '%s' repositories." % \ 73 | (repostype, reqtype) 74 | raise OfflineImapError(errstr, OfflineImapError.ERROR.REPO, 75 | exc_info()[2]) from exc 76 | 77 | return repo(name, account) 78 | 79 | def __init__(self, account, reqtype): 80 | """Load the correct Repository type and return that. The 81 | __init__ of the corresponding Repository class will be 82 | executed instead of this stub 83 | 84 | :param account: :class:`Account` 85 | :param reqtype: 'remote', 'local', or 'status' 86 | """ 87 | -------------------------------------------------------------------------------- /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 19 | from queue import Queue, Empty 20 | import traceback 21 | from offlineimap.ui import getglobalui 22 | 23 | STOP_MONITOR = 'STOP_MONITOR' 24 | 25 | 26 | # General utilities 27 | 28 | 29 | def semaphorereset(semaphore, originalstate): 30 | """Block until `semaphore` gets back to its original state, ie all acquired 31 | resources have been released.""" 32 | 33 | for i in range(originalstate): 34 | semaphore.acquire() 35 | # Now release these. 36 | for i in range(originalstate): 37 | semaphore.release() 38 | 39 | 40 | class accountThreads: 41 | """Store the list of all threads in the software so it can be used to find out 42 | what's running and what's not.""" 43 | 44 | def __init__(self): 45 | self.lock = Lock() 46 | self.list = [] 47 | 48 | def add(self, thread): 49 | with self.lock: 50 | self.list.append(thread) 51 | 52 | def remove(self, thread): 53 | with self.lock: 54 | self.list.remove(thread) 55 | 56 | def pop(self): 57 | with self.lock: 58 | if len(self.list) < 1: 59 | return None 60 | return self.list.pop() 61 | 62 | def wait(self): 63 | while True: 64 | thread = self.pop() 65 | if thread is None: 66 | break 67 | thread.join() 68 | 69 | 70 | ###################################################################### 71 | # Exit-notify threads 72 | ###################################################################### 73 | 74 | exitedThreads = Queue() 75 | 76 | 77 | def monitor(): 78 | """An infinite "monitoring" loop watching for finished ExitNotifyThread's. 79 | 80 | This one is supposed to run in the main thread. 81 | """ 82 | 83 | global exitedThreads 84 | ui = getglobalui() 85 | 86 | while True: 87 | # Loop forever and call 'callback' for each thread that exited 88 | try: 89 | # We need a timeout in the get() call, so that ctrl-c can throw a 90 | # SIGINT (http://bugs.python.org/issue1360). A timeout with empty 91 | # Queue will raise `Empty`. 92 | # 93 | # ExitNotifyThread add themselves to the exitedThreads queue once 94 | # they are done (normally or with exception). 95 | thread = exitedThreads.get(True, 60) 96 | # Request to abort when callback returns True. 97 | 98 | if thread.exit_exception is not None: 99 | if isinstance(thread.exit_exception, SystemExit): 100 | # Bring a SystemExit into the main thread. 101 | # Do not send it back to UI layer right now. 102 | # Maybe later send it to ui.terminate? 103 | raise SystemExit 104 | ui.threadException(thread) # Expected to terminate the program. 105 | # Should never hit this line. 106 | raise AssertionError("thread has 'exit_exception' set to" 107 | " '%s' [%s] but this value is unexpected" 108 | " and the ui did not stop the program." % 109 | (repr(thread.exit_exception), type(thread.exit_exception))) 110 | 111 | # Only the monitor thread has this exit message set. 112 | elif thread.exit_message == STOP_MONITOR: 113 | break # Exit the loop here. 114 | else: 115 | ui.threadExited(thread) 116 | except Empty: 117 | pass 118 | 119 | 120 | class ExitNotifyThread(Thread): 121 | """This class is designed to alert a "monitor" to the fact that a 122 | thread has exited and to provide for the ability for it to find out 123 | why. All instances are made daemon threads (.daemon=True, so we 124 | bail out when the mainloop dies. 125 | 126 | The thread can set instance variables self.exit_message for a human 127 | readable reason of the thread exit. 128 | 129 | There is one instance of this class at runtime. The main thread waits for 130 | the monitor to end.""" 131 | 132 | def __init__(self, *args, **kwargs): 133 | super(ExitNotifyThread, self).__init__(*args, **kwargs) 134 | # These are all child threads that are supposed to go away when 135 | # the main thread is killed. 136 | self.daemon = True 137 | self.exit_message = None 138 | self._exit_exc = None 139 | self._exit_stacktrace = None 140 | 141 | def run(self): 142 | """Allow profiling of a run and store exceptions.""" 143 | 144 | global exitedThreads 145 | try: 146 | Thread.run(self) 147 | except Exception as e: 148 | # Thread exited with Exception, store it 149 | tb = traceback.format_exc() 150 | self.set_exit_exception(e, tb) 151 | 152 | exitedThreads.put(self, True) 153 | 154 | def set_exit_exception(self, exc, st=None): 155 | """Sets Exception and stacktrace of a thread, so that other 156 | threads can query its exit status""" 157 | 158 | self._exit_exc = exc 159 | self._exit_stacktrace = st 160 | 161 | @property 162 | def exit_exception(self): 163 | """Returns the cause of the exit, one of: 164 | Exception() -- the thread aborted with this exception 165 | None -- normal termination.""" 166 | 167 | return self._exit_exc 168 | 169 | @property 170 | def exit_stacktrace(self): 171 | """Returns a string representing the stack trace if set""" 172 | 173 | return self._exit_stacktrace 174 | 175 | 176 | ###################################################################### 177 | # Instance-limited threads 178 | ###################################################################### 179 | 180 | limitedNamespaces = {} 181 | 182 | 183 | def initInstanceLimit(limitNamespace, instancemax): 184 | """Initialize the instance-limited thread implementation. 185 | 186 | Run up to intancemax threads for the given limitNamespace. This allows to 187 | honor maxsyncaccounts and maxconnections.""" 188 | 189 | global limitedNamespaces 190 | 191 | if limitNamespace not in limitedNamespaces: 192 | limitedNamespaces[limitNamespace] = BoundedSemaphore(instancemax) 193 | 194 | 195 | class InstanceLimitedThread(ExitNotifyThread): 196 | def __init__(self, limitNamespace, *args, **kwargs): 197 | self.limitNamespace = limitNamespace 198 | super(InstanceLimitedThread, self).__init__(*args, **kwargs) 199 | 200 | def start(self): 201 | global limitedNamespaces 202 | 203 | # Will block until the semaphore has free slots. 204 | limitedNamespaces[self.limitNamespace].acquire() 205 | ExitNotifyThread.start(self) 206 | 207 | def run(self): 208 | global limitedNamespaces 209 | 210 | try: 211 | ExitNotifyThread.run(self) 212 | finally: 213 | if limitedNamespaces and limitedNamespaces[self.limitNamespace]: 214 | limitedNamespaces[self.limitNamespace].release() 215 | -------------------------------------------------------------------------------- /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 | from urllib.parse import urlencode 17 | import sys 18 | import time 19 | import logging 20 | from threading import currentThread 21 | 22 | import offlineimap 23 | from offlineimap.ui.UIBase import UIBase 24 | 25 | protocol = '7.2.0' 26 | 27 | 28 | class MachineLogFormatter(logging.Formatter): 29 | """urlencodes any outputted line, to avoid multi-line output""" 30 | 31 | def format(self, record): 32 | # Mapping of log levels to historic tag names 33 | severity_map = { 34 | 'info': 'msg', 35 | 'warning': 'warn', 36 | } 37 | line = super(MachineLogFormatter, self).format(record) 38 | severity = record.levelname.lower() 39 | if severity in severity_map: 40 | severity = severity_map[severity] 41 | if hasattr(record, "machineui"): 42 | command = record.machineui["command"] 43 | whoami = record.machineui["id"] 44 | else: 45 | command = "" 46 | whoami = currentThread().getName() 47 | 48 | prefix = "%s:%s" % (command, urlencode([('', whoami)])[1:]) 49 | return "%s:%s:%s" % (severity, prefix, urlencode([('', line)])[1:]) 50 | 51 | 52 | class MachineUI(UIBase): 53 | def __init__(self, config, loglevel=logging.INFO): 54 | super(MachineUI, self).__init__(config, loglevel) 55 | self._log_con_handler.createLock() 56 | """lock needed to block on password input""" 57 | # Set up the formatter that urlencodes the strings... 58 | self._log_con_handler.setFormatter(MachineLogFormatter()) 59 | 60 | # Arguments: 61 | # - handler: must be method from self.logger that reflects 62 | # the severity of the passed message 63 | # - command: command that produced this message 64 | # - msg: the message itself 65 | def _printData(self, handler, command, msg): 66 | handler(msg, 67 | extra={ 68 | 'machineui': { 69 | 'command': command, 70 | 'id': currentThread().getName(), 71 | } 72 | }) 73 | 74 | def _msg(self, msg): 75 | self._printData(self.logger.info, '_display', msg) 76 | 77 | def warn(self, msg, minor=0): 78 | # TODO, remove and cleanup the unused minor stuff 79 | self._printData(self.logger.warning, '', msg) 80 | 81 | def registerthread(self, account): 82 | super(MachineUI, self).registerthread(account) 83 | self._printData(self.logger.info, 'registerthread', account) 84 | 85 | def unregisterthread(self, thread): 86 | UIBase.unregisterthread(self, thread) 87 | self._printData(self.logger.info, 'unregisterthread', thread.getName()) 88 | 89 | def debugging(self, debugtype): 90 | self._printData(self.logger.debug, 'debugging', debugtype) 91 | 92 | def acct(self, accountname): 93 | self._printData(self.logger.info, 'acct', accountname) 94 | 95 | def acctdone(self, accountname): 96 | self._printData(self.logger.info, 'acctdone', accountname) 97 | 98 | def validityproblem(self, folder): 99 | self._printData(self.logger.warning, 'validityproblem', "%s\n%s\n%s\n%s" % 100 | (folder.getname(), folder.getrepository().getname(), 101 | folder.get_saveduidvalidity(), folder.get_uidvalidity())) 102 | 103 | def connecting(self, reposname, hostname, port): 104 | self._printData(self.logger.info, 'connecting', "%s\n%s\n%s" % (hostname, 105 | str(port), reposname)) 106 | 107 | def syncfolders(self, srcrepos, destrepos): 108 | self._printData(self.logger.info, 'syncfolders', "%s\n%s" % (self.getnicename(srcrepos), 109 | self.getnicename(destrepos))) 110 | 111 | def syncingfolder(self, srcrepos, srcfolder, destrepos, destfolder): 112 | self._printData(self.logger.info, 'syncingfolder', "%s\n%s\n%s\n%s\n" % 113 | (self.getnicename(srcrepos), srcfolder.getname(), 114 | self.getnicename(destrepos), destfolder.getname())) 115 | 116 | def loadmessagelist(self, repos, folder): 117 | self._printData(self.logger.info, 'loadmessagelist', "%s\n%s" % (self.getnicename(repos), 118 | folder.getvisiblename())) 119 | 120 | def messagelistloaded(self, repos, folder, count): 121 | self._printData(self.logger.info, 'messagelistloaded', "%s\n%s\n%d" % 122 | (self.getnicename(repos), folder.getname(), count)) 123 | 124 | def syncingmessages(self, sr, sf, dr, df): 125 | self._printData(self.logger.info, 'syncingmessages', "%s\n%s\n%s\n%s\n" % 126 | (self.getnicename(sr), sf.getname(), self.getnicename(dr), 127 | df.getname())) 128 | 129 | def ignorecopyingmessage(self, uid, srcfolder, destfolder): 130 | self._printData(self.logger.info, 'ignorecopyingmessage', "%d\n%s\n%s\n%s[%s]" % 131 | (uid, self.getnicename(srcfolder), srcfolder.getname(), 132 | self.getnicename(destfolder), destfolder)) 133 | 134 | def copyingmessage(self, uid, num, num_to_copy, srcfolder, destfolder): 135 | self._printData(self.logger.info, 'copyingmessage', "%d\n%s\n%s\n%s[%s]" % 136 | (uid, self.getnicename(srcfolder), srcfolder.getname(), 137 | self.getnicename(destfolder), destfolder)) 138 | 139 | def folderlist(self, ulist): 140 | return "\f".join(["%s\t%s" % (self.getnicename(x), x.getname()) for x in ulist]) 141 | 142 | def uidlist(self, ulist): 143 | return "\f".join([str(u) for u in ulist]) 144 | 145 | def deletingmessages(self, uidlist, destlist): 146 | ds = self.folderlist(destlist) 147 | self._printData(self.logger.info, 'deletingmessages', "%s\n%s" % (self.uidlist(uidlist), ds)) 148 | 149 | def addingflags(self, uidlist, flags, dest): 150 | self._printData(self.logger.info, "addingflags", "%s\n%s\n%s" % (self.uidlist(uidlist), 151 | "\f".join(flags), 152 | dest)) 153 | 154 | def deletingflags(self, uidlist, flags, dest): 155 | self._printData(self.logger.info, 'deletingflags', "%s\n%s\n%s" % (self.uidlist(uidlist), 156 | "\f".join(flags), 157 | dest)) 158 | 159 | def threadException(self, thread): 160 | self._printData(self.logger.warning, 'threadException', "%s\n%s" % 161 | (thread.getName(), self.getThreadExceptionString(thread))) 162 | self.delThreadDebugLog(thread) 163 | self.terminate(100) 164 | 165 | def terminate(self, exitstatus=0, errortitle='', errormsg=''): 166 | self._printData(self.logger.info, 'terminate', "%d\n%s\n%s" % (exitstatus, errortitle, errormsg)) 167 | sys.exit(exitstatus) 168 | 169 | def mainException(self): 170 | self._printData(self.logger.warning, 'mainException', self.getMainExceptionString()) 171 | 172 | def threadExited(self, thread): 173 | self._printData(self.logger.info, 'threadExited', thread.getName()) 174 | UIBase.threadExited(self, thread) 175 | 176 | def sleeping(self, sleepsecs, remainingsecs): 177 | self._printData(self.logger.info, 'sleeping', "%d\n%d" % (sleepsecs, remainingsecs)) 178 | if sleepsecs > 0: 179 | time.sleep(sleepsecs) 180 | return 0 181 | 182 | def getpass(self, username, config, errmsg=None): 183 | if errmsg: 184 | self._printData(self.logger.warning, 185 | 'getpasserror', "%s\n%s" % (username, errmsg), 186 | False) 187 | 188 | self._log_con_handler.acquire() # lock the console output 189 | try: 190 | self._printData(self.logger.info, 'getpass', username) 191 | return sys.stdin.readline()[:-1] 192 | finally: 193 | self._log_con_handler.release() 194 | 195 | def init_banner(self): 196 | self._printData(self.logger.info, 'protocol', protocol) 197 | self._printData(self.logger.info, 'initbanner', offlineimap.banner) 198 | 199 | def callhook(self, msg): 200 | self._printData(self.logger.info, 'callhook', msg) 201 | -------------------------------------------------------------------------------- /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 | 24 | class Basic(UIBase): 25 | """'Basic' simply sets log level to INFO.""" 26 | 27 | def __init__(self, config, loglevel=logging.INFO): 28 | return super(Basic, self).__init__(config, loglevel) 29 | 30 | 31 | class Quiet(UIBase): 32 | """'Quiet' simply sets log level to WARNING""" 33 | 34 | def __init__(self, config, loglevel=logging.WARNING): 35 | return super(Quiet, self).__init__(config, loglevel) 36 | 37 | 38 | class Syslog(UIBase): 39 | """'Syslog' sets log level to INFO and outputs to syslog instead of stdout""" 40 | 41 | def __init__(self, config, loglevel=logging.INFO): 42 | return super(Syslog, self).__init__(config, loglevel) 43 | 44 | def setup_consolehandler(self): 45 | # create syslog handler 46 | ch = logging.handlers.SysLogHandler('/dev/log') 47 | # create formatter and add it to the handlers 48 | self.formatter = logging.Formatter("offlineimap[%(process)d]: %(message)s") 49 | ch.setFormatter(self.formatter) 50 | # add the handlers to the logger 51 | self.logger.addHandler(ch) 52 | self.logger.info(offlineimap.banner) 53 | return ch 54 | 55 | def setup_sysloghandler(self): 56 | pass # Do not honor -s (log to syslog) CLI option. 57 | -------------------------------------------------------------------------------- /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 | 21 | logfile = open("/tmp/logfile", "wt") 22 | loglock = Lock() 23 | 24 | 25 | class DebuggingLock: 26 | def __init__(self, name): 27 | self.lock = Lock() 28 | self.name = name 29 | 30 | def acquire(self, blocking=1): 31 | self.print_tb("Acquire lock") 32 | self.lock.acquire(blocking) 33 | self.logmsg("===== %s: Thread %s acquired lock\n" % 34 | (self.name, currentThread().getName())) 35 | 36 | def release(self): 37 | self.print_tb("Release lock") 38 | self.lock.release() 39 | 40 | def logmsg(self, msg): 41 | loglock.acquire() 42 | logfile.write(msg + "\n") 43 | logfile.flush() 44 | loglock.release() 45 | 46 | def print_tb(self, msg): 47 | self.logmsg(".... %s: Thread %s attempting to %s\n" % 48 | (self.name, currentThread().getName(), msg) + 49 | "\n".join(traceback.format_list(traceback.extract_stack()))) 50 | -------------------------------------------------------------------------------- /offlineimap/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfflineIMAP/offlineimap3/db347452273bb0f1b1a8ea952f6fb46cf95fedbf/offlineimap/utils/__init__.py -------------------------------------------------------------------------------- /offlineimap/utils/const.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2013-2014 Eygene A. Ryabinkin and contributors 3 | 4 | Collection of classes that implement const-like behaviour 5 | for various objects. 6 | """ 7 | import copy 8 | 9 | 10 | class ConstProxy: 11 | """Implements read-only access to a given object 12 | that can be attached to each instance only once.""" 13 | 14 | def __init__(self): 15 | self.__dict__['__source'] = None 16 | 17 | def __getattr__(self, name): 18 | src = self.__dict__['__source'] 19 | if src is None: 20 | raise ValueError("using non-initialized ConstProxy() object") 21 | return copy.deepcopy(getattr(src, name)) 22 | 23 | def __setattr__(self, name, value): 24 | raise AttributeError("tried to set '%s' to '%s' for constant object" % 25 | (name, value)) 26 | 27 | def __delattr__(self, name): 28 | raise RuntimeError("tried to delete field '%s' from constant object" % 29 | name) 30 | 31 | def set_source(self, source): 32 | """ Sets source object for this instance. """ 33 | if self.__dict__['__source'] is not None: 34 | raise ValueError("source object is already set") 35 | self.__dict__['__source'] = source 36 | -------------------------------------------------------------------------------- /offlineimap/utils/distro_utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2006-2018 Eygene A. Ryabinkin & contributors. 3 | 4 | Module that supports distribution-specific functions. 5 | """ 6 | import platform 7 | import os 8 | 9 | # For the former we will just return the value, for an iterable 10 | # we will walk through the values and will return the first 11 | # one that corresponds to the existing file. 12 | __DEF_OS_LOCATIONS = { 13 | 'freebsd': ['/usr/local/share/certs/ca-root-nss.crt'], 14 | 'openbsd': ['/etc/ssl/cert.pem'], 15 | 'dragonfly': ['/etc/ssl/cert.pem'], 16 | 'darwin': [ 17 | # MacPorts, port curl-ca-bundle 18 | '/opt/local/share/curl/curl-ca-bundle.crt', 19 | # homebrew, package openssl 20 | '/usr/local/etc/openssl/cert.pem', 21 | # homebrew, apple silicon package ca-certificates 22 | '/opt/homebrew/etc/ca-certificates/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_name = platform.system().lower() 47 | 48 | if os_name.startswith('linux'): 49 | # linux_distribution deprecated in Python 3.7 50 | try: 51 | from platform import linux_distribution 52 | except ImportError: 53 | from distro import linux_distribution 54 | 55 | distro_name = linux_distribution()[0] 56 | if distro_name: 57 | os_name = os_name + "-%s" % distro_name.split()[0].lower() 58 | if os.path.exists('/etc/arch-release'): 59 | os_name = "linux-arch" 60 | 61 | return os_name 62 | 63 | 64 | def get_os_sslcertfile_searchpath(): 65 | """Returns search path for CA bundle for the current OS. 66 | 67 | We will return an iterable even if configuration has just 68 | a single value: it is easier for our callers to be sure 69 | that they can iterate over result. 70 | 71 | Returned value of None means that there is no search path 72 | at all. 73 | """ 74 | os_name = get_os_name() 75 | location = __DEF_OS_LOCATIONS.get(os_name, []) 76 | 77 | try: 78 | import ssl 79 | verify_paths = ssl.get_default_verify_paths() 80 | cafile_by_envvar = os.getenv(verify_paths.openssl_cafile_env) 81 | if cafile_by_envvar is not None: 82 | location += [cafile_by_envvar] 83 | cafile_resolved = verify_paths.cafile 84 | if cafile_resolved is not None: 85 | location += [cafile_resolved] 86 | cafile_hardcoded = verify_paths.openssl_cafile 87 | if cafile_hardcoded is not None: 88 | location += [cafile_hardcoded] 89 | except AttributeError: 90 | pass 91 | finally: 92 | if len(location) == 0: 93 | return None 94 | 95 | return location 96 | 97 | 98 | def get_os_sslcertfile(): 99 | """ 100 | Finds out the location for the distribution-specific 101 | CA certificate file bundle. 102 | 103 | Returns the location of the file or None if there is 104 | no known CA certificate file or all known locations 105 | correspond to non-existing filesystem objects. 106 | """ 107 | 108 | location = get_os_sslcertfile_searchpath() 109 | if location is None: 110 | return None 111 | 112 | for l_file in location: 113 | assert isinstance(l_file, str) 114 | if os.path.exists(l_file) and (os.path.isfile(l_file) or 115 | os.path.islink(l_file)): 116 | return l_file 117 | 118 | return None 119 | -------------------------------------------------------------------------------- /offlineimap/utils/stacktrace.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2013 Eygene A. Ryabinkin 3 | Functions to perform stack tracing (for multithreaded programs 4 | as well as for single-threaded ones). 5 | """ 6 | 7 | import sys 8 | import threading 9 | import traceback 10 | 11 | 12 | def dump(out): 13 | """ Dumps current stack trace into I/O object 'out' """ 14 | id2name = {} 15 | for th_en in threading.enumerate(): 16 | id2name[th_en.ident] = th_en.name 17 | 18 | count = 0 19 | for i, stack in list(sys._current_frames().items()): 20 | out.write("\n# Thread #%d (id=%d), %s\n" % (count, i, id2name[i])) 21 | count = count + 1 22 | for file, lno, name, line in traceback.extract_stack(stack): 23 | out.write('File: "%s", line %d, in %s' % (file, lno, name)) 24 | if line: 25 | out.write(" %s" % (line.strip())) 26 | out.write("\n") 27 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | dependencies = [ 3 | "distro", 4 | "imaplib2>=3.5", 5 | "rfc6555", 6 | "urllib3~=1.25.9" 7 | ] 8 | name = "offlineimap" 9 | version = "8.0.0" 10 | description = "IMAP synchronization tool" 11 | authors = [ 12 | { name = "John Goerzen & contributors", email = "jgoerzen@complete.org" } 13 | ] 14 | license = { text = "GPL-2.0" } 15 | readme = "README.md" 16 | keywords = ["client", "imap", "cli", "email", "mail", "synchronization", "sync", "offline"] 17 | requires-python = ">=3.6" 18 | 19 | classifiers = [ 20 | "Development Status :: 5 - Production/Stable", 21 | "Environment :: Console", 22 | "License :: OSI Approved :: GNU General Public License v2 (GPLv2)", 23 | "Operating System :: POSIX", 24 | "Programming Language :: Python :: 3", 25 | "Programming Language :: Python :: 3.6", 26 | "Programming Language :: Python :: 3.7", 27 | "Programming Language :: Python :: 3.8", 28 | "Programming Language :: Python :: 3.9", 29 | "Programming Language :: Python :: 3.10", 30 | "Programming Language :: Python :: 3.11", 31 | "Programming Language :: Python :: 3.12", 32 | "Programming Language :: Python :: Implementation :: PyPy", 33 | "Topic :: Office/Business :: Scheduling", 34 | "Topic :: Utilities" 35 | ] 36 | 37 | [project.urls] 38 | homepage = "http://www.offlineimap.org" 39 | documentation = "https://www.offlineimap.org/documentation.html" 40 | issues = "https://github.com/OfflineIMAP/offlineimap3/issues" 41 | repository = "https://github.com/OfflineIMAP/offlineimap3/" 42 | 43 | [build-system] 44 | requires = [ 45 | "setuptools>=18.5", 46 | "wheel" 47 | ] 48 | 49 | [project.optional-dependencies] 50 | keyring = ["keyring"] 51 | cygwin = ["portalocker[cygwin]"] 52 | kerberos = ["gssapi[kerberos]"] 53 | testinternet = ["certifi~=2020.6.20"] 54 | 55 | [project.scripts] 56 | offlineimap = "offlineimap.init:main" 57 | -------------------------------------------------------------------------------- /requirements-certify.txt: -------------------------------------------------------------------------------- 1 | certifi~=2020.6.20 2 | -------------------------------------------------------------------------------- /requirements-cygwin.txt: -------------------------------------------------------------------------------- 1 | portalocker[cygwin] 2 | -------------------------------------------------------------------------------- /requirements-kerberos.txt: -------------------------------------------------------------------------------- 1 | gssapi[kerberos] 2 | -------------------------------------------------------------------------------- /requirements-keyring.txt: -------------------------------------------------------------------------------- 1 | keyring 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # Requirements 2 | rfc6555 3 | distro; python_version > "3.6" 4 | imaplib2>=3.5 5 | urllib3~=1.25.9 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 re 25 | try: 26 | from setuptools import setup, Command 27 | except: 28 | from distutils.core import setup, Command 29 | 30 | 31 | with open('offlineimap/__init__.py') as f: 32 | version_grp = re.search(r"__version__ = ['\"](.+)['\"]", f.read()) 33 | if version_grp: 34 | version = version_grp.group(1) 35 | else: 36 | version = "0.0.0" 37 | 38 | f.seek(0) 39 | description_grp = re.search(r"__description__ = ['\"](.+)['\"]", f.read()) 40 | if description_grp: 41 | description = description_grp.group(1) 42 | else: 43 | description = "Disconnected Universal IMAP Mail Synchronization/Reader Support" 44 | 45 | f.seek(0) 46 | author_grp = re.search(r"__author__ = ['\"](.+)['\"]", f.read()) 47 | if author_grp: 48 | author = author_grp.group(1) 49 | else: 50 | author = "John Goerzen" 51 | 52 | f.seek(0) 53 | author_email_grp = re.search(r"__author_email__ = ['\"](.+)['\"]", f.read()) 54 | if author_email_grp: 55 | author_email = author_email_grp.group(1) 56 | else: 57 | author_email = "" 58 | 59 | f.seek(0) 60 | homepage_grp = re.search(r"__homepage__ = ['\"](.+)['\"]", f.read()) 61 | if homepage_grp: 62 | homepage = homepage_grp.group(1) 63 | else: 64 | homepage = "http://www.offlineimap.org" 65 | 66 | f.seek(0) 67 | copyright_grp = re.search(r"__copyright__ = ['\"](.+)['\"]", f.read()) 68 | if copyright_grp: 69 | copyright = copyright_grp.group(1) 70 | else: 71 | copyright = "" 72 | 73 | 74 | setup(name="offlineimap", 75 | version=version, 76 | description=description, 77 | long_description=description, 78 | author=author, 79 | author_email=author_email, 80 | url=homepage, 81 | packages=['offlineimap', 'offlineimap.folder', 82 | 'offlineimap.repository', 'offlineimap.ui', 83 | 'offlineimap.utils'], 84 | scripts=['bin/offlineimap'], 85 | setup_requires=['setuptools>=18.5', 'wheel', 'imaplib2'], 86 | license=copyright + ", Licensed under the GPL version 2", 87 | install_requires=['distro', 88 | 'imaplib2>=3.5', 89 | 'rfc6555', 90 | 'urllib3~=1.25.9'], 91 | extras_require={'kerberos':'gssapi[kerberos]', 92 | 'keyring':'keyring[keyring]', 93 | 'cygwin':'portalocker[cygwin]', 94 | 'testinternet':'certifi~=2020.6.20'} 95 | ) 96 | 97 | -------------------------------------------------------------------------------- /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: python3 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 | from io import StringIO 18 | 19 | default_conf = StringIO("""[general] 20 | #will be set automatically 21 | metadata = 22 | accounts = test 23 | ui = quiet 24 | 25 | [Account test] 26 | localrepository = Maildir 27 | remoterepository = IMAP 28 | 29 | [Repository Maildir] 30 | Type = Maildir 31 | # will be set automatically during tests 32 | localfolders = 33 | 34 | [Repository IMAP] 35 | type=IMAP 36 | # Don't hammer the server with too many connection attempts: 37 | maxconnections=1 38 | folderfilter= lambda f: f.startswith('INBOX.OLItest') or f.startswith('INBOX/OLItest') 39 | """) 40 | -------------------------------------------------------------------------------- /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 | 'python3 setup.py test' 14 | 15 | System requirements 16 | =================== 17 | 18 | This test suite depend on python 3 to run out of the box. 19 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfflineIMAP/offlineimap3/db347452273bb0f1b1a8ea952f6fb46cf95fedbf/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: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfflineIMAP/offlineimap3/db347452273bb0f1b1a8ea952f6fb46cf95fedbf/test/tests/__init__.py -------------------------------------------------------------------------------- /test/tests/test_00_globals.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright 2013 Eygene A. Ryabinkin 3 | 4 | import unittest 5 | from offlineimap import globals 6 | 7 | 8 | class Opt: 9 | def __init__(self): 10 | self.one = "baz" 11 | self.two = 42 12 | self.three = True 13 | 14 | 15 | class TestOfflineimapGlobals(unittest.TestCase): 16 | 17 | @classmethod 18 | def setUpClass(cls): 19 | cls.o = Opt() 20 | globals.set_options(cls.o) 21 | 22 | def test_initial_state(self): 23 | for k in self.o.__dict__.keys(): 24 | self.assertTrue(getattr(self.o, k) == 25 | getattr(globals.options, k)) 26 | 27 | def test_object_changes(self): 28 | self.o.one = "one" 29 | self.o.two = 119 30 | self.o.three = False 31 | return self.test_initial_state() 32 | 33 | def test_modification(self): 34 | with self.assertRaises(AttributeError): 35 | globals.options.two = True 36 | 37 | def test_deletion(self): 38 | with self.assertRaises(RuntimeError): 39 | del globals.options.three 40 | 41 | def test_nonexistent_key(self): 42 | with self.assertRaises(AttributeError): 43 | raise AttributeError 44 | 45 | def test_double_init(self): 46 | with self.assertRaises(ValueError): 47 | globals.set_options(True) 48 | 49 | 50 | if __name__ == "__main__": 51 | suite = unittest.TestLoader().loadTestsFromTestCase(TestOfflineimapGlobals) 52 | unittest.TextTestRunner(verbosity=2).run(suite) 53 | -------------------------------------------------------------------------------- /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 test.OLItest import OLITestLib 20 | from offlineimap import imaputil 21 | from offlineimap.ui import UI_LIST, setglobalui 22 | 23 | 24 | # Things need to be setup first, usually setup.py initializes everything. 25 | # but if e.g. called from command line, we take care of default values here: 26 | if not OLITestLib.cred_file: 27 | OLITestLib(cred_file='./test/credentials.conf', cmd='./offlineimap.py') 28 | 29 | 30 | def setUpModule(): 31 | logging.info("Set Up test module %s" % __name__) 32 | OLITestLib.create_test_dir(suffix=__name__) 33 | 34 | 35 | def tearDownModule(): 36 | logging.info("Tear Down test module") 37 | # comment out next line to keep testdir after test runs. TODO: make nicer 38 | OLITestLib.delete_test_dir() 39 | 40 | 41 | # Stuff that can be used 42 | # self.assertEqual(self.seq, range(10)) 43 | # should raise an exception for an immutable sequence 44 | # self.assertRaises(TypeError, random.shuffle, (1,2,3)) 45 | # self.assertTrue(element in self.seq) 46 | # self.assertFalse(element in self.seq) 47 | 48 | class TestInternalFunctions(unittest.TestCase): 49 | """While the other test files test OfflineImap as a program, these 50 | tests directly invoke internal helper functions to guarantee that 51 | they deliver results as expected""" 52 | 53 | @classmethod 54 | def setUpClass(cls): 55 | # This is run before all tests in this class 56 | config = OLITestLib.get_default_config() 57 | setglobalui(UI_LIST['quiet'](config)) 58 | 59 | def test_01_imapsplit(self): 60 | """Test imaputil.imapsplit()""" 61 | res = imaputil.imapsplit('(\\HasNoChildren) "." "INBOX.Sent"') 62 | self.assertEqual(res, ['(\\HasNoChildren)', '"."', '"INBOX.Sent"']) 63 | 64 | res = imaputil.imapsplit('"mo\\" o" sdfsdf') 65 | self.assertEqual(res, ['"mo\\" o"', 'sdfsdf']) 66 | 67 | def test_02_flagsplit(self): 68 | """Test imaputil.flagsplit()""" 69 | res = imaputil.flagsplit('(\\Draft \\Deleted)') 70 | self.assertEqual(res, ['\\Draft', '\\Deleted']) 71 | 72 | res = imaputil.flagsplit('(FLAGS (\\Seen Old) UID 4807)') 73 | self.assertEqual(res, ['FLAGS', '(\\Seen Old)', 'UID', '4807']) 74 | 75 | def test_04_flags2hash(self): 76 | """Test imaputil.flags2hash()""" 77 | res = imaputil.flags2hash('(FLAGS (\\Seen Old) UID 4807)') 78 | self.assertEqual(res, {'FLAGS': '(\\Seen Old)', 'UID': '4807'}) 79 | 80 | def test_05_flagsimap2maildir(self): 81 | """Test imaputil.flagsimap2maildir()""" 82 | res = imaputil.flagsimap2maildir('(\\Draft \\Deleted)') 83 | self.assertEqual(res, set('DT')) 84 | 85 | def test_06_flagsmaildir2imap(self): 86 | """Test imaputil.flagsmaildir2imap()""" 87 | res = imaputil.flagsmaildir2imap(set('DR')) 88 | self.assertEqual(res, '(\\Answered \\Draft)') 89 | # test all possible flags 90 | res = imaputil.flagsmaildir2imap(set('SRFTD')) 91 | self.assertEqual(res, '(\\Answered \\Deleted \\Draft \\Flagged \\Seen)') 92 | 93 | def test_07_uid_sequence(self): 94 | """Test imaputil.uid_sequence()""" 95 | res = imaputil.uid_sequence([1, 2, 3, 4, 5, 10, 12, 13]) 96 | self.assertEqual(res, '1:5,10,12:13') 97 | -------------------------------------------------------------------------------- /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 unittest 17 | import logging 18 | from test.OLItest import OLITestLib 19 | 20 | # Things need to be setup first, usually setup.py initializes everything. 21 | # but if e.g. called from command line, we take care of default values here: 22 | if not OLITestLib.cred_file: 23 | OLITestLib(cred_file='./test/credentials.conf', cmd='./offlineimap.py') 24 | 25 | 26 | def setUpModule(): 27 | logging.info("Set Up test module %s" % __name__) 28 | OLITestLib.create_test_dir(suffix=__name__) 29 | 30 | 31 | def tearDownModule(): 32 | logging.info("Tear Down test module") 33 | OLITestLib.delete_test_dir() 34 | 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 | OLITestLib.run_OLI() 57 | boxes, mails = OLITestLib.count_maildir_mails('') 58 | self.assertTrue((boxes, mails) == (0, 0), 59 | msg="Expected 0 folders and 0 " 60 | "mails, but sync led to {0} folders and {1} mails" 61 | .format(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 | OLITestLib.run_OLI() 68 | boxes, mails = OLITestLib.count_maildir_mails('') 69 | self.assertTrue((boxes, mails) == (1, 0), 70 | msg="Expected 1 folders and 0 " 71 | "mails, but sync led to {0} folders and {1} mails" 72 | .format(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 | boxes, mails = OLITestLib.count_maildir_mails('') 85 | self.assertTrue((boxes, mails) == (1, 0), 86 | msg="Expected 1 folders and 0 " 87 | "mails, but sync led to {0} folders and {1} mails" 88 | .format(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, 105 | msg="Mismatching nametrans rules did " 106 | "NOT trigger an 'infinite folder generation' error. Output was:\n" 107 | "{0}".format(res)) 108 | # Write out default config file again 109 | OLITestLib.write_config_file() 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), 126 | msg="Expected 1 folders and 1 " 127 | "mails, but sync led to {0} folders and {1} mails" 128 | .format(boxes, mails)) 129 | # The local Mail should have been assigned a proper UID now, check! 130 | uids = OLITestLib.get_maildir_uids('INBOX.OLItest') 131 | self.assertFalse(None in uids, 132 | msg="All mails should have been " 133 | "assigned the IMAP's UID number, but {0} messages had no valid ID " 134 | .format(len([None for x in uids if x is None]))) 135 | 136 | def test_06_createfolders(self): 137 | """Test if createfolders works as expected 138 | 139 | Create a local Maildir, then sync with remote "createfolders" 140 | disabled. Delete local Maildir and sync. We should have no new 141 | local maildir then. TODO: Rewrite this test to directly test 142 | and count the remote folders when the helper functions have 143 | been written""" 144 | config = OLITestLib.get_default_config() 145 | config.set('Repository IMAP', 'createfolders', 146 | 'False') 147 | OLITestLib.write_config_file(config) 148 | 149 | # delete all remote and local testfolders 150 | OLITestLib.delete_remote_testfolders() 151 | OLITestLib.delete_maildir('') 152 | OLITestLib.create_maildir('INBOX.OLItest') 153 | OLITestLib.run_OLI() 154 | OLITestLib.delete_maildir('INBOX.OLItest') 155 | OLITestLib.run_OLI() 156 | boxes, mails = OLITestLib.count_maildir_mails('') 157 | self.assertTrue((boxes, mails) == (0, 0), 158 | msg="Expected 0 folders and 0 " 159 | "mails, but sync led to {} folders and {} mails" 160 | .format(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 unittest 17 | import logging 18 | from test.OLItest import OLITestLib 19 | 20 | # Things need to be setup first, usually setup.py initializes everything. 21 | # but if e.g. called from command line, we take care of default values here: 22 | if not OLITestLib.cred_file: 23 | OLITestLib(cred_file='./test/credentials.conf', cmd='./offlineimap.py') 24 | 25 | 26 | def setUpModule(): 27 | logging.info("Set Up test module %s" % __name__) 28 | OLITestLib.create_test_dir(suffix=__name__) 29 | 30 | 31 | def tearDownModule(): 32 | logging.info("Tear Down test module") 33 | OLITestLib.delete_test_dir() 34 | 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 | --------------------------------------------------------------------------------