├── docs ├── requirements.txt ├── .gitattributes ├── reference.rst ├── Makefile ├── make.bat ├── conf.py └── index.rst ├── requirements-dev.txt ├── MANIFEST.in ├── setup.py ├── imaplib2 ├── __init__.py ├── tests │ └── test_imports.py └── imaplib2.py ├── CONTRIBUTING.md ├── tox.ini ├── pyproject.toml ├── README.md ├── .pre-commit-config.yaml ├── .github └── workflows │ ├── codespell.yml │ ├── release.yml │ └── test.yml ├── .readthedocs.yaml ├── setup.cfg ├── .gitignore ├── LICENCE └── CODE_OF_CONDUCT.md /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx 2 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | pre-commit >= 2.15 2 | -------------------------------------------------------------------------------- /docs/.gitattributes: -------------------------------------------------------------------------------- 1 | /make.bat text eol=crlf 2 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include imaplib2/tests/test_imports.py 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup 4 | 5 | setup() 6 | -------------------------------------------------------------------------------- /docs/reference.rst: -------------------------------------------------------------------------------- 1 | Library Reference 2 | ================= 3 | 4 | .. autofunction:: imaplib2.version 5 | -------------------------------------------------------------------------------- /imaplib2/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from .imaplib2 import * 4 | from .imaplib2 import __version__, __doc__ 5 | -------------------------------------------------------------------------------- /imaplib2/tests/test_imports.py: -------------------------------------------------------------------------------- 1 | def test_import_normally(): 2 | from imaplib2 import IMAP4_SSL 3 | assert IMAP4_SSL 4 | 5 | 6 | def test_import_hack(): 7 | from imaplib2.imaplib2 import IMAP4_SSL 8 | assert IMAP4_SSL 9 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | [![Jazzband](https://jazzband.co/static/img/jazzband.svg)](https://jazzband.co/) 2 | 3 | This is a [Jazzband](https://jazzband.co/) project. By contributing you agree to 4 | abide by the [Contributor Code of Conduct](https://jazzband.co/about/conduct) 5 | and follow the [guidelines](https://jazzband.co/about/guidelines). 6 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | min_version = 4.0 3 | requires = 4 | tox-gh-actions 5 | env_list = 6 | py312 7 | py311 8 | py310 9 | 10 | [testenv] 11 | description = run test 12 | package = wheel 13 | deps = 14 | pytest 15 | pytest-cov 16 | commands = 17 | pytest {posargs} 18 | 19 | [gh-actions] 20 | python = 21 | 3.10: py310 22 | 3.11: py311 23 | 3.12: py312 24 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.coverage.run] 6 | source = ["imaplib2/tests"] 7 | source_pkgs = ["imaplib2"] 8 | 9 | [tool.coverage.paths] 10 | source = [ 11 | ".", 12 | ".tox/*/lib/*/site-packages/", 13 | '.tox\\*\\Lib\\site-packages\\', 14 | ] 15 | 16 | [tool.codespell] 17 | # Ref: https://github.com/codespell-project/codespell#using-a-config-file 18 | skip = '.git*' 19 | check-hidden = true 20 | # ignore-regex = '' 21 | ignore-words-list = 'donn' 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # imaplib2: a threaded Python IMAP4 client 2 | 3 | [![Jazzband](https://jazzband.co/static/img/badge.svg)](https://jazzband.co/) 4 | [![codecov](https://codecov.io/gh/jazzband/imaplib2/branch/master/graph/badge.svg?token=DZZ3P6438E)](https://codecov.io/gh/jazzband/imaplib2) 5 | 6 | Based on RFC 3501 and original imaplib module. 7 | 8 | This is a version of imaplib that uses threads to allow full use of the 9 | IMAP4 concurrency features, and to de-couple a user of imaplib from i/o 10 | lags, except where explicitly allowed. 11 | 12 | Documented in imaplib2.html 13 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v5.0.0 6 | hooks: 7 | - id: trailing-whitespace 8 | - id: end-of-file-fixer 9 | - id: check-yaml 10 | - id: check-added-large-files 11 | 12 | - repo: https://github.com/codespell-project/codespell 13 | # Configuration for codespell is in pyproject.toml 14 | rev: v2.4.1 15 | hooks: 16 | - id: codespell 17 | additional_dependencies: 18 | - tomli 19 | -------------------------------------------------------------------------------- /.github/workflows/codespell.yml: -------------------------------------------------------------------------------- 1 | # Codespell configuration is within pyproject.toml 2 | --- 3 | name: Codespell 4 | 5 | on: 6 | push: 7 | branches: [master] 8 | pull_request: 9 | branches: [master] 10 | 11 | permissions: 12 | contents: read 13 | 14 | jobs: 15 | codespell: 16 | name: Check for spelling errors 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v4 22 | - name: Annotate locations with typos 23 | uses: codespell-project/codespell-problem-matcher@v1 24 | - name: Codespell 25 | uses: codespell-project/actions-codespell@v2 26 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file for Sphinx projects 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | 4 | # Required 5 | version: 2 6 | 7 | # Set the OS, Python version and other tools you might need 8 | build: 9 | os: ubuntu-22.04 10 | tools: 11 | python: "3.12" 12 | 13 | # Build documentation in the "docs/" directory with Sphinx 14 | sphinx: 15 | configuration: docs/conf.py 16 | 17 | # Optional but recommended, declare the Python requirements required 18 | # to build your documentation 19 | # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 20 | python: 21 | install: 22 | - requirements: docs/requirements.txt 23 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = imaplib2 3 | version = attr: imaplib2.imaplib2.__version__ 4 | description = A threaded Python IMAP4 client. 5 | long_description = file: README.md 6 | long_description_content_type = text/markdown 7 | url = https://github.com/jazzband/imaplib2/ 8 | author = Piers Lauder 9 | license = MIT 10 | classifiers = 11 | Intended Audience :: Developers 12 | Programming Language :: Python :: 3 13 | Programming Language :: Python :: 3 :: Only 14 | Programming Language :: Python :: 3.10 15 | Programming Language :: Python :: 3.11 16 | Programming Language :: Python :: 3.12 17 | Topic :: Software Development :: Libraries 18 | Topic :: Utilities 19 | License :: OSI Approved :: MIT License 20 | 21 | [options] 22 | packages = find: 23 | python_requires = >=3.10 24 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.https://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | 47 | # Translations 48 | *.mo 49 | *.pot 50 | 51 | # Django stuff: 52 | *.log 53 | 54 | # Sphinx documentation 55 | docs/_build/ 56 | 57 | # PyBuilder 58 | target/ 59 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | deploy: 9 | if: github.repository == 'jazzband/imaplib2' 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v2 14 | - name: Set up Python 15 | uses: actions/setup-python@v2 16 | with: 17 | python-version: 3.10 18 | - name: Install dependencies 19 | run: | 20 | python -m pip install --upgrade pip 21 | pip install --upgrade build twine 22 | - name: Build and check 23 | run: | 24 | python -m build --sdist --wheel 25 | twine check dist/* 26 | - name: Publish to PyPI 27 | if: github.event.action == 'published' 28 | uses: pypa/gh-action-pypi-publish@master 29 | with: 30 | user: jazzband 31 | password: ${{ secrets.JAZZBAND_RELEASE_KEY }} 32 | repository_url: https://jazzband.co/projects/imaplib2/upload 33 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021-2024 The imaplib2 contributors. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | release: 9 | types: [created] 10 | workflow_dispatch: 11 | 12 | jobs: 13 | build_and_test: 14 | name: Install and test 15 | runs-on: ubuntu-20.04 16 | strategy: 17 | matrix: 18 | python-version: ["3.10", "3.11", "3.12"] 19 | 20 | steps: 21 | - name: Checkout code 22 | uses: actions/checkout@v2 23 | - name: Set up Python ${{ matrix.python-version }} 24 | uses: actions/setup-python@v2 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | - name: Install dependencies 28 | run: | 29 | python -m pip install --upgrade pip 30 | pip install --upgrade tox 31 | - name: Test with tox with coverage 32 | run: tox -- --cov --cov-report=xml -v 33 | #- name: Upload coverage to Codecov 34 | # uses: codecov/codecov-action@v1 35 | # with: 36 | # token: ${{ secrets.CODECOV_TOKEN }} 37 | # files: ./coverage.xml 38 | # flags: py${{ matrix.python-version }} 39 | # fail_ci_if_error: true 40 | # verbose: true 41 | - name: Install docs dependencies 42 | working-directory: docs 43 | run: pip install --requirement requirements.txt 44 | - name: Build docs 45 | working-directory: docs 46 | run: make html 47 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | sys.path.insert(0, os.path.abspath('..')) 16 | 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = 'imaplib2' 21 | copyright = '2021, The imaplib2 contributors' 22 | author = 'Piers Lauder' 23 | 24 | # The full version, including alpha/beta/rc tags 25 | release = '3.06' 26 | 27 | 28 | # -- General configuration --------------------------------------------------- 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = [ 34 | 'sphinx.ext.autodoc', 35 | ] 36 | 37 | # Add any paths that contain templates here, relative to this directory. 38 | templates_path = ['_templates'] 39 | 40 | # List of patterns, relative to source directory, that match files and 41 | # directories to ignore when looking for source files. 42 | # This pattern also affects html_static_path and html_extra_path. 43 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 44 | 45 | 46 | # -- Options for HTML output ------------------------------------------------- 47 | 48 | # The theme to use for HTML and HTML Help pages. See the documentation for 49 | # a list of builtin themes. 50 | # 51 | html_theme = 'alabaster' 52 | 53 | # Add any paths that contain custom static files (such as style sheets) here, 54 | # relative to this directory. They are copied after the builtin static files, 55 | # so a file named "default.css" will overwrite the builtin "default.css". 56 | html_static_path = ['_static'] 57 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | As contributors and maintainers of the Jazzband projects, and in the interest of 4 | fostering an open and welcoming community, we pledge to respect all people who 5 | contribute through reporting issues, posting feature requests, updating documentation, 6 | submitting pull requests or patches, and other activities. 7 | 8 | We are committed to making participation in the Jazzband a harassment-free experience 9 | for everyone, regardless of the level of experience, gender, gender identity and 10 | expression, sexual orientation, disability, personal appearance, body size, race, 11 | ethnicity, age, religion, or nationality. 12 | 13 | Examples of unacceptable behavior by participants include: 14 | 15 | - The use of sexualized language or imagery 16 | - Personal attacks 17 | - Trolling or insulting/derogatory comments 18 | - Public or private harassment 19 | - Publishing other's private information, such as physical or electronic addresses, 20 | without explicit permission 21 | - Other unethical or unprofessional conduct 22 | 23 | The Jazzband roadies have the right and responsibility to remove, edit, or reject 24 | comments, commits, code, wiki edits, issues, and other contributions that are not 25 | aligned to this Code of Conduct, or to ban temporarily or permanently any contributor 26 | for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 27 | 28 | By adopting this Code of Conduct, the roadies commit themselves to fairly and 29 | consistently applying these principles to every aspect of managing the jazzband 30 | projects. Roadies who do not follow or enforce the Code of Conduct may be permanently 31 | removed from the Jazzband roadies. 32 | 33 | This code of conduct applies both within project spaces and in public spaces when an 34 | individual is representing the project or its community. 35 | 36 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by 37 | contacting the roadies at `roadies@jazzband.co`. All complaints will be reviewed and 38 | investigated and will result in a response that is deemed necessary and appropriate to 39 | the circumstances. Roadies are obligated to maintain confidentiality with regard to the 40 | reporter of an incident. 41 | 42 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 43 | 1.3.0, available at [https://contributor-covenant.org/version/1/3/0/][version] 44 | 45 | [homepage]: https://contributor-covenant.org 46 | [version]: https://contributor-covenant.org/version/1/3/0/ 47 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Python threaded IMAP4 client module ``imaplib2`` 2 | ================================================ 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | :caption: Contents: 7 | 8 | reference 9 | 10 | .. contents:: :local: 11 | 12 | This module defines a class, ``IMAP4``, which encapsulates a threaded 13 | connection to an IMAP4 server and implements the IMAP4rev1 client 14 | protocol as defined in RFC 3501 with several extensions. This module 15 | presents an almost identical API as that provided by the standard python 16 | library module ``imaplib``, the main difference being that this version 17 | allows parallel execution of commands on the IMAP4 server, and 18 | implements the IMAP4rev1 ``IDLE`` extension. (``imaplib2`` can be 19 | substituted for ``imaplib`` in existing clients with no changes in the 20 | code, but see the *caveat* below.) 21 | 22 | An ``IMAP4`` instance is instantiated with an optional ``host`` and/or 23 | ``port``. The defaults are ``localhost`` and ``143`` - the standard 24 | IMAP4 port number. 25 | 26 | There are also five other optional arguments: 27 | ``debug=level, debug_file=file, identifier=string, timeout=seconds, debug_buf_lvl=level``. 28 | Setting ``debug`` *level* (default: 0) to anything above 29 | ``debug_buf_lvl`` (default: 3) causes every action to be printed to 30 | *file* (default: *sys.stderr*). Otherwise actions are logged in a 31 | circular buffer and the last 20 printed on errors. The third argument 32 | provides a string to be prepended to thread names - useful during 33 | debugging (default: target host). The forth argument sets a timeout for 34 | responses from the server, after which the instance will abort. Note 35 | that this timeout is overridden by an IDLE timeout when active. 36 | 37 | **Caveat**: Once an instance has been created, the invoker must call the 38 | ``logout`` method before discarding it, to shut down the threads. 39 | 40 | There are two classes derived from ``IMAP4`` which provide alternate 41 | transport mechanisms: 42 | 43 | ``IMAP4_SSL`` 44 | IMAP4 client class over an SSL connection. 45 | ``IMAP4_stream`` 46 | IMAP4 client class over a stream. 47 | 48 | There are also 2 utility methods provided for processing IMAP4 date 49 | strings: 50 | 51 | ``Internaldate2Time``\ (*datestr*) 52 | Converts an IMAP4 ``INTERNALDATE`` string to Universal Time. Returns 53 | a ``time`` module tuple. 54 | ``Time2Internaldate``\ (*date_time*) 55 | Converts ``date_time`` (a ``time`` module tuple, or an integer or 56 | float seconds) to an IMAP4 ``INTERNALDATE`` representation. Returns a 57 | string in the form: 58 | ``"DD-Mmm-YYYY HH:MM:SS +HHMM"`` (including double-quotes). 59 | 60 | And there is one utility method for parsing IMAP4 ``FLAGS`` responses: 61 | 62 | ``ParseFlags``\ (*response*) 63 | Convert an IMAP4 flags response (a string of the form 64 | ``"...FLAGS (flag ...)"``) to a python tuple.. 65 | 66 | IMAP4 Objects 67 | ------------- 68 | 69 | All IMAP4rev1 commands are represented by methods of the same name 70 | 71 | Each command returns a tuple: ``(type, [data, ...])`` where ``type`` is 72 | usually ``'OK'`` or ``'NO'``, and ``data`` is either the text from the 73 | command response (always true when ``type`` is ``'NO'``), or mandated 74 | results from the command. Each ``data`` is either a string, or a tuple. 75 | If a tuple, then the first part is the header of the response, and the 76 | second part contains the data (ie: *literal* value). 77 | 78 | Any logical errors raise the exception class 79 | ``.error("")``. IMAP4 server errors raise 80 | ``.abort("")``, which is a sub-class of ``error``. 81 | Mailbox status changes from ``READ-WRITE`` to ``READ-ONLY`` raise 82 | ``.readonly("")``, which is a sub-class of ``abort``. 83 | Note that closing the instance and instantiating a new one will usually 84 | recover from an ``abort``. 85 | 86 | All commands take two optional named arguments: ``callback`` and 87 | ``cb_arg``. If ``callback`` is provided then the command is asynchronous 88 | (the IMAP4 command is scheduled, and the call returns immediately), and 89 | the result will be posted by invoking ``callback`` with a single 90 | argument: 91 | 92 | or, if there was a problem: 93 | 94 | Otherwise the command is synchronous (waits for result). But note that 95 | state-changing commands will both block until previous commands have 96 | completed, and block subsequent commands until they have finished. 97 | 98 | All (non-callback) arguments to commands are converted to strings, 99 | except for ``authenticate``, and the last argument to ``append`` which 100 | is passed as an IMAP4 literal. If necessary (the string contains any 101 | non-printing characters or white-space and isn't enclosed with either 102 | parentheses or double quotes or single quotes) each string is quoted. 103 | However, the *password* argument to the ``login`` command is always 104 | quoted. 105 | 106 | If you want to avoid having an argument string quoted (eg: the *flags* 107 | argument to ``store``) then enclose the string in parentheses (eg: 108 | ``(\Deleted)``). If you are using *sequence sets* containing the 109 | wildcard character '*', then enclose the argument in single quotes: the 110 | quotes will be removed and the resulting string passed unquoted. 111 | 112 | To summarise the quoting rules: 113 | 114 | - a string is automatically quoted if it contains at least one of the 115 | IMAP4 *atom-special* characters with the following exceptions: 116 | - the password argument to the ``login`` command is always quoted; 117 | - a string enclosed in ``"..."`` or ``(...)`` is passed as is; 118 | - a string enclosed in ``'...'`` is stripped of the enclosing single 119 | quotes and the rest passed as is. 120 | 121 | Note also that you can pass in an argument with a type that doesn't 122 | evaluate to *basestring* (eg: ``bytearray``) and it will be converted to 123 | a string without quoting. 124 | 125 | There is one instance variable, ``state``, that is useful for tracking 126 | whether the client needs to login to the server. If it has the value 127 | ``"AUTH"`` after instantiating the class, then the connection is 128 | pre-authenticated (otherwise it will be ``"NONAUTH"``). Selecting a 129 | mailbox changes the state to be ``"SELECTED"``, closing a mailbox 130 | changes back to ``"AUTH"``, and once the client has logged out, the 131 | state changes to ``"LOGOUT"`` and no further commands may be issued. 132 | 133 | There is another instance variable, ``capabilities``, that holds a list 134 | of the capabilities provided by the server (the same as the list 135 | returned by the IMAP4 ``CAPABILITY`` command). 136 | 137 | An ``IMAP4`` instance has the following methods: 138 | 139 | ``append``\ (*mailbox, flags, date_time, message*) 140 | Append message to named mailbox. All args except ``message`` can be 141 | None 142 | ``authenticate``\ (*mechanism, authobject*) 143 | Authenticate command - requires response processing. 144 | *mechanism* specifies which authentication mechanism is to be used - 145 | it must appear in ``.capabilities`` in the form 146 | ``AUTH=``\ *mechanism*. 147 | *authobject* must be a callable object: 148 | 149 | It will be called to process server continuation responses. It should 150 | return data that will be encoded and sent to the server. It should 151 | return None if the client abort response ``*`` should be sent 152 | instead. 153 | ``capability``\ () 154 | Return server IMAP4 capabilities. 155 | ``check``\ () 156 | Checkpoint mailbox on server. 157 | ``close``\ () 158 | Close currently selected mailbox. Deleted messages are removed from 159 | writable mailbox. This is the recommended command before LOGOUT. 160 | ``copy``\ (*message_set, new_mailbox*) 161 | Copy *message_set* messages onto end of *new_mailbox*. 162 | ``create``\ (*mailbox*) 163 | Create new mailbox. 164 | ``delete``\ (*mailbox*) 165 | Delete old mailbox. 166 | ``enable``\ (*capability*) 167 | Send an RFC5161 enable string to the server. EG: ask the server to 168 | enable UTF-8 message encoding: 169 | 170 | :: 171 | 172 | if 'ENABLE' in imapobj.capabilities: 173 | imapobj.enable("UTF8=ACCEPT") 174 | 175 | ``enable_compression``\ () 176 | Ask the server to start compressing the connection. Should be called 177 | from user of this class after instantiation, as in: 178 | 179 | :: 180 | 181 | if 'COMPRESS=DEFLATE' in imapobj.capabilities: 182 | imapobj.enable_compression() 183 | 184 | ``examine``\ (*mailbox*\ ='INBOX') 185 | Select a mailbox for READ-ONLY access. Flush all untagged responses. 186 | Returned *data* is count of messages in mailbox (EXISTS response). 187 | Mandated responses are 188 | ``'FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY'``, so other responses 189 | should be obtained by calling ``response('FLAGS')`` etc. 190 | ``expunge``\ () 191 | Permanently remove deleted items from selected mailbox. Generates an 192 | EXPUNGE response for each deleted message. Returned *data* contains a 193 | list of EXPUNGE message numbers in order received. 194 | ``fetch``\ (*message_set, message_parts*) 195 | Fetch (parts of) messages. ``message_parts`` should be a string of 196 | selected parts enclosed in parentheses, eg: ``"(UID BODY[TEXT])"``. 197 | Returned *data* are tuples of message part envelope and data, 198 | followed by a string containing the trailer. 199 | ``getacl``\ (*mailbox*) 200 | Get the Access Control Lists for a mailbox. 201 | ``getannotation``\ (*mailbox_name, entry_specifier, attribute_specifier*) 202 | Retrieve ANNOTATIONS. 203 | ``getquota``\ (*root*) 204 | Get the quota root's resource usage and limits. (Part of the IMAP4 205 | QUOTA extension defined in RFC2087.) 206 | ``getquotaroot``\ (*mailbox*) 207 | Get the list of quota roots for the named mailbox. 208 | ``id``\ (*field1, value1, ...*) 209 | IMAP4 ID extension: exchange information for problem analysis and 210 | determination. NB: a single argument is assumed to be correctly 211 | formatted and is passed through unchanged (for backward compatibility 212 | with earlier version). The ID extension is defined in RFC 2971. 213 | ``idle``\ (*timeout=*\ ``None``) 214 | Put server into IDLE mode until server notifies some change, or 215 | *timeout* (secs) occurs [default: 29 minutes], or another IMAP4 216 | command is scheduled. 217 | ``list``\ (*directory=*\ ``'""'``\ *, pattern=*\ ``'*'``) 218 | List mailbox names in directory matching pattern. Returned *data* is 219 | list of LIST responses. 220 | ``login``\ (*user, password*) 221 | Identify client using plaintext password. The *password* argument 222 | will be quoted. 223 | ``login_cram_md5``\ (*user, password*) 224 | Force use of CRAM-MD5 authentication. 225 | ``logout``\ () 226 | Shutdown connection to server. Returns server BYE response. NB: You 227 | must call this to shut down threads before discarding an instance. 228 | ``lsub``\ (*directory*\ =\ ``'""'``, *pattern*\ =\ ``'*'``) 229 | List *subscribed* mailbox names in directory matching pattern. 230 | Returned *data* are tuples of message part envelope and data. 231 | ``myrights``\ (*mailbox*) 232 | Show my Access Control Lists for *mailbox* (i.e. the rights that I 233 | have on *mailbox*). 234 | ``namespace``\ () 235 | Returns IMAP namespaces per RFC2342. 236 | ``noop``\ () 237 | Send NOOP command. 238 | ``partial``\ (*message_num, message_part, start, length*) 239 | Fetch truncated part of a message. Returned *data* is tuple of 240 | message part envelope and data. NB: obsolete. 241 | ``pop_untagged_responses``\ () 242 | (Helper method.) Generator for obtaining untagged responses. Returns 243 | and removes untagged responses in order of reception. Use at your own 244 | risk! (Removing untagged responses required by outstanding commands 245 | may cause errors.) 246 | ``proxyauth``\ (*user*) 247 | Assume authentication as *user*. (Allows an authorised administrator 248 | to proxy into any user's mailbox.) 249 | ``recent``\ () 250 | (Helper method.) Return RECENT responses if any exist, else prompt 251 | server for an update using the NOOP command. Returned *data* is 252 | ``None`` if no new messages, else list of RECENT responses, most 253 | recent last. 254 | ``rename``\ (*oldmailbox, newmailbox*) 255 | Rename old mailbox name to new. 256 | ``response``\ (*code*) 257 | (Helper method.) Return data for response *code* if received, or 258 | None. Response value is cleared. Returns the given *code* in place of 259 | the usual *type.* 260 | ``search``\ (*charset, criterium, ...*) 261 | Search mailbox for matching messages. Returned *data* contains a 262 | space separated list of matching message numbers. 263 | ``select``\ (*mailbox*\ ='INBOX', *readonly*\ =False) 264 | Select a mailbox. Flush all untagged responses. Returned *data* is 265 | count of messages in mailbox (EXISTS response). Mandated responses 266 | are ``'FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY'``, so other 267 | responses should be obtained by calling ``response('FLAGS')`` etc. 268 | ``setacl``\ (*mailbox, who, what*) 269 | Set the Access Control Lists for a mailbox. 270 | ``setannotation``\ (*mailbox_name, entry, attribute_value[, entry, attribute_value]\**) 271 | Set ANNOTATIONS. 272 | ``setquota``\ (*root, limits*) 273 | Set the quota root's resource limits. 274 | ``sort``\ (*sort_criteria, charset, search_criteria, ...*) 275 | IMAP4rev1 extension SORT command. 276 | ``starttls``\ (*keyfile*, *certfile*, *ca_certs*, *cert_verify_cb*, *ssl_version*\ ="ssl23", *tls_level*\ ="tls_compat") 277 | Start TLS negotiation as per RFC 2595. If non-null, *cert_verify_cb* 278 | will be called to verify the server certificate, with peer 279 | certificate and hostname as parameters. If *cert_verify_cb* returns a 280 | non-null response, an SSL exception will be raised with the response 281 | as reason. ``starttls`` should be called from user of the IMAP4 class 282 | after instantiation, as in: 283 | 284 | :: 285 | 286 | if 'STARTTLS' in imapobj.capabilities: 287 | imapobj.starttls() 288 | 289 | The recognized values for ``tls_level`` are: 290 | 291 | *tls_secure*: accept only TLS protocols recognized as "secure" 292 | *tls_no_ssl*: disable SSLv2 and SSLv3 support 293 | *tls_compat*: accept all SSL/TLS versions 294 | 295 | | 296 | 297 | ``status``\ (*mailbox, names*) 298 | 299 | Request named status conditions for mailbox. 300 | 301 | | 302 | 303 | ``store``\ (*message_set, command, flag_list*) 304 | 305 | Alters flag dispositions for messages in mailbox. 306 | 307 | | 308 | 309 | ``subscribe``\ (*mailbox*) 310 | 311 | Subscribe to new mailbox. 312 | 313 | | 314 | 315 | ``thread``\ (*threading_algorithm, charset, search_criteria, ...*) 316 | 317 | IMAP4rev1 extension THREAD command. 318 | 319 | | 320 | 321 | ``uid``\ (*command, arg, ...*) 322 | 323 | Execute ``command arg ...`` with messages identified by UID, rather than 324 | message number. Returns response appropriate to *command*. 325 | 326 | | 327 | 328 | ``unsubscribe``\ (*mailbox*) 329 | 330 | Unsubscribe from old mailbox. 331 | 332 | | 333 | 334 | ``xatom``\ (*command, arg, ...*) 335 | 336 | Allow simple extension commands as notified by server in CAPABILITY 337 | response. Returns response appropriate to *command*. 338 | 339 | ``IMAP4`` instances have a variable, ``PROTOCOL_VERSION``, that is set 340 | to the most recent supported protocol in the CAPABILITY response. 341 | 342 | Usage 343 | ----- 344 | 345 | Here is a minimal example (without error checking) that opens a mailbox 346 | and retrieves and prints all messages: 347 | 348 | :: 349 | 350 | def cb(cb_arg_list): 351 | response, cb_arg, error = cb_arg_list 352 | typ, data = response 353 | if not data: 354 | return 355 | for field in data: 356 | if type(field) is not tuple: 357 | continue 358 | print('Message %s:\n%s\n' 359 | % (field[0].split()[0], field[1])) 360 | 361 | import getpass, imaplib2 362 | M = imaplib2.IMAP4() 363 | M.LOGIN(getpass.getuser(), getpass.getpass()) 364 | M.SELECT(readonly=True) 365 | typ, data = M.SEARCH(None, 'ALL') 366 | for num in data[0].split(): 367 | M.FETCH(num, '(RFC822)', callback=cb) 368 | M.CLOSE() 369 | M.LOGOUT() 370 | 371 | Note that IMAP4 message numbers change as the mailbox changes, so it is 372 | highly advisable to use UIDs instead via the ``UID`` command. 373 | 374 | At the end of the module, there is a test section that contains a more 375 | extensive example of usage. 376 | 377 | References 378 | ---------- 379 | 380 | Documents describing the protocol, and sources and binaries for servers 381 | implementing it, can all be found at http://www.washington.edu/imap. 382 | 383 | 384 | Indices and tables 385 | ================== 386 | 387 | * :ref:`genindex` 388 | * :ref:`modindex` 389 | * :ref:`search` 390 | -------------------------------------------------------------------------------- /imaplib2/imaplib2.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Threaded IMAP4 client for Python 3. 4 | 5 | Based on RFC 3501 and original imaplib module. 6 | 7 | Public classes: IMAP4 8 | IMAP4_SSL 9 | IMAP4_stream 10 | 11 | Public functions: Internaldate2Time 12 | ParseFlags 13 | Time2Internaldate 14 | """ 15 | 16 | 17 | __all__ = ("IMAP4", "IMAP4_SSL", "IMAP4_stream", 18 | "Internaldate2Time", "ParseFlags", "Time2Internaldate", 19 | "Mon2num", "MonthNames", "InternalDate", "version") 20 | 21 | __version__ = "3.07" 22 | __release__ = "3" 23 | __revision__ = "07" 24 | __credits__ = """ 25 | Authentication code contributed by Donn Cave June 1998. 26 | String method conversion by ESR, February 2001. 27 | GET/SETACL contributed by Anthony Baxter April 2001. 28 | IMAP4_SSL contributed by Tino Lange March 2002. 29 | GET/SETQUOTA contributed by Andreas Zeidler June 2002. 30 | PROXYAUTH contributed by Rick Holbert November 2002. 31 | IDLE via threads suggested by Philippe Normand January 2005. 32 | GET/SETANNOTATION contributed by Tomas Lindroos June 2005. 33 | COMPRESS/DEFLATE contributed by Bron Gondwana May 2009. 34 | STARTTLS from Jython's imaplib by Alan Kennedy. 35 | ID contributed by Dave Baggett November 2009. 36 | Improved untagged responses handling suggested by Dave Baggett November 2009. 37 | Improved thread naming, and 0 read detection contributed by Grant Edwards June 2010. 38 | Improved timeout handling contributed by Ivan Vovnenko October 2010. 39 | Timeout handling further improved by Ethan Glasser-Camp December 2010. 40 | Time2Internaldate() patch to match RFC2060 specification of English month names from bugs.python.org/issue11024 March 2011. 41 | starttls() bug fixed with the help of Sebastian Spaeth April 2011. 42 | Threads now set the "daemon" flag (suggested by offlineimap-project) April 2011. 43 | Single quoting introduced with the help of Vladimir Marek August 2011. 44 | Support for specifying SSL version by Ryan Kavanagh July 2013. 45 | Fix for gmail "read 0" error provided by Jim Greenleaf August 2013. 46 | Fix for offlineimap "indexerror: string index out of range" bug provided by Eygene Ryabinkin August 2013. 47 | Fix for missing idle_lock in _handler() provided by Franklin Brook August 2014. 48 | Conversion to Python3 provided by F. Malina February 2015. 49 | Fix for READ-ONLY error from multiple EXAMINE/SELECT calls by Pierre-Louis Bonicoli March 2015. 50 | Fix for null strings appended to untagged responses by Pierre-Louis Bonicoli March 2015. 51 | Fix for correct byte encoding for _CRAM_MD5_AUTH taken from python3.5 imaplib.py June 2015. 52 | Fix for correct Python 3 exception handling by Tobias Brink August 2015. 53 | Fix to allow interruptible IDLE command by Tim Peoples September 2015. 54 | Add support for TLS levels by Ben Boeckel September 2015. 55 | Fix for shutown exception by Sebastien Gross November 2015. 56 | Fix for python3 conversion errata in _reader for poll by Mathias Ball August 2019.""" 57 | __author__ = "Piers Lauder " 58 | __URL__ = "https://github.com/jazzband/imaplib2" 59 | __license__ = "MIT" 60 | 61 | import binascii, calendar, errno, os, queue, random, re, select, socket, sys, time, threading, zlib 62 | 63 | 64 | select_module = select 65 | 66 | # Globals 67 | 68 | CRLF = b'\r\n' 69 | IMAP4_PORT = 143 70 | IMAP4_SSL_PORT = 993 71 | 72 | IDLE_TIMEOUT_RESPONSE = b'* IDLE TIMEOUT\r\n' 73 | IDLE_TIMEOUT = 60*29 # Don't stay in IDLE state longer 74 | READ_POLL_TIMEOUT = 30 # Without this timeout interrupted network connections can hang reader 75 | READ_SIZE = 32768 # Consume all available in socket 76 | 77 | DFLT_DEBUG_BUF_LVL = 3 # Level above which the logging output goes directly to stderr 78 | 79 | TLS_SECURE = "tls_secure" # Recognised TLS levels 80 | TLS_NO_SSL = "tls_no_ssl" 81 | TLS_COMPAT = "tls_compat" 82 | 83 | AllowedVersions = ('IMAP4REV1', 'IMAP4') # Most recent first 84 | 85 | # Commands 86 | 87 | CMD_VAL_STATES = 0 88 | CMD_VAL_ASYNC = 1 89 | NONAUTH, AUTH, SELECTED, LOGOUT = 'NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT' 90 | 91 | Commands = { 92 | # name valid states asynchronous 93 | 'APPEND': ((AUTH, SELECTED), False), 94 | 'AUTHENTICATE': ((NONAUTH,), False), 95 | 'CAPABILITY': ((NONAUTH, AUTH, SELECTED), True), 96 | 'CHECK': ((SELECTED,), True), 97 | 'CLOSE': ((SELECTED,), False), 98 | 'COMPRESS': ((AUTH,), False), 99 | 'COPY': ((SELECTED,), True), 100 | 'CREATE': ((AUTH, SELECTED), True), 101 | 'DELETE': ((AUTH, SELECTED), True), 102 | 'DELETEACL': ((AUTH, SELECTED), True), 103 | 'ENABLE': ((AUTH,), False), 104 | 'EXAMINE': ((AUTH, SELECTED), False), 105 | 'EXPUNGE': ((SELECTED,), True), 106 | 'FETCH': ((SELECTED,), True), 107 | 'GETACL': ((AUTH, SELECTED), True), 108 | 'GETANNOTATION':((AUTH, SELECTED), True), 109 | 'GETQUOTA': ((AUTH, SELECTED), True), 110 | 'GETQUOTAROOT': ((AUTH, SELECTED), True), 111 | 'ID': ((NONAUTH, AUTH, LOGOUT, SELECTED), True), 112 | 'IDLE': ((SELECTED,), False), 113 | 'LIST': ((AUTH, SELECTED), True), 114 | 'LOGIN': ((NONAUTH,), False), 115 | 'LOGOUT': ((NONAUTH, AUTH, LOGOUT, SELECTED), False), 116 | 'LSUB': ((AUTH, SELECTED), True), 117 | 'MYRIGHTS': ((AUTH, SELECTED), True), 118 | 'NAMESPACE': ((AUTH, SELECTED), True), 119 | 'NOOP': ((NONAUTH, AUTH, SELECTED), True), 120 | 'PARTIAL': ((SELECTED,), True), 121 | 'PROXYAUTH': ((AUTH,), False), 122 | 'RENAME': ((AUTH, SELECTED), True), 123 | 'SEARCH': ((SELECTED,), True), 124 | 'SELECT': ((AUTH, SELECTED), False), 125 | 'SETACL': ((AUTH, SELECTED), False), 126 | 'SETANNOTATION':((AUTH, SELECTED), True), 127 | 'SETQUOTA': ((AUTH, SELECTED), False), 128 | 'SORT': ((SELECTED,), True), 129 | 'STARTTLS': ((NONAUTH,), False), 130 | 'STATUS': ((AUTH, SELECTED), True), 131 | 'STORE': ((SELECTED,), True), 132 | 'SUBSCRIBE': ((AUTH, SELECTED), False), 133 | 'THREAD': ((SELECTED,), True), 134 | 'UID': ((SELECTED,), True), 135 | 'UNSUBSCRIBE': ((AUTH, SELECTED), False), 136 | } 137 | 138 | UID_direct = ('SEARCH', 'SORT', 'THREAD') 139 | 140 | 141 | def version(use_tuple=False): 142 | """ 143 | Return the version of this module, either as a single string (the default) or a :class:`tuple` 144 | of ``major_version, minor_version`` as integers 145 | 146 | :param use_tuple: Whether to return :class:`tuple` 147 | :type use_tuple: :class:`bool` 148 | :return: The version of this library, the format depends on the value of ``use_tuple`` 149 | :rtype: (:class:`int`, :class:`int`) if ``use_tuple`` 150 | :rtype: :class:`str` otherwise 151 | """ 152 | 153 | if use_tuple: 154 | return (int(__release__), int(__revision__)) 155 | 156 | return __version__ 157 | 158 | 159 | def Int2AP(num): 160 | 161 | """string = Int2AP(num) 162 | Return 'num' converted to bytes using characters from the set 'A'..'P' 163 | """ 164 | 165 | val = b''; AP = b'ABCDEFGHIJKLMNOP' 166 | num = int(abs(num)) 167 | while num: 168 | num, mod = divmod(num, 16) 169 | val = AP[mod:mod+1] + val 170 | return val 171 | 172 | 173 | 174 | class Request(object): 175 | 176 | """Private class to represent a request awaiting response.""" 177 | 178 | def __init__(self, parent, name=None, callback=None, cb_arg=None, cb_self=False): 179 | self.parent = parent 180 | self.name = name 181 | self.callback = callback # Function called to process result 182 | if not cb_self: 183 | self.callback_arg = cb_arg # Optional arg passed to "callback" 184 | else: 185 | self.callback_arg = (self, cb_arg) # Self reference required in callback arg 186 | 187 | self.tag = parent.tagpre + bytes(str(parent.tagnum), 'ASCII') 188 | parent.tagnum += 1 189 | 190 | self.ready = threading.Event() 191 | self.response = None 192 | self.aborted = None 193 | self.data = None 194 | 195 | 196 | def abort(self, typ, val): 197 | self.aborted = (typ, val) 198 | self.deliver(None) 199 | 200 | 201 | def get_response(self, exc_fmt=None): 202 | self.callback = None 203 | if __debug__: self.parent._log(3, '%s:%s.ready.wait' % (self.name, self.tag)) 204 | self.ready.wait() 205 | 206 | if self.aborted is not None: 207 | typ, val = self.aborted 208 | if exc_fmt is None: 209 | exc_fmt = '%s - %%s' % typ 210 | raise typ(exc_fmt % str(val)) 211 | 212 | return self.response 213 | 214 | 215 | def deliver(self, response): 216 | if self.callback is not None: 217 | self.callback((response, self.callback_arg, self.aborted)) 218 | return 219 | 220 | self.response = response 221 | self.ready.set() 222 | if __debug__: self.parent._log(3, '%s:%s.ready.set' % (self.name, self.tag)) 223 | 224 | 225 | 226 | 227 | class IMAP4(object): 228 | 229 | """Threaded IMAP4 client class. 230 | 231 | Instantiate with: 232 | IMAP4(host=None, port=None, debug=None, debug_file=None, identifier=None, timeout=None, debug_buf_lvl=None) 233 | 234 | host - host's name (default: localhost); 235 | port - port number (default: standard IMAP4 port); 236 | debug - debug level (default: 0 - no debug); 237 | debug_file - debug stream (default: sys.stderr); 238 | identifier - thread identifier prefix (default: host); 239 | timeout - timeout in seconds when expecting a command response (default: no timeout), 240 | debug_buf_lvl - debug level at which buffering is turned off. 241 | 242 | All IMAP4rev1 commands are supported by methods of the same name. 243 | 244 | Each command returns a tuple: (type, [data, ...]) where 'type' 245 | is usually 'OK' or 'NO', and 'data' is either the text from the 246 | tagged response, or untagged results from command. Each 'data' is 247 | either a string, or a tuple. If a tuple, then the first part is the 248 | header of the response, and the second part contains the data (ie: 249 | 'literal' value). 250 | 251 | Errors raise the exception class .error(""). 252 | IMAP4 server errors raise .abort(""), which is 253 | a sub-class of 'error'. Mailbox status changes from READ-WRITE to 254 | READ-ONLY raise the exception class .readonly(""), 255 | which is a sub-class of 'abort'. 256 | 257 | "error" exceptions imply a program error. 258 | "abort" exceptions imply the connection should be reset, and 259 | the command re-tried. 260 | "readonly" exceptions imply the command should be re-tried. 261 | 262 | All commands take two optional named arguments: 263 | 'callback' and 'cb_arg' 264 | If 'callback' is provided then the command is asynchronous, so after 265 | the command is queued for transmission, the call returns immediately 266 | with the tuple (None, None). 267 | The result will be posted by invoking "callback" with one arg, a tuple: 268 | callback((result, cb_arg, None)) 269 | or, if there was a problem: 270 | callback((None, cb_arg, (exception class, reason))) 271 | 272 | Otherwise the command is synchronous (waits for result). But note 273 | that state-changing commands will both block until previous commands 274 | have completed, and block subsequent commands until they have finished. 275 | 276 | All (non-callback) string arguments to commands are converted to bytes, 277 | except for AUTHENTICATE, and the last argument to APPEND which is 278 | passed as an IMAP4 literal. NB: the 'password' argument to the LOGIN 279 | command is always quoted. 280 | 281 | There is one instance variable, 'state', that is useful for tracking 282 | whether the client needs to login to the server. If it has the 283 | value "AUTH" after instantiating the class, then the connection 284 | is pre-authenticated (otherwise it will be "NONAUTH"). Selecting a 285 | mailbox changes the state to be "SELECTED", closing a mailbox changes 286 | back to "AUTH", and once the client has logged out, the state changes 287 | to "LOGOUT" and no further commands may be issued. 288 | 289 | Note: to use this module, you must read the RFCs pertaining to the 290 | IMAP4 protocol, as the semantics of the arguments to each IMAP4 291 | command are left to the invoker, not to mention the results. Also, 292 | most IMAP servers implement a sub-set of the commands available here. 293 | 294 | Note also that you must call logout() to shut down threads before 295 | discarding an instance. 296 | """ 297 | 298 | class error(Exception): pass # Logical errors - debug required 299 | class abort(error): pass # Service errors - close and retry 300 | class readonly(abort): pass # Mailbox status changed to READ-ONLY 301 | 302 | # These must be encoded according to utf8 setting in _mode_xxx(): 303 | _literal = br'.*{(?P\d+)}$' 304 | _untagged_status = br'\* (?P\d+) (?P[A-Z-]+)( (?P.*))?' 305 | 306 | continuation_cre = re.compile(br'\+( (?P.*))?') 307 | mapCRLF_cre = re.compile(br'\r\n|\r|\n') 308 | response_code_cre = re.compile(br'\[(?P[A-Z-]+)( (?P[^\]]*))?\]') 309 | untagged_response_cre = re.compile(br'\* (?P[A-Z-]+)( (?P.*))?') 310 | 311 | 312 | def __init__(self, host=None, port=None, debug=None, debug_file=None, identifier=None, timeout=None, debug_buf_lvl=None): 313 | 314 | self.state = NONAUTH # IMAP4 protocol state 315 | self.literal = None # A literal argument to a command 316 | self.tagged_commands = {} # Tagged commands awaiting response 317 | self.untagged_responses = [] # [[typ: [data, ...]], ...] 318 | self.mailbox = None # Current mailbox selected 319 | self.is_readonly = False # READ-ONLY desired state 320 | self.idle_rqb = None # Server IDLE Request - see _IdleCont 321 | self.idle_timeout = None # Must prod server occasionally 322 | 323 | self._expecting_data = False # Expecting message data 324 | self._expecting_data_len = 0 # How many characters we expect 325 | self._accumulated_data = [] # Message data accumulated so far 326 | self._literal_expected = None # Message data descriptor 327 | 328 | self.compressor = None # COMPRESS/DEFLATE if not None 329 | self.decompressor = None 330 | self._tls_established = False 331 | 332 | # Create unique tag for this session, 333 | # and compile tagged response matcher. 334 | 335 | self.tagnum = 0 336 | self.tagpre = Int2AP(random.randint(4096, 65535)) 337 | self.tagre = re.compile(br'(?P' 338 | + self.tagpre 339 | + br'\d+) (?P[A-Z]+) (?P.*)', re.ASCII) 340 | 341 | self._mode_ascii() 342 | 343 | if __debug__: self._init_debug(debug, debug_file, debug_buf_lvl) 344 | 345 | self.resp_timeout = timeout # Timeout waiting for command response 346 | 347 | if timeout is not None and timeout < READ_POLL_TIMEOUT: 348 | self.read_poll_timeout = timeout 349 | else: 350 | self.read_poll_timeout = READ_POLL_TIMEOUT 351 | self.read_size = READ_SIZE 352 | 353 | # Open socket to server. 354 | 355 | self.open(host, port) 356 | 357 | if __debug__: 358 | if debug: 359 | self._mesg('connected to %s on port %s' % (host, port)) 360 | 361 | # Threading 362 | 363 | if identifier is not None: 364 | self.identifier = identifier 365 | else: 366 | self.identifier = self.host 367 | if self.identifier: 368 | self.identifier += ' ' 369 | 370 | self.Terminate = self.TerminateReader = False 371 | 372 | self.state_change_free = threading.Event() 373 | self.state_change_pending = threading.Lock() 374 | self.commands_lock = threading.Lock() 375 | self.idle_lock = threading.Lock() 376 | 377 | self.ouq = queue.Queue(10) 378 | self.inq = queue.Queue() 379 | 380 | self.wrth = threading.Thread(target=self._writer) 381 | self.wrth.daemon = True 382 | self.wrth.start() 383 | self.rdth = threading.Thread(target=self._reader) 384 | self.rdth.daemon = True 385 | self.rdth.start() 386 | self.inth = threading.Thread(target=self._handler) 387 | self.inth.daemon = True 388 | self.inth.start() 389 | 390 | # Get server welcome message, 391 | # request and store CAPABILITY response. 392 | 393 | try: 394 | self.welcome = self._request_push(name='welcome', tag='continuation').get_response('IMAP4 protocol error: %s')[1] 395 | 396 | if self._get_untagged_response('PREAUTH'): 397 | self.state = AUTH 398 | if __debug__: self._log(1, 'state => AUTH') 399 | elif self._get_untagged_response('OK'): 400 | if __debug__: self._log(1, 'state => NONAUTH') 401 | else: 402 | raise self.error('unrecognised server welcome message: %s' % repr(self.welcome)) 403 | 404 | self._get_capabilities() 405 | if __debug__: self._log(1, 'CAPABILITY: %r' % (self.capabilities,)) 406 | 407 | for version in AllowedVersions: 408 | if not version in self.capabilities: 409 | continue 410 | self.PROTOCOL_VERSION = version 411 | break 412 | else: 413 | raise self.error('server not IMAP4 compliant') 414 | except: 415 | self._close_threads() 416 | raise 417 | 418 | 419 | def __getattr__(self, attr): 420 | # Allow UPPERCASE variants of IMAP4 command methods. 421 | if attr in Commands: 422 | return getattr(self, attr.lower()) 423 | raise AttributeError("Unknown IMAP4 command: '%s'" % attr) 424 | 425 | 426 | def __enter__(self): 427 | return self 428 | 429 | def __exit__(self, *args): 430 | try: 431 | self.logout() 432 | except OSError: 433 | pass 434 | 435 | 436 | def _mode_ascii(self): 437 | self.utf8_enabled = False 438 | self._encoding = 'ascii' 439 | self.literal_cre = re.compile(self._literal, re.ASCII) 440 | self.untagged_status_cre = re.compile(self._untagged_status, re.ASCII) 441 | 442 | 443 | def _mode_utf8(self): 444 | self.utf8_enabled = True 445 | self._encoding = 'utf-8' 446 | self.literal_cre = re.compile(self._literal) 447 | self.untagged_status_cre = re.compile(self._untagged_status) 448 | 449 | 450 | 451 | # Overridable methods 452 | 453 | 454 | def open(self, host=None, port=None): 455 | """open(host=None, port=None) 456 | Setup connection to remote server on "host:port" 457 | (default: localhost:standard IMAP4 port). 458 | This connection will be used by the routines: 459 | read, send, shutdown, socket.""" 460 | 461 | self.host = self._choose_nonull_or_dflt('', host) 462 | self.port = self._choose_nonull_or_dflt(IMAP4_PORT, port) 463 | self.sock = self.open_socket() 464 | self.read_fd = self.sock.fileno() 465 | 466 | 467 | def open_socket(self): 468 | """open_socket() 469 | Open socket choosing first address family available.""" 470 | 471 | return socket.create_connection((self.host, self.port), self.resp_timeout) 472 | 473 | 474 | def ssl_wrap_socket(self): 475 | 476 | try: 477 | import ssl 478 | 479 | TLS_MAP = {} 480 | if hasattr(ssl, "PROTOCOL_TLSv1_2"): 481 | TLS_MAP[TLS_SECURE] = { 482 | "tls1_2": ssl.PROTOCOL_TLSv1_2, 483 | "tls1_1": ssl.PROTOCOL_TLSv1_1, 484 | } 485 | else: 486 | TLS_MAP[TLS_SECURE] = {} 487 | TLS_MAP[TLS_NO_SSL] = TLS_MAP[TLS_SECURE].copy() 488 | TLS_MAP[TLS_NO_SSL].update({ 489 | "tls1": ssl.PROTOCOL_TLSv1, 490 | }) 491 | TLS_MAP[TLS_COMPAT] = TLS_MAP[TLS_NO_SSL].copy() 492 | TLS_MAP[TLS_COMPAT].update({ 493 | "ssl23": ssl.PROTOCOL_SSLv23, 494 | None: ssl.PROTOCOL_SSLv23, 495 | }) 496 | if hasattr(ssl, "PROTOCOL_SSLv3"): # Might not be available. 497 | TLS_MAP[TLS_COMPAT].update({ 498 | "ssl3": ssl.PROTOCOL_SSLv3 499 | }) 500 | 501 | if self.ca_certs is not None: 502 | cert_reqs = ssl.CERT_REQUIRED 503 | else: 504 | cert_reqs = ssl.CERT_NONE 505 | 506 | if self.tls_level not in TLS_MAP: 507 | raise RuntimeError("unknown tls_level: %s" % self.tls_level) 508 | 509 | if self.ssl_version not in TLS_MAP[self.tls_level]: 510 | raise socket.sslerror("Invalid SSL version '%s' requested for tls_version '%s'" % (self.ssl_version, self.tls_level)) 511 | 512 | ssl_version = TLS_MAP[self.tls_level][self.ssl_version] 513 | 514 | if getattr(ssl, 'HAS_SNI', False): 515 | ctx = ssl.SSLContext(ssl_version) 516 | ctx.verify_mode = cert_reqs 517 | if self.ca_certs is not None: 518 | ctx.load_verify_locations(self.ca_certs) 519 | if self.certfile or self.keyfile: 520 | ctx.load_cert_chain(self.certfile, self.keyfile) 521 | self.sock = ctx.wrap_socket(self.sock, server_hostname=self.host) 522 | else: 523 | self.sock = ssl.wrap_socket(self.sock, self.keyfile, self.certfile, ca_certs=self.ca_certs, cert_reqs=cert_reqs, ssl_version=ssl_version) 524 | 525 | ssl_exc = ssl.SSLError 526 | self.read_fd = self.sock.fileno() 527 | except ImportError: 528 | # No ssl module, and socket.ssl has no fileno(), and does not allow certificate verification 529 | raise socket.sslerror("imaplib SSL mode does not work without ssl module") 530 | 531 | if self.cert_verify_cb is not None: 532 | cert_err = self.cert_verify_cb(self.sock.getpeercert(), self.host) 533 | if cert_err: 534 | raise ssl_exc(cert_err) 535 | 536 | # Allow sending of keep-alive messages - seems to prevent some servers 537 | # from closing SSL, leading to deadlocks. 538 | self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) 539 | 540 | 541 | 542 | def start_compressing(self): 543 | """start_compressing() 544 | Enable deflate compression on the socket (RFC 4978).""" 545 | 546 | # rfc 1951 - pure DEFLATE, so use -15 for both windows 547 | self.decompressor = zlib.decompressobj(-15) 548 | self.compressor = zlib.compressobj(zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -15) 549 | 550 | 551 | def read(self, size): 552 | """data = read(size) 553 | Read at most 'size' bytes from remote.""" 554 | 555 | if self.decompressor is None: 556 | return self.sock.recv(size) 557 | 558 | if self.decompressor.unconsumed_tail: 559 | data = self.decompressor.unconsumed_tail 560 | else: 561 | data = self.sock.recv(READ_SIZE) 562 | 563 | return self.decompressor.decompress(data, size) 564 | 565 | 566 | def send(self, data): 567 | """send(data) 568 | Send 'data' to remote.""" 569 | 570 | if self.compressor is not None: 571 | data = self.compressor.compress(data) 572 | data += self.compressor.flush(zlib.Z_SYNC_FLUSH) 573 | 574 | self.sock.sendall(data) 575 | 576 | 577 | def shutdown(self): 578 | """shutdown() 579 | Close I/O established in "open".""" 580 | 581 | try: 582 | self.sock.shutdown(socket.SHUT_RDWR) 583 | except Exception as e: 584 | # The server might already have closed the connection 585 | if e.errno != errno.ENOTCONN: 586 | raise 587 | finally: 588 | self.sock.close() 589 | 590 | 591 | def socket(self): 592 | """socket = socket() 593 | Return socket instance used to connect to IMAP4 server.""" 594 | 595 | return self.sock 596 | 597 | 598 | 599 | # Utility methods 600 | 601 | 602 | def enable_compression(self): 603 | """enable_compression() 604 | Ask the server to start compressing the connection. 605 | Should be called from user of this class after instantiation, as in: 606 | if 'COMPRESS=DEFLATE' in imapobj.capabilities: 607 | imapobj.enable_compression()""" 608 | 609 | try: 610 | typ, dat = self._simple_command('COMPRESS', 'DEFLATE') 611 | if typ == 'OK': 612 | self.start_compressing() 613 | if __debug__: self._log(1, 'Enabled COMPRESS=DEFLATE') 614 | finally: 615 | self._release_state_change() 616 | 617 | 618 | def pop_untagged_responses(self): 619 | """ for typ,data in pop_untagged_responses(): pass 620 | Generator for any remaining untagged responses. 621 | Returns and removes untagged responses in order of reception. 622 | Use at your own risk!""" 623 | 624 | while self.untagged_responses: 625 | self.commands_lock.acquire() 626 | try: 627 | yield self.untagged_responses.pop(0) 628 | finally: 629 | self.commands_lock.release() 630 | 631 | 632 | def recent(self, **kw): 633 | """(typ, [data]) = recent() 634 | Return 'RECENT' responses if any exist, 635 | else prompt server for an update using the 'NOOP' command. 636 | 'data' is None if no new messages, 637 | else list of RECENT responses, most recent last.""" 638 | 639 | name = 'RECENT' 640 | typ, dat = self._untagged_response(None, [None], name) 641 | if dat != [None]: 642 | return self._deliver_dat(typ, dat, kw) 643 | kw['untagged_response'] = name 644 | return self.noop(**kw) # Prod server for response 645 | 646 | 647 | def response(self, code, **kw): 648 | """(code, [data]) = response(code) 649 | Return data for response 'code' if received, or None. 650 | Old value for response 'code' is cleared.""" 651 | 652 | typ, dat = self._untagged_response(code, [None], code.upper()) 653 | return self._deliver_dat(typ, dat, kw) 654 | 655 | 656 | 657 | 658 | # IMAP4 commands 659 | 660 | 661 | def append(self, mailbox, flags, date_time, message, **kw): 662 | """(typ, [data]) = append(mailbox, flags, date_time, message) 663 | Append message to named mailbox. 664 | All args except `message' can be None.""" 665 | 666 | name = 'APPEND' 667 | if not mailbox: 668 | mailbox = 'INBOX' 669 | if flags: 670 | if (flags[0],flags[-1]) != ('(',')'): 671 | flags = '(%s)' % flags 672 | else: 673 | flags = None 674 | if date_time: 675 | date_time = Time2Internaldate(date_time) 676 | else: 677 | date_time = None 678 | if isinstance(message, str): 679 | message = bytes(message, 'ASCII') 680 | literal = self.mapCRLF_cre.sub(CRLF, message) 681 | if self.utf8_enabled: 682 | literal = b'UTF8 (' + literal + b')' 683 | self.literal = literal 684 | try: 685 | return self._simple_command(name, mailbox, flags, date_time, **kw) 686 | finally: 687 | self._release_state_change() 688 | 689 | 690 | def authenticate(self, mechanism, authobject, **kw): 691 | """(typ, [data]) = authenticate(mechanism, authobject) 692 | Authenticate command - requires response processing. 693 | 694 | 'mechanism' specifies which authentication mechanism is to 695 | be used - it must appear in .capabilities in the 696 | form AUTH=. 697 | 698 | 'authobject' must be a callable object: 699 | 700 | data = authobject(response) 701 | 702 | It will be called to process server continuation responses, 703 | the 'response' argument will be a 'bytes'. It should return 704 | bytes that will be encoded and sent to server. It should 705 | return None if the client abort response '*' should be sent 706 | instead.""" 707 | 708 | self.literal = _Authenticator(authobject).process 709 | try: 710 | typ, dat = self._simple_command('AUTHENTICATE', mechanism.upper()) 711 | if typ != 'OK': 712 | self._deliver_exc(self.error, dat[-1], kw) 713 | self.state = AUTH 714 | if __debug__: self._log(1, 'state => AUTH') 715 | finally: 716 | self._release_state_change() 717 | return self._deliver_dat(typ, dat, kw) 718 | 719 | 720 | def capability(self, **kw): 721 | """(typ, [data]) = capability() 722 | Fetch capabilities list from server.""" 723 | 724 | name = 'CAPABILITY' 725 | kw['untagged_response'] = name 726 | return self._simple_command(name, **kw) 727 | 728 | 729 | def check(self, **kw): 730 | """(typ, [data]) = check() 731 | Checkpoint mailbox on server.""" 732 | 733 | return self._simple_command('CHECK', **kw) 734 | 735 | 736 | def close(self, **kw): 737 | """(typ, [data]) = close() 738 | Close currently selected mailbox. 739 | 740 | Deleted messages are removed from writable mailbox. 741 | This is the recommended command before 'LOGOUT'.""" 742 | 743 | if self.state != 'SELECTED': 744 | raise self.error('No mailbox selected.') 745 | try: 746 | typ, dat = self._simple_command('CLOSE') 747 | finally: 748 | self.state = AUTH 749 | if __debug__: self._log(1, 'state => AUTH') 750 | self._release_state_change() 751 | return self._deliver_dat(typ, dat, kw) 752 | 753 | 754 | def copy(self, message_set, new_mailbox, **kw): 755 | """(typ, [data]) = copy(message_set, new_mailbox) 756 | Copy 'message_set' messages onto end of 'new_mailbox'.""" 757 | 758 | return self._simple_command('COPY', message_set, new_mailbox, **kw) 759 | 760 | 761 | def create(self, mailbox, **kw): 762 | """(typ, [data]) = create(mailbox) 763 | Create new mailbox.""" 764 | 765 | return self._simple_command('CREATE', mailbox, **kw) 766 | 767 | 768 | def delete(self, mailbox, **kw): 769 | """(typ, [data]) = delete(mailbox) 770 | Delete old mailbox.""" 771 | 772 | return self._simple_command('DELETE', mailbox, **kw) 773 | 774 | 775 | def deleteacl(self, mailbox, who, **kw): 776 | """(typ, [data]) = deleteacl(mailbox, who) 777 | Delete the ACLs (remove any rights) set for who on mailbox.""" 778 | 779 | return self._simple_command('DELETEACL', mailbox, who, **kw) 780 | 781 | 782 | def enable(self, capability): 783 | """Send an RFC5161 enable string to the server. 784 | 785 | (typ, [data]) = .enable(capability) 786 | """ 787 | if 'ENABLE' not in self.capabilities: 788 | raise self.error("Server does not support ENABLE") 789 | typ, data = self._simple_command('ENABLE', capability) 790 | if typ == 'OK' and 'UTF8=ACCEPT' in capability.upper(): 791 | self._mode_utf8() 792 | return typ, data 793 | 794 | 795 | def examine(self, mailbox='INBOX', **kw): 796 | """(typ, [data]) = examine(mailbox='INBOX') 797 | Select a mailbox for READ-ONLY access. (Flushes all untagged responses.) 798 | 'data' is count of messages in mailbox ('EXISTS' response). 799 | Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY'), so 800 | other responses should be obtained via "response('FLAGS')" etc.""" 801 | 802 | return self.select(mailbox=mailbox, readonly=True, **kw) 803 | 804 | 805 | def expunge(self, **kw): 806 | """(typ, [data]) = expunge() 807 | Permanently remove deleted items from selected mailbox. 808 | Generates 'EXPUNGE' response for each deleted message. 809 | 'data' is list of 'EXPUNGE'd message numbers in order received.""" 810 | 811 | name = 'EXPUNGE' 812 | kw['untagged_response'] = name 813 | return self._simple_command(name, **kw) 814 | 815 | 816 | def fetch(self, message_set, message_parts, **kw): 817 | """(typ, [data, ...]) = fetch(message_set, message_parts) 818 | Fetch (parts of) messages. 819 | 'message_parts' should be a string of selected parts 820 | enclosed in parentheses, eg: "(UID BODY[TEXT])". 821 | 'data' are tuples of message part envelope and data, 822 | followed by a string containing the trailer.""" 823 | 824 | name = 'FETCH' 825 | kw['untagged_response'] = name 826 | return self._simple_command(name, message_set, message_parts, **kw) 827 | 828 | 829 | def getacl(self, mailbox, **kw): 830 | """(typ, [data]) = getacl(mailbox) 831 | Get the ACLs for a mailbox.""" 832 | 833 | kw['untagged_response'] = 'ACL' 834 | return self._simple_command('GETACL', mailbox, **kw) 835 | 836 | 837 | def getannotation(self, mailbox, entry, attribute, **kw): 838 | """(typ, [data]) = getannotation(mailbox, entry, attribute) 839 | Retrieve ANNOTATIONs.""" 840 | 841 | kw['untagged_response'] = 'ANNOTATION' 842 | return self._simple_command('GETANNOTATION', mailbox, entry, attribute, **kw) 843 | 844 | 845 | def getquota(self, root, **kw): 846 | """(typ, [data]) = getquota(root) 847 | Get the quota root's resource usage and limits. 848 | (Part of the IMAP4 QUOTA extension defined in rfc2087.)""" 849 | 850 | kw['untagged_response'] = 'QUOTA' 851 | return self._simple_command('GETQUOTA', root, **kw) 852 | 853 | 854 | def getquotaroot(self, mailbox, **kw): 855 | # Hmmm, this is non-std! Left for backwards-compatibility, sigh. 856 | # NB: usage should have been defined as: 857 | # (typ, [QUOTAROOT responses...]) = getquotaroot(mailbox) 858 | # (typ, [QUOTA responses...]) = response('QUOTA') 859 | """(typ, [[QUOTAROOT responses...], [QUOTA responses...]]) = getquotaroot(mailbox) 860 | Get the list of quota roots for the named mailbox.""" 861 | 862 | typ, dat = self._simple_command('GETQUOTAROOT', mailbox) 863 | typ, quota = self._untagged_response(typ, dat, 'QUOTA') 864 | typ, quotaroot = self._untagged_response(typ, dat, 'QUOTAROOT') 865 | return self._deliver_dat(typ, [quotaroot, quota], kw) 866 | 867 | 868 | def id(self, *kv_pairs, **kw): 869 | """(typ, [data]) = .id(kv_pairs) 870 | 'kv_pairs' is a possibly empty list of keys and values. 871 | 'data' is a list of ID key value pairs or NIL. 872 | NB: a single argument is assumed to be correctly formatted and is passed through unchanged 873 | (for backward compatibility with earlier version). 874 | Exchange information for problem analysis and determination. 875 | The ID extension is defined in RFC 2971. """ 876 | 877 | name = 'ID' 878 | kw['untagged_response'] = name 879 | 880 | if not kv_pairs: 881 | data = 'NIL' 882 | elif len(kv_pairs) == 1: 883 | data = kv_pairs[0] # Assume invoker passing correctly formatted string (back-compat) 884 | else: 885 | data = '(%s)' % ' '.join([(arg and self._quote(arg) or 'NIL') for arg in kv_pairs]) 886 | 887 | return self._simple_command(name, data, **kw) 888 | 889 | 890 | def idle(self, timeout=None, **kw): 891 | """"(typ, [data]) = idle(timeout=None) 892 | Put server into IDLE mode until server notifies some change, 893 | or 'timeout' (secs) occurs (default: 29 minutes), 894 | or another IMAP4 command is scheduled.""" 895 | 896 | name = 'IDLE' 897 | self.literal = _IdleCont(self, timeout).process 898 | try: 899 | return self._simple_command(name, **kw) 900 | finally: 901 | self._release_state_change() 902 | 903 | 904 | def list(self, directory='""', pattern='*', **kw): 905 | """(typ, [data]) = list(directory='""', pattern='*') 906 | List mailbox names in directory matching pattern. 907 | 'data' is list of LIST responses. 908 | 909 | NB: for 'pattern': 910 | % matches all except separator ( so LIST "" "%" returns names at root) 911 | * matches all (so LIST "" "*" returns whole directory tree from root)""" 912 | 913 | name = 'LIST' 914 | kw['untagged_response'] = name 915 | return self._simple_command(name, directory, pattern, **kw) 916 | 917 | 918 | def login(self, user, password, **kw): 919 | """(typ, [data]) = login(user, password) 920 | Identify client using plaintext password. 921 | NB: 'password' will be quoted.""" 922 | 923 | try: 924 | typ, dat = self._simple_command('LOGIN', user, self._quote(password)) 925 | if typ != 'OK': 926 | self._deliver_exc(self.error, dat[-1], kw) 927 | self.state = AUTH 928 | if __debug__: self._log(1, 'state => AUTH') 929 | finally: 930 | self._release_state_change() 931 | return self._deliver_dat(typ, dat, kw) 932 | 933 | 934 | def login_cram_md5(self, user, password, **kw): 935 | """(typ, [data]) = login_cram_md5(user, password) 936 | Force use of CRAM-MD5 authentication.""" 937 | 938 | self.user, self.password = user, password 939 | return self.authenticate('CRAM-MD5', self._CRAM_MD5_AUTH, **kw) 940 | 941 | 942 | def _CRAM_MD5_AUTH(self, challenge): 943 | """Authobject to use with CRAM-MD5 authentication.""" 944 | import hmac 945 | pwd = (self.password.encode('utf-8') if isinstance(self.password, str) 946 | else self.password) 947 | return self.user + " " + hmac.HMAC(pwd, challenge, 'md5').hexdigest() 948 | 949 | 950 | def logout(self, **kw): 951 | """(typ, [data]) = logout() 952 | Shutdown connection to server. 953 | Returns server 'BYE' response. 954 | NB: You must call this to shut down threads before discarding an instance.""" 955 | 956 | self.state = LOGOUT 957 | if __debug__: self._log(1, 'state => LOGOUT') 958 | 959 | try: 960 | try: 961 | typ, dat = self._simple_command('LOGOUT') 962 | except: 963 | typ, dat = 'NO', ['%s: %s' % sys.exc_info()[:2]] 964 | if __debug__: self._log(1, dat) 965 | 966 | self._close_threads() 967 | finally: 968 | self._release_state_change() 969 | 970 | if __debug__: self._log(1, 'connection closed') 971 | 972 | bye = self._get_untagged_response('BYE', leave=True) 973 | if bye: 974 | typ, dat = 'BYE', bye 975 | return self._deliver_dat(typ, dat, kw) 976 | 977 | 978 | def lsub(self, directory='""', pattern='*', **kw): 979 | """(typ, [data, ...]) = lsub(directory='""', pattern='*') 980 | List 'subscribed' mailbox names in directory matching pattern. 981 | 'data' are tuples of message part envelope and data.""" 982 | 983 | name = 'LSUB' 984 | kw['untagged_response'] = name 985 | return self._simple_command(name, directory, pattern, **kw) 986 | 987 | 988 | def myrights(self, mailbox, **kw): 989 | """(typ, [data]) = myrights(mailbox) 990 | Show my ACLs for a mailbox (i.e. the rights that I have on mailbox).""" 991 | 992 | name = 'MYRIGHTS' 993 | kw['untagged_response'] = name 994 | return self._simple_command(name, mailbox, **kw) 995 | 996 | 997 | def namespace(self, **kw): 998 | """(typ, [data, ...]) = namespace() 999 | Returns IMAP namespaces ala rfc2342.""" 1000 | 1001 | name = 'NAMESPACE' 1002 | kw['untagged_response'] = name 1003 | return self._simple_command(name, **kw) 1004 | 1005 | 1006 | def noop(self, **kw): 1007 | """(typ, [data]) = noop() 1008 | Send NOOP command.""" 1009 | 1010 | if __debug__: self._dump_ur(3) 1011 | return self._simple_command('NOOP', **kw) 1012 | 1013 | 1014 | def partial(self, message_num, message_part, start, length, **kw): 1015 | """(typ, [data, ...]) = partial(message_num, message_part, start, length) 1016 | Fetch truncated part of a message. 1017 | 'data' is tuple of message part envelope and data. 1018 | NB: obsolete.""" 1019 | 1020 | name = 'PARTIAL' 1021 | kw['untagged_response'] = 'FETCH' 1022 | return self._simple_command(name, message_num, message_part, start, length, **kw) 1023 | 1024 | 1025 | def proxyauth(self, user, **kw): 1026 | """(typ, [data]) = proxyauth(user) 1027 | Assume authentication as 'user'. 1028 | (Allows an authorised administrator to proxy into any user's mailbox.)""" 1029 | 1030 | try: 1031 | return self._simple_command('PROXYAUTH', user, **kw) 1032 | finally: 1033 | self._release_state_change() 1034 | 1035 | 1036 | def rename(self, oldmailbox, newmailbox, **kw): 1037 | """(typ, [data]) = rename(oldmailbox, newmailbox) 1038 | Rename old mailbox name to new.""" 1039 | 1040 | return self._simple_command('RENAME', oldmailbox, newmailbox, **kw) 1041 | 1042 | 1043 | def search(self, charset, *criteria, **kw): 1044 | """(typ, [data]) = search(charset, criterion, ...) 1045 | Search mailbox for matching messages. 1046 | If UTF8 is enabled, charset MUST be None. 1047 | 'data' is space separated list of matching message numbers.""" 1048 | 1049 | name = 'SEARCH' 1050 | kw['untagged_response'] = name 1051 | if charset: 1052 | if self.utf8_enabled: 1053 | raise self.error("Non-None charset not valid in UTF8 mode") 1054 | return self._simple_command(name, 'CHARSET', charset, *criteria, **kw) 1055 | return self._simple_command(name, *criteria, **kw) 1056 | 1057 | 1058 | def select(self, mailbox='INBOX', readonly=False, **kw): 1059 | """(typ, [data]) = select(mailbox='INBOX', readonly=False) 1060 | Select a mailbox. (Flushes all untagged responses.) 1061 | 'data' is count of messages in mailbox ('EXISTS' response). 1062 | Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY'), so 1063 | other responses should be obtained via "response('FLAGS')" etc.""" 1064 | 1065 | self.mailbox = mailbox 1066 | 1067 | self.is_readonly = bool(readonly) 1068 | if readonly: 1069 | name = 'EXAMINE' 1070 | else: 1071 | name = 'SELECT' 1072 | try: 1073 | rqb = self._command(name, mailbox) 1074 | typ, dat = rqb.get_response('command: %s => %%s' % rqb.name) 1075 | if typ != 'OK': 1076 | if self.state == SELECTED: 1077 | self.state = AUTH 1078 | if __debug__: self._log(1, 'state => AUTH') 1079 | if typ == 'BAD': 1080 | self._deliver_exc(self.error, '%s command error: %s %s. Data: %.100s' % (name, typ, dat, mailbox), kw) 1081 | return self._deliver_dat(typ, dat, kw) 1082 | self.state = SELECTED 1083 | if __debug__: self._log(1, 'state => SELECTED') 1084 | finally: 1085 | self._release_state_change() 1086 | 1087 | if self._get_untagged_response('READ-ONLY', leave=True) and not readonly: 1088 | if __debug__: self._dump_ur(1) 1089 | self._deliver_exc(self.readonly, '%s is not writable' % mailbox, kw) 1090 | typ, dat = self._untagged_response(typ, [None], 'EXISTS') 1091 | return self._deliver_dat(typ, dat, kw) 1092 | 1093 | 1094 | def setacl(self, mailbox, who, what, **kw): 1095 | """(typ, [data]) = setacl(mailbox, who, what) 1096 | Set a mailbox acl.""" 1097 | 1098 | try: 1099 | return self._simple_command('SETACL', mailbox, who, what, **kw) 1100 | finally: 1101 | self._release_state_change() 1102 | 1103 | 1104 | def setannotation(self, *args, **kw): 1105 | """(typ, [data]) = setannotation(mailbox[, entry, attribute]+) 1106 | Set ANNOTATIONs.""" 1107 | 1108 | kw['untagged_response'] = 'ANNOTATION' 1109 | return self._simple_command('SETANNOTATION', *args, **kw) 1110 | 1111 | 1112 | def setquota(self, root, limits, **kw): 1113 | """(typ, [data]) = setquota(root, limits) 1114 | Set the quota root's resource limits.""" 1115 | 1116 | kw['untagged_response'] = 'QUOTA' 1117 | try: 1118 | return self._simple_command('SETQUOTA', root, limits, **kw) 1119 | finally: 1120 | self._release_state_change() 1121 | 1122 | 1123 | def sort(self, sort_criteria, charset, *search_criteria, **kw): 1124 | """(typ, [data]) = sort(sort_criteria, charset, search_criteria, ...) 1125 | IMAP4rev1 extension SORT command.""" 1126 | 1127 | name = 'SORT' 1128 | if (sort_criteria[0],sort_criteria[-1]) != ('(',')'): 1129 | sort_criteria = '(%s)' % sort_criteria 1130 | kw['untagged_response'] = name 1131 | return self._simple_command(name, sort_criteria, charset, *search_criteria, **kw) 1132 | 1133 | 1134 | def starttls(self, keyfile=None, certfile=None, ca_certs=None, cert_verify_cb=None, ssl_version="ssl23", tls_level=TLS_COMPAT, **kw): 1135 | """(typ, [data]) = starttls(keyfile=None, certfile=None, ca_certs=None, cert_verify_cb=None, ssl_version="ssl23", tls_level="tls_compat") 1136 | Start TLS negotiation as per RFC 2595.""" 1137 | 1138 | name = 'STARTTLS' 1139 | 1140 | if name not in self.capabilities: 1141 | raise self.abort('TLS not supported by server') 1142 | 1143 | if self._tls_established: 1144 | raise self.abort('TLS session already established') 1145 | 1146 | # Must now shutdown reader thread after next response, and restart after changing read_fd 1147 | 1148 | self.read_size = 1 # Don't consume TLS handshake 1149 | self.TerminateReader = True 1150 | 1151 | try: 1152 | typ, dat = self._simple_command(name) 1153 | finally: 1154 | self._release_state_change() 1155 | self.rdth.join() 1156 | self.TerminateReader = False 1157 | self.read_size = READ_SIZE 1158 | 1159 | if typ != 'OK': 1160 | # Restart reader thread and error 1161 | self.rdth = threading.Thread(target=self._reader) 1162 | self.rdth.daemon = True 1163 | self.rdth.start() 1164 | raise self.error("Couldn't establish TLS session: %s" % dat) 1165 | 1166 | self.keyfile = keyfile 1167 | self.certfile = certfile 1168 | self.ca_certs = ca_certs 1169 | self.cert_verify_cb = cert_verify_cb 1170 | self.ssl_version = ssl_version 1171 | self.tls_level = tls_level 1172 | 1173 | try: 1174 | self.ssl_wrap_socket() 1175 | finally: 1176 | # Restart reader thread 1177 | self.rdth = threading.Thread(target=self._reader) 1178 | self.rdth.daemon = True 1179 | self.rdth.start() 1180 | 1181 | self._get_capabilities() 1182 | 1183 | self._tls_established = True 1184 | 1185 | typ, dat = self._untagged_response(typ, dat, name) 1186 | return self._deliver_dat(typ, dat, kw) 1187 | 1188 | 1189 | def status(self, mailbox, names, **kw): 1190 | """(typ, [data]) = status(mailbox, names) 1191 | Request named status conditions for mailbox.""" 1192 | 1193 | name = 'STATUS' 1194 | kw['untagged_response'] = name 1195 | return self._simple_command(name, mailbox, names, **kw) 1196 | 1197 | 1198 | def store(self, message_set, command, flags, **kw): 1199 | """(typ, [data]) = store(message_set, command, flags) 1200 | Alters flag dispositions for messages in mailbox.""" 1201 | 1202 | if (flags[0],flags[-1]) != ('(',')'): 1203 | flags = '(%s)' % flags # Avoid quoting the flags 1204 | kw['untagged_response'] = 'FETCH' 1205 | return self._simple_command('STORE', message_set, command, flags, **kw) 1206 | 1207 | 1208 | def subscribe(self, mailbox, **kw): 1209 | """(typ, [data]) = subscribe(mailbox) 1210 | Subscribe to new mailbox.""" 1211 | 1212 | try: 1213 | return self._simple_command('SUBSCRIBE', mailbox, **kw) 1214 | finally: 1215 | self._release_state_change() 1216 | 1217 | 1218 | def thread(self, threading_algorithm, charset, *search_criteria, **kw): 1219 | """(type, [data]) = thread(threading_alogrithm, charset, search_criteria, ...) 1220 | IMAPrev1 extension THREAD command.""" 1221 | 1222 | name = 'THREAD' 1223 | kw['untagged_response'] = name 1224 | return self._simple_command(name, threading_algorithm, charset, *search_criteria, **kw) 1225 | 1226 | 1227 | def uid(self, command, *args, **kw): 1228 | """(typ, [data]) = uid(command, arg, ...) 1229 | Execute "command arg ..." with messages identified by UID, 1230 | rather than message number. 1231 | Assumes 'command' is legal in current state. 1232 | Returns response appropriate to 'command'.""" 1233 | 1234 | command = command.upper() 1235 | if command in UID_direct: 1236 | resp = command 1237 | else: 1238 | resp = 'FETCH' 1239 | kw['untagged_response'] = resp 1240 | return self._simple_command('UID', command, *args, **kw) 1241 | 1242 | 1243 | def unsubscribe(self, mailbox, **kw): 1244 | """(typ, [data]) = unsubscribe(mailbox) 1245 | Unsubscribe from old mailbox.""" 1246 | 1247 | try: 1248 | return self._simple_command('UNSUBSCRIBE', mailbox, **kw) 1249 | finally: 1250 | self._release_state_change() 1251 | 1252 | 1253 | def xatom(self, name, *args, **kw): 1254 | """(typ, [data]) = xatom(name, arg, ...) 1255 | Allow simple extension commands notified by server in CAPABILITY response. 1256 | Assumes extension command 'name' is legal in current state. 1257 | Returns response appropriate to extension command 'name'.""" 1258 | 1259 | name = name.upper() 1260 | if not name in Commands: 1261 | Commands[name] = ((self.state,), False) 1262 | try: 1263 | return self._simple_command(name, *args, **kw) 1264 | finally: 1265 | self._release_state_change() 1266 | 1267 | 1268 | 1269 | # Internal methods 1270 | 1271 | 1272 | def _append_untagged(self, typ, dat): 1273 | 1274 | # Append new 'dat' to end of last untagged response if same 'typ', 1275 | # else append new response. 1276 | 1277 | if dat is None: dat = b'' 1278 | 1279 | self.commands_lock.acquire() 1280 | 1281 | if self.untagged_responses: 1282 | urn, urd = self.untagged_responses[-1] 1283 | if urn != typ: 1284 | urd = None 1285 | else: 1286 | urd = None 1287 | 1288 | if urd is None: 1289 | urd = [] 1290 | self.untagged_responses.append([typ, urd]) 1291 | 1292 | urd.append(dat) 1293 | 1294 | self.commands_lock.release() 1295 | 1296 | if __debug__: self._log(5, 'untagged_responses[%s] %s += ["%.80r"]' % (typ, len(urd)-1, dat)) 1297 | 1298 | 1299 | def _check_bye(self): 1300 | 1301 | bye = self._get_untagged_response('BYE', leave=True) 1302 | if bye: 1303 | raise self.abort(bye[-1].decode('ASCII', 'replace')) 1304 | 1305 | 1306 | def _choose_nonull_or_dflt(self, dflt, *args): 1307 | if isinstance(dflt, str): 1308 | dflttyp = str # Allow any string type 1309 | else: 1310 | dflttyp = type(dflt) 1311 | for arg in args: 1312 | if arg is not None: 1313 | if isinstance(arg, dflttyp): 1314 | return arg 1315 | if __debug__: self._log(0, 'bad arg is %s, expecting %s' % (type(arg), dflttyp)) 1316 | return dflt 1317 | 1318 | 1319 | def _command(self, name, *args, **kw): 1320 | 1321 | if Commands[name][CMD_VAL_ASYNC]: 1322 | cmdtyp = 'async' 1323 | else: 1324 | cmdtyp = 'sync' 1325 | 1326 | if __debug__: self._log(1, '[%s] %s %s' % (cmdtyp, name, args)) 1327 | 1328 | if __debug__: self._log(3, 'state_change_pending.acquire') 1329 | self.state_change_pending.acquire() 1330 | 1331 | self._end_idle() 1332 | 1333 | if cmdtyp == 'async': 1334 | self.state_change_pending.release() 1335 | if __debug__: self._log(3, 'state_change_pending.release') 1336 | else: 1337 | # Need to wait for all async commands to complete 1338 | self._check_bye() 1339 | self.commands_lock.acquire() 1340 | if self.tagged_commands: 1341 | self.state_change_free.clear() 1342 | need_event = True 1343 | else: 1344 | need_event = False 1345 | self.commands_lock.release() 1346 | if need_event: 1347 | if __debug__: self._log(3, 'sync command %s waiting for empty commands Q' % name) 1348 | self.state_change_free.wait() 1349 | if __debug__: self._log(3, 'sync command %s proceeding' % name) 1350 | 1351 | if self.state not in Commands[name][CMD_VAL_STATES]: 1352 | self.literal = None 1353 | raise self.error('command %s illegal in state %s' 1354 | % (name, self.state)) 1355 | 1356 | self._check_bye() 1357 | 1358 | if name in ('EXAMINE', 'SELECT'): 1359 | self.commands_lock.acquire() 1360 | self.untagged_responses = [] # Flush all untagged responses 1361 | self.commands_lock.release() 1362 | else: 1363 | for typ in ('OK', 'NO', 'BAD'): 1364 | while self._get_untagged_response(typ): 1365 | continue 1366 | 1367 | if not self.is_readonly and self._get_untagged_response('READ-ONLY', leave=True): 1368 | self.literal = None 1369 | raise self.readonly('mailbox status changed to READ-ONLY') 1370 | 1371 | if self.Terminate: 1372 | raise self.abort('connection closed') 1373 | 1374 | rqb = self._request_push(name=name, **kw) 1375 | 1376 | name = bytes(name, self._encoding) 1377 | data = rqb.tag + b' ' + name 1378 | for arg in args: 1379 | if arg is None: continue 1380 | if isinstance(arg, str): 1381 | arg = bytes(arg, self._encoding) 1382 | data = data + b' ' + arg 1383 | 1384 | literal = self.literal 1385 | if literal is not None: 1386 | self.literal = None 1387 | if type(literal) is type(self._command): 1388 | literator = literal 1389 | else: 1390 | literator = None 1391 | data = data + bytes(' {%s}' % len(literal), self._encoding) 1392 | 1393 | if __debug__: self._log(4, 'data=%r' % data) 1394 | 1395 | rqb.data = data + CRLF 1396 | 1397 | if literal is None: 1398 | self.ouq.put(rqb) 1399 | return rqb 1400 | 1401 | # Must setup continuation expectancy *before* ouq.put 1402 | crqb = self._request_push(name=name, tag='continuation') 1403 | 1404 | self.ouq.put(rqb) 1405 | 1406 | while True: 1407 | # Wait for continuation response 1408 | 1409 | ok, data = crqb.get_response('command: %s => %%s' % name) 1410 | if __debug__: self._log(4, 'continuation => %s, %r' % (ok, data)) 1411 | 1412 | # NO/BAD response? 1413 | 1414 | if not ok: 1415 | break 1416 | 1417 | if data == 'go ahead': # Apparently not uncommon broken IMAP4 server response to AUTHENTICATE command 1418 | data = '' 1419 | 1420 | # Send literal 1421 | 1422 | if literator is not None: 1423 | literal = literator(data, rqb) 1424 | 1425 | if literal is None: 1426 | break 1427 | 1428 | if literator is not None: 1429 | # Need new request for next continuation response 1430 | crqb = self._request_push(name=name, tag='continuation') 1431 | 1432 | if __debug__: self._log(4, 'write literal size %s' % len(literal)) 1433 | crqb.data = literal + CRLF 1434 | self.ouq.put(crqb) 1435 | 1436 | if literator is None: 1437 | break 1438 | 1439 | return rqb 1440 | 1441 | 1442 | def _command_complete(self, rqb, kw): 1443 | 1444 | # Called for non-callback commands 1445 | 1446 | self._check_bye() 1447 | typ, dat = rqb.get_response('command: %s => %%s' % rqb.name) 1448 | if typ == 'BAD': 1449 | if __debug__: self._print_log() 1450 | raise self.error('%s command error: %s %s. Data: %.100s' % (rqb.name, typ, dat, rqb.data)) 1451 | if 'untagged_response' in kw: 1452 | return self._untagged_response(typ, dat, kw['untagged_response']) 1453 | return typ, dat 1454 | 1455 | 1456 | def _command_completer(self, cb_arg_list): 1457 | 1458 | # Called for callback commands 1459 | response, cb_arg, error = cb_arg_list 1460 | rqb, kw = cb_arg 1461 | rqb.callback = kw['callback'] 1462 | rqb.callback_arg = kw.get('cb_arg') 1463 | if error is not None: 1464 | if __debug__: self._print_log() 1465 | typ, val = error 1466 | rqb.abort(typ, val) 1467 | return 1468 | bye = self._get_untagged_response('BYE', leave=True) 1469 | if bye: 1470 | rqb.abort(self.abort, bye[-1].decode('ASCII', 'replace')) 1471 | return 1472 | typ, dat = response 1473 | if typ == 'BAD': 1474 | if __debug__: self._print_log() 1475 | rqb.abort(self.error, '%s command error: %s %s. Data: %.100s' % (rqb.name, typ, dat, rqb.data)) 1476 | return 1477 | if __debug__: self._log(4, '_command_completer(%s, %s, None) = %s' % (response, cb_arg, rqb.tag)) 1478 | if 'untagged_response' in kw: 1479 | response = self._untagged_response(typ, dat, kw['untagged_response']) 1480 | rqb.deliver(response) 1481 | 1482 | 1483 | def _deliver_dat(self, typ, dat, kw): 1484 | 1485 | if 'callback' in kw: 1486 | kw['callback'](((typ, dat), kw.get('cb_arg'), None)) 1487 | return typ, dat 1488 | 1489 | 1490 | def _deliver_exc(self, exc, dat, kw): 1491 | 1492 | if 'callback' in kw: 1493 | kw['callback']((None, kw.get('cb_arg'), (exc, dat))) 1494 | raise exc(dat) 1495 | 1496 | 1497 | def _end_idle(self): 1498 | 1499 | self.idle_lock.acquire() 1500 | irqb = self.idle_rqb 1501 | if irqb is None: 1502 | self.idle_lock.release() 1503 | return 1504 | self.idle_rqb = None 1505 | self.idle_timeout = None 1506 | self.idle_lock.release() 1507 | irqb.data = bytes('DONE', 'ASCII') + CRLF 1508 | self.ouq.put(irqb) 1509 | if __debug__: self._log(2, 'server IDLE finished') 1510 | 1511 | 1512 | def _get_capabilities(self): 1513 | typ, dat = self.capability() 1514 | if dat == [None]: 1515 | raise self.error('no CAPABILITY response from server') 1516 | dat = str(dat[-1], "ASCII") 1517 | dat = dat.upper() 1518 | self.capabilities = tuple(dat.split()) 1519 | 1520 | 1521 | def _get_untagged_response(self, name, leave=False): 1522 | 1523 | self.commands_lock.acquire() 1524 | 1525 | for i, (typ, dat) in enumerate(self.untagged_responses): 1526 | if typ == name: 1527 | if not leave: 1528 | del self.untagged_responses[i] 1529 | self.commands_lock.release() 1530 | if __debug__: self._log(5, '_get_untagged_response(%s) => %.80r' % (name, dat)) 1531 | return dat 1532 | 1533 | self.commands_lock.release() 1534 | return None 1535 | 1536 | 1537 | def _match(self, cre, s): 1538 | 1539 | # Run compiled regular expression 'cre' match method on 's'. 1540 | # Save result, return success. 1541 | 1542 | self.mo = cre.match(s) 1543 | return self.mo is not None 1544 | 1545 | 1546 | def _put_response(self, resp): 1547 | 1548 | if self._expecting_data: 1549 | rlen = len(resp) 1550 | dlen = min(self._expecting_data_len, rlen) 1551 | if __debug__: self._log(5, '_put_response expecting data len %s, got %s' % (self._expecting_data_len, rlen)) 1552 | self._expecting_data_len -= dlen 1553 | self._expecting_data = (self._expecting_data_len != 0) 1554 | if rlen <= dlen: 1555 | self._accumulated_data.append(resp) 1556 | return 1557 | self._accumulated_data.append(resp[:dlen]) 1558 | resp = resp[dlen:] 1559 | 1560 | if self._accumulated_data: 1561 | typ, dat = self._literal_expected 1562 | self._append_untagged(typ, (dat, b''.join(self._accumulated_data))) 1563 | self._accumulated_data = [] 1564 | 1565 | # Protocol mandates all lines terminated by CRLF 1566 | resp = resp[:-2] 1567 | if __debug__: self._log(5, '_put_response(%r)' % resp) 1568 | 1569 | if 'continuation' in self.tagged_commands: 1570 | continuation_expected = True 1571 | else: 1572 | continuation_expected = False 1573 | 1574 | if self._literal_expected is not None: 1575 | dat = resp 1576 | if self._match(self.literal_cre, dat): 1577 | self._literal_expected[1] = dat 1578 | self._expecting_data = True 1579 | self._expecting_data_len = int(self.mo.group('size')) 1580 | if __debug__: self._log(4, 'expecting literal size %s' % self._expecting_data_len) 1581 | return 1582 | typ = self._literal_expected[0] 1583 | self._literal_expected = None 1584 | if dat: 1585 | self._append_untagged(typ, dat) # Tail 1586 | if __debug__: self._log(4, 'literal completed') 1587 | else: 1588 | # Command completion response? 1589 | if self._match(self.tagre, resp): 1590 | tag = self.mo.group('tag') 1591 | typ = str(self.mo.group('type'), 'ASCII') 1592 | dat = self.mo.group('data') 1593 | if typ in ('OK', 'NO', 'BAD') and self._match(self.response_code_cre, dat): 1594 | self._append_untagged(str(self.mo.group('type'), 'ASCII'), self.mo.group('data')) 1595 | if not tag in self.tagged_commands: 1596 | if __debug__: self._log(1, 'unexpected tagged response: %r' % resp) 1597 | else: 1598 | self._request_pop(tag, (typ, [dat])) 1599 | else: 1600 | dat2 = None 1601 | 1602 | # '*' (untagged) responses? 1603 | 1604 | if not self._match(self.untagged_response_cre, resp): 1605 | if self._match(self.untagged_status_cre, resp): 1606 | dat2 = self.mo.group('data2') 1607 | 1608 | if self.mo is None: 1609 | # Only other possibility is '+' (continuation) response... 1610 | 1611 | if self._match(self.continuation_cre, resp): 1612 | if not continuation_expected: 1613 | if __debug__: self._log(1, "unexpected continuation response: '%r'" % resp) 1614 | return 1615 | self._request_pop('continuation', (True, self.mo.group('data'))) 1616 | return 1617 | 1618 | if __debug__: self._log(1, "unexpected response: '%r'" % resp) 1619 | return 1620 | 1621 | typ = str(self.mo.group('type'), 'ASCII') 1622 | dat = self.mo.group('data') 1623 | if dat is None: dat = b'' # Null untagged response 1624 | if dat2: dat = dat + b' ' + dat2 1625 | 1626 | # Is there a literal to come? 1627 | 1628 | if self._match(self.literal_cre, dat): 1629 | self._expecting_data = True 1630 | self._expecting_data_len = int(self.mo.group('size')) 1631 | if __debug__: self._log(4, 'read literal size %s' % self._expecting_data_len) 1632 | self._literal_expected = [typ, dat] 1633 | return 1634 | 1635 | self._append_untagged(typ, dat) 1636 | if typ in ('OK', 'NO', 'BAD') and self._match(self.response_code_cre, dat): 1637 | self._append_untagged(str(self.mo.group('type'), 'ASCII'), self.mo.group('data')) 1638 | 1639 | if typ != 'OK': # NO, BYE, IDLE 1640 | self._end_idle() 1641 | 1642 | # Command waiting for aborted continuation response? 1643 | 1644 | if continuation_expected: 1645 | self._request_pop('continuation', (False, resp)) 1646 | 1647 | # Bad news? 1648 | 1649 | if typ in ('NO', 'BAD', 'BYE'): 1650 | if typ == 'BYE': 1651 | self.Terminate = True 1652 | if __debug__: self._log(1, '%s response: %r' % (typ, dat)) 1653 | 1654 | 1655 | def _quote(self, arg): 1656 | 1657 | return '"%s"' % arg.replace('\\', '\\\\').replace('"', '\\"') 1658 | 1659 | 1660 | def _release_state_change(self): 1661 | 1662 | if self.state_change_pending.locked(): 1663 | self.state_change_pending.release() 1664 | if __debug__: self._log(3, 'state_change_pending.release') 1665 | 1666 | 1667 | def _request_pop(self, name, data): 1668 | 1669 | self.commands_lock.acquire() 1670 | rqb = self.tagged_commands.pop(name) 1671 | if not self.tagged_commands: 1672 | need_event = True 1673 | else: 1674 | need_event = False 1675 | self.commands_lock.release() 1676 | 1677 | if __debug__: self._log(4, '_request_pop(%s, %r) [%d] = %s' % (name, data, len(self.tagged_commands), rqb.tag)) 1678 | rqb.deliver(data) 1679 | 1680 | if need_event: 1681 | if __debug__: self._log(3, 'state_change_free.set') 1682 | self.state_change_free.set() 1683 | 1684 | 1685 | def _request_push(self, tag=None, name=None, **kw): 1686 | 1687 | self.commands_lock.acquire() 1688 | rqb = Request(self, name=name, **kw) 1689 | if tag is None: 1690 | tag = rqb.tag 1691 | self.tagged_commands[tag] = rqb 1692 | self.commands_lock.release() 1693 | if __debug__: self._log(4, '_request_push(%s, %s, %s) = %s' % (tag, name, repr(kw), rqb.tag)) 1694 | return rqb 1695 | 1696 | 1697 | def _simple_command(self, name, *args, **kw): 1698 | 1699 | if 'callback' in kw: 1700 | # Note: old calling sequence for back-compat with python <2.6 1701 | self._command(name, callback=self._command_completer, cb_arg=kw, cb_self=True, *args) 1702 | return (None, None) 1703 | return self._command_complete(self._command(name, *args), kw) 1704 | 1705 | 1706 | def _untagged_response(self, typ, dat, name): 1707 | 1708 | if typ == 'NO': 1709 | return typ, dat 1710 | data = self._get_untagged_response(name) 1711 | if not data: 1712 | return typ, [None] 1713 | while True: 1714 | dat = self._get_untagged_response(name) 1715 | if not dat: 1716 | break 1717 | data += dat 1718 | if __debug__: self._log(4, '_untagged_response(%s, ?, %s) => %.80r' % (typ, name, data)) 1719 | return typ, data 1720 | 1721 | 1722 | 1723 | # Threads 1724 | 1725 | 1726 | def _close_threads(self): 1727 | 1728 | if __debug__: self._log(1, '_close_threads') 1729 | 1730 | self.ouq.put(None) 1731 | self.wrth.join() 1732 | 1733 | if __debug__: self._log(1, 'call shutdown') 1734 | 1735 | self.shutdown() 1736 | 1737 | self.rdth.join() 1738 | self.inth.join() 1739 | 1740 | 1741 | def _handler(self): 1742 | 1743 | resp_timeout = self.resp_timeout 1744 | 1745 | threading.current_thread().name = self.identifier + 'handler' 1746 | 1747 | time.sleep(0.1) # Don't start handling before main thread ready 1748 | 1749 | if __debug__: self._log(1, 'starting') 1750 | 1751 | typ, val = self.abort, 'connection terminated' 1752 | 1753 | while not self.Terminate: 1754 | 1755 | self.idle_lock.acquire() 1756 | if self.idle_timeout is not None: 1757 | timeout = self.idle_timeout - time.time() 1758 | if timeout <= 0: 1759 | timeout = 1 1760 | if __debug__: 1761 | if self.idle_rqb is not None: 1762 | self._log(5, 'server IDLING, timeout=%.2f' % timeout) 1763 | else: 1764 | timeout = resp_timeout 1765 | self.idle_lock.release() 1766 | 1767 | try: 1768 | line = self.inq.get(True, timeout) 1769 | except queue.Empty: 1770 | if self.idle_rqb is None: 1771 | if resp_timeout is not None and self.tagged_commands: 1772 | if __debug__: self._log(1, 'response timeout') 1773 | typ, val = self.abort, 'no response after %s secs' % resp_timeout 1774 | break 1775 | continue 1776 | if self.idle_timeout > time.time(): 1777 | continue 1778 | if __debug__: self._log(2, 'server IDLE timedout') 1779 | line = IDLE_TIMEOUT_RESPONSE 1780 | 1781 | if line is None: 1782 | if __debug__: self._log(1, 'inq None - terminating') 1783 | break 1784 | 1785 | if not isinstance(line, bytes): 1786 | typ, val = line 1787 | break 1788 | 1789 | try: 1790 | self._put_response(line) 1791 | except: 1792 | typ, val = self.error, 'program error: %s - %s' % sys.exc_info()[:2] 1793 | break 1794 | 1795 | self.Terminate = True 1796 | 1797 | if __debug__: self._log(1, 'terminating: %s' % repr(val)) 1798 | 1799 | while not self.ouq.empty(): 1800 | try: 1801 | qel = self.ouq.get_nowait() 1802 | if qel is not None: 1803 | qel.abort(typ, val) 1804 | except queue.Empty: 1805 | break 1806 | self.ouq.put(None) 1807 | 1808 | self.commands_lock.acquire() 1809 | for name in list(self.tagged_commands.keys()): 1810 | rqb = self.tagged_commands.pop(name) 1811 | rqb.abort(typ, val) 1812 | self.state_change_free.set() 1813 | self.commands_lock.release() 1814 | if __debug__: self._log(3, 'state_change_free.set') 1815 | 1816 | if __debug__: self._log(1, 'finished') 1817 | 1818 | 1819 | if hasattr(select_module, "poll"): 1820 | 1821 | def _reader(self): 1822 | 1823 | threading.current_thread().name = self.identifier + 'reader' 1824 | 1825 | if __debug__: self._log(1, 'starting using poll') 1826 | 1827 | def poll_error(state): 1828 | PollErrors = { 1829 | select.POLLERR: 'Error', 1830 | select.POLLHUP: 'Hang up', 1831 | select.POLLNVAL: 'Invalid request: descriptor not open', 1832 | } 1833 | return ' '.join([PollErrors[s] for s in PollErrors.keys() if (s & state)]) 1834 | 1835 | line_part = b'' 1836 | 1837 | poll = select.poll() 1838 | 1839 | poll.register(self.read_fd, select.POLLIN) 1840 | 1841 | rxzero = 0 1842 | terminate = False 1843 | read_poll_timeout = self.read_poll_timeout * 1000 # poll() timeout is in millisecs 1844 | 1845 | while not (terminate or self.Terminate): 1846 | if self.state == LOGOUT: 1847 | timeout = 10 1848 | else: 1849 | timeout = read_poll_timeout 1850 | try: 1851 | r = poll.poll(timeout) 1852 | if __debug__: self._log(5, 'poll => %s' % repr(r)) 1853 | if not r: 1854 | continue # Timeout 1855 | 1856 | fd,state = r[0] 1857 | 1858 | if state & select.POLLIN: 1859 | data = self.read(self.read_size) # Drain ssl buffer if present 1860 | start = 0 1861 | dlen = len(data) 1862 | if __debug__: self._log(5, 'rcvd %s' % dlen) 1863 | if dlen == 0: 1864 | rxzero += 1 1865 | if rxzero > 5: 1866 | raise IOError("Too many read 0") 1867 | time.sleep(0.1) 1868 | continue # Try again 1869 | rxzero = 0 1870 | 1871 | while True: 1872 | stop = data.find(b'\n', start) 1873 | if stop < 0: 1874 | line_part += data[start:] 1875 | break 1876 | stop += 1 1877 | line_part, start, line = \ 1878 | b'', stop, line_part + data[start:stop] 1879 | if __debug__: self._log(4, '< %r' % line) 1880 | self.inq.put(line) 1881 | if self.TerminateReader: 1882 | terminate = True 1883 | 1884 | if state & ~(select.POLLIN): 1885 | raise IOError(poll_error(state)) 1886 | except: 1887 | reason = 'socket error: %s - %s' % sys.exc_info()[:2] 1888 | if __debug__: 1889 | if not self.Terminate: 1890 | self._print_log() 1891 | if self.debug: self.debug += 4 # Output all 1892 | self._log(1, reason) 1893 | self.inq.put((self.abort, reason)) 1894 | break 1895 | 1896 | poll.unregister(self.read_fd) 1897 | 1898 | if __debug__: self._log(1, 'finished') 1899 | 1900 | else: 1901 | 1902 | # No "poll" - use select() 1903 | 1904 | def _reader(self): 1905 | 1906 | threading.current_thread().name = self.identifier + 'reader' 1907 | 1908 | if __debug__: self._log(1, 'starting using select') 1909 | 1910 | line_part = b'' 1911 | 1912 | rxzero = 0 1913 | terminate = False 1914 | 1915 | while not (terminate or self.Terminate): 1916 | if self.state == LOGOUT: 1917 | timeout = 1 1918 | else: 1919 | timeout = self.read_poll_timeout 1920 | try: 1921 | r,w,e = select.select([self.read_fd], [], [], timeout) 1922 | if __debug__: self._log(5, 'select => %s, %s, %s' % (r,w,e)) 1923 | if not r: # Timeout 1924 | continue 1925 | 1926 | data = self.read(self.read_size) # Drain ssl buffer if present 1927 | start = 0 1928 | dlen = len(data) 1929 | if __debug__: self._log(5, 'rcvd %s' % dlen) 1930 | if dlen == 0: 1931 | rxzero += 1 1932 | if rxzero > 5: 1933 | raise IOError("Too many read 0") 1934 | time.sleep(0.1) 1935 | continue # Try again 1936 | rxzero = 0 1937 | 1938 | while True: 1939 | stop = data.find(b'\n', start) 1940 | if stop < 0: 1941 | line_part += data[start:] 1942 | break 1943 | stop += 1 1944 | line_part, start, line = \ 1945 | b'', stop, line_part + data[start:stop] 1946 | if __debug__: self._log(4, '< %r' % line) 1947 | self.inq.put(line) 1948 | if self.TerminateReader: 1949 | terminate = True 1950 | except: 1951 | reason = 'socket error: %s - %s' % sys.exc_info()[:2] 1952 | if __debug__: 1953 | if not self.Terminate: 1954 | self._print_log() 1955 | if self.debug: self.debug += 4 # Output all 1956 | self._log(1, reason) 1957 | self.inq.put((self.abort, reason)) 1958 | break 1959 | 1960 | if __debug__: self._log(1, 'finished') 1961 | 1962 | 1963 | def _writer(self): 1964 | 1965 | threading.current_thread().name = self.identifier + 'writer' 1966 | 1967 | if __debug__: self._log(1, 'starting') 1968 | 1969 | reason = 'Terminated' 1970 | 1971 | while not self.Terminate: 1972 | rqb = self.ouq.get() 1973 | if rqb is None: 1974 | break # Outq flushed 1975 | 1976 | try: 1977 | self.send(rqb.data) 1978 | if __debug__: self._log(4, '> %r' % rqb.data) 1979 | except: 1980 | reason = 'socket error: %s - %s' % sys.exc_info()[:2] 1981 | if __debug__: 1982 | if not self.Terminate: 1983 | self._print_log() 1984 | if self.debug: self.debug += 4 # Output all 1985 | self._log(1, reason) 1986 | rqb.abort(self.abort, reason) 1987 | break 1988 | 1989 | self.inq.put((self.abort, reason)) 1990 | 1991 | if __debug__: self._log(1, 'finished') 1992 | 1993 | 1994 | 1995 | # Debugging 1996 | 1997 | 1998 | if __debug__: 1999 | 2000 | def _init_debug(self, debug=None, debug_file=None, debug_buf_lvl=None): 2001 | self.debug_lock = threading.Lock() 2002 | 2003 | self.debug = self._choose_nonull_or_dflt(0, debug) 2004 | self.debug_file = self._choose_nonull_or_dflt(sys.stderr, debug_file) 2005 | self.debug_buf_lvl = self._choose_nonull_or_dflt(DFLT_DEBUG_BUF_LVL, debug_buf_lvl) 2006 | 2007 | self._cmd_log_len = 20 2008 | self._cmd_log_idx = 0 2009 | self._cmd_log = {} # Last `_cmd_log_len' interactions 2010 | if self.debug: 2011 | self._mesg('imaplib2 version %s' % __version__) 2012 | self._mesg('imaplib2 debug level %s, buffer level %s' % (self.debug, self.debug_buf_lvl)) 2013 | 2014 | 2015 | def _dump_ur(self, lvl): 2016 | if lvl > self.debug: 2017 | return 2018 | 2019 | l = self.untagged_responses # NB: bytes array 2020 | if not l: 2021 | return 2022 | 2023 | t = '\n\t\t' 2024 | l = ['%s: "%s"' % (x[0], x[1][0] and b'" "'.join(x[1]) or '') for x in l] 2025 | self.debug_lock.acquire() 2026 | self._mesg('untagged responses dump:%s%s' % (t, t.join(l))) 2027 | self.debug_lock.release() 2028 | 2029 | 2030 | def _log(self, lvl, line): 2031 | if lvl > self.debug: 2032 | return 2033 | 2034 | if line[-2:] == CRLF: 2035 | line = line[:-2] + '\\r\\n' 2036 | 2037 | tn = threading.current_thread().name 2038 | 2039 | if lvl <= 1 or self.debug > self.debug_buf_lvl: 2040 | self.debug_lock.acquire() 2041 | self._mesg(line, tn) 2042 | self.debug_lock.release() 2043 | if lvl != 1: 2044 | return 2045 | 2046 | # Keep log of last `_cmd_log_len' interactions for debugging. 2047 | self.debug_lock.acquire() 2048 | self._cmd_log[self._cmd_log_idx] = (line, tn, time.time()) 2049 | self._cmd_log_idx += 1 2050 | if self._cmd_log_idx >= self._cmd_log_len: 2051 | self._cmd_log_idx = 0 2052 | self.debug_lock.release() 2053 | 2054 | 2055 | def _mesg(self, s, tn=None, secs=None): 2056 | if secs is None: 2057 | secs = time.time() 2058 | if tn is None: 2059 | tn = threading.current_thread().name 2060 | tm = time.strftime('%M:%S', time.localtime(secs)) 2061 | try: 2062 | self.debug_file.write(' %s.%02d %s %s\n' % (tm, (secs*100)%100, tn, s)) 2063 | self.debug_file.flush() 2064 | finally: 2065 | pass 2066 | 2067 | 2068 | def _print_log(self): 2069 | self.debug_lock.acquire() 2070 | i, n = self._cmd_log_idx, self._cmd_log_len 2071 | if n: self._mesg('last %d log messages:' % n) 2072 | while n: 2073 | try: 2074 | self._mesg(*self._cmd_log[i]) 2075 | except: 2076 | pass 2077 | i += 1 2078 | if i >= self._cmd_log_len: 2079 | i = 0 2080 | n -= 1 2081 | self.debug_lock.release() 2082 | 2083 | 2084 | 2085 | class IMAP4_SSL(IMAP4): 2086 | 2087 | """IMAP4 client class over SSL connection 2088 | 2089 | Instantiate with: 2090 | IMAP4_SSL(host=None, port=None, keyfile=None, certfile=None, ca_certs=None, cert_verify_cb=None, ssl_version="ssl23", debug=None, debug_file=None, identifier=None, timeout=None, debug_buf_lvl=None, tls_level="tls_compat") 2091 | 2092 | host - host's name (default: localhost); 2093 | port - port number (default: standard IMAP4 SSL port); 2094 | keyfile - PEM formatted file that contains your private key (default: None); 2095 | certfile - PEM formatted certificate chain file (default: None); 2096 | ca_certs - PEM formatted certificate chain file used to validate server certificates (default: None); 2097 | cert_verify_cb - function to verify authenticity of server certificates (default: None); 2098 | ssl_version - SSL version to use (default: "ssl23", choose from: "tls1","ssl3","ssl23"); 2099 | debug - debug level (default: 0 - no debug); 2100 | debug_file - debug stream (default: sys.stderr); 2101 | identifier - thread identifier prefix (default: host); 2102 | timeout - timeout in seconds when expecting a command response. 2103 | debug_buf_lvl - debug level at which buffering is turned off. 2104 | tls_level - TLS security level (default: "tls_compat"). 2105 | 2106 | The recognized values for tls_level are: 2107 | tls_secure: accept only TLS protocols recognized as "secure" 2108 | tls_no_ssl: disable SSLv2 and SSLv3 support 2109 | tls_compat: accept all SSL/TLS versions 2110 | 2111 | For more documentation see the docstring of the parent class IMAP4. 2112 | """ 2113 | 2114 | 2115 | def __init__(self, host=None, port=None, keyfile=None, certfile=None, ca_certs=None, cert_verify_cb=None, ssl_version="ssl23", debug=None, debug_file=None, identifier=None, timeout=None, debug_buf_lvl=None, tls_level=TLS_COMPAT): 2116 | self.keyfile = keyfile 2117 | self.certfile = certfile 2118 | self.ca_certs = ca_certs 2119 | self.cert_verify_cb = cert_verify_cb 2120 | self.ssl_version = ssl_version 2121 | self.tls_level = tls_level 2122 | IMAP4.__init__(self, host, port, debug, debug_file, identifier, timeout, debug_buf_lvl) 2123 | 2124 | 2125 | def open(self, host=None, port=None): 2126 | """open(host=None, port=None) 2127 | Setup secure connection to remote server on "host:port" 2128 | (default: localhost:standard IMAP4 SSL port). 2129 | This connection will be used by the routines: 2130 | read, send, shutdown, socket, ssl.""" 2131 | 2132 | self.host = self._choose_nonull_or_dflt('', host) 2133 | self.port = self._choose_nonull_or_dflt(IMAP4_SSL_PORT, port) 2134 | self.sock = self.open_socket() 2135 | self.ssl_wrap_socket() 2136 | 2137 | 2138 | def read(self, size): 2139 | """data = read(size) 2140 | Read at most 'size' bytes from remote.""" 2141 | 2142 | if self.decompressor is None: 2143 | return self.sock.read(size) 2144 | 2145 | if self.decompressor.unconsumed_tail: 2146 | data = self.decompressor.unconsumed_tail 2147 | else: 2148 | data = self.sock.read(READ_SIZE) 2149 | 2150 | return self.decompressor.decompress(data, size) 2151 | 2152 | 2153 | def send(self, data): 2154 | """send(data) 2155 | Send 'data' to remote.""" 2156 | 2157 | if self.compressor is not None: 2158 | data = self.compressor.compress(data) 2159 | data += self.compressor.flush(zlib.Z_SYNC_FLUSH) 2160 | 2161 | if hasattr(self.sock, "sendall"): 2162 | self.sock.sendall(data) 2163 | else: 2164 | dlen = len(data) 2165 | while dlen > 0: 2166 | sent = self.sock.write(data) 2167 | if sent == dlen: 2168 | break # avoid copy 2169 | data = data[sent:] 2170 | dlen = dlen - sent 2171 | 2172 | 2173 | def ssl(self): 2174 | """ssl = ssl() 2175 | Return ssl instance used to communicate with the IMAP4 server.""" 2176 | 2177 | return self.sock 2178 | 2179 | 2180 | 2181 | class IMAP4_stream(IMAP4): 2182 | 2183 | """IMAP4 client class over a stream 2184 | 2185 | Instantiate with: 2186 | IMAP4_stream(command, debug=None, debug_file=None, identifier=None, timeout=None, debug_buf_lvl=None) 2187 | 2188 | command - string that can be passed to subprocess.Popen(); 2189 | debug - debug level (default: 0 - no debug); 2190 | debug_file - debug stream (default: sys.stderr); 2191 | identifier - thread identifier prefix (default: host); 2192 | timeout - timeout in seconds when expecting a command response. 2193 | debug_buf_lvl - debug level at which buffering is turned off. 2194 | 2195 | For more documentation see the docstring of the parent class IMAP4. 2196 | """ 2197 | 2198 | 2199 | def __init__(self, command, debug=None, debug_file=None, identifier=None, timeout=None, debug_buf_lvl=None): 2200 | self.command = command 2201 | self.host = command 2202 | self.port = None 2203 | self.sock = None 2204 | self.writefile, self.readfile = None, None 2205 | self.read_fd = None 2206 | IMAP4.__init__(self, None, None, debug, debug_file, identifier, timeout, debug_buf_lvl) 2207 | 2208 | 2209 | def open(self, host=None, port=None): 2210 | """open(host=None, port=None) 2211 | Setup a stream connection via 'self.command'. 2212 | This connection will be used by the routines: 2213 | read, send, shutdown, socket.""" 2214 | 2215 | from subprocess import Popen, PIPE 2216 | from io import DEFAULT_BUFFER_SIZE 2217 | 2218 | if __debug__: self._log(0, 'opening stream from command "%s"' % self.command) 2219 | self._P = Popen(self.command, shell=True, stdin=PIPE, stdout=PIPE, close_fds=True, bufsize=DEFAULT_BUFFER_SIZE) 2220 | self.writefile, self.readfile = self._P.stdin, self._P.stdout 2221 | self.read_fd = self.readfile.fileno() 2222 | 2223 | 2224 | def read(self, size): 2225 | """Read 'size' bytes from remote.""" 2226 | 2227 | if self.decompressor is None: 2228 | return os.read(self.read_fd, size) 2229 | 2230 | if self.decompressor.unconsumed_tail: 2231 | data = self.decompressor.unconsumed_tail 2232 | else: 2233 | data = os.read(self.read_fd, READ_SIZE) 2234 | 2235 | return self.decompressor.decompress(data, size) 2236 | 2237 | 2238 | def send(self, data): 2239 | """Send data to remote.""" 2240 | 2241 | if self.compressor is not None: 2242 | data = self.compressor.compress(data) 2243 | data += self.compressor.flush(zlib.Z_SYNC_FLUSH) 2244 | 2245 | self.writefile.write(data) 2246 | self.writefile.flush() 2247 | 2248 | 2249 | def shutdown(self): 2250 | """Close I/O established in "open".""" 2251 | 2252 | self.readfile.close() 2253 | self.writefile.close() 2254 | self._P.wait() 2255 | 2256 | 2257 | class _Authenticator(object): 2258 | 2259 | """Private class to provide en/de-coding 2260 | for base64 authentication conversation.""" 2261 | 2262 | def __init__(self, mechinst): 2263 | self.mech = mechinst # Callable object to provide/process data 2264 | 2265 | def process(self, data, rqb): 2266 | ret = self.mech(self.decode(data)) 2267 | if ret is None: 2268 | return b'*' # Abort conversation 2269 | return self.encode(ret) 2270 | 2271 | def encode(self, inp): 2272 | # 2273 | # Invoke binascii.b2a_base64 iteratively with 2274 | # short even length buffers, strip the trailing 2275 | # line feed from the result and append. "Even" 2276 | # means a number that factors to both 6 and 8, 2277 | # so when it gets to the end of the 8-bit input 2278 | # there's no partial 6-bit output. 2279 | # 2280 | oup = b'' 2281 | if isinstance(inp, str): 2282 | inp = inp.encode('utf-8') 2283 | while inp: 2284 | if len(inp) > 48: 2285 | t = inp[:48] 2286 | inp = inp[48:] 2287 | else: 2288 | t = inp 2289 | inp = b'' 2290 | e = binascii.b2a_base64(t) 2291 | if e: 2292 | oup = oup + e[:-1] 2293 | return oup 2294 | 2295 | def decode(self, inp): 2296 | if not inp: 2297 | return b'' 2298 | return binascii.a2b_base64(inp) 2299 | 2300 | 2301 | 2302 | 2303 | class _IdleCont(object): 2304 | 2305 | """When process is called, server is in IDLE state 2306 | and will send asynchronous changes.""" 2307 | 2308 | def __init__(self, parent, timeout): 2309 | self.parent = parent 2310 | self.timeout = parent._choose_nonull_or_dflt(IDLE_TIMEOUT, timeout) 2311 | self.parent.idle_timeout = self.timeout + time.time() 2312 | 2313 | def process(self, data, rqb): 2314 | self.parent.idle_lock.acquire() 2315 | self.parent.idle_rqb = rqb 2316 | self.parent.idle_timeout = self.timeout + time.time() 2317 | self.parent.idle_lock.release() 2318 | if __debug__: self.parent._log(2, 'server IDLE started, timeout in %.2f secs' % self.timeout) 2319 | return None 2320 | 2321 | 2322 | 2323 | MonthNames = [None, 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 2324 | 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] 2325 | 2326 | Mon2num = {s.encode():n+1 for n, s in enumerate(MonthNames[1:])} 2327 | 2328 | InternalDate = re.compile(br'.*INTERNALDATE "' 2329 | br'(?P[ 0123][0-9])-(?P[A-Z][a-z][a-z])-(?P[0-9][0-9][0-9][0-9])' 2330 | br' (?P[0-9][0-9]):(?P[0-9][0-9]):(?P[0-9][0-9])' 2331 | br' (?P[-+])(?P[0-9][0-9])(?P[0-9][0-9])' 2332 | br'"') 2333 | 2334 | 2335 | def Internaldate2Time(resp): 2336 | 2337 | """time_tuple = Internaldate2Time(resp) 2338 | 2339 | Parse an IMAP4 INTERNALDATE string. 2340 | 2341 | Return corresponding local time. The return value is a 2342 | time.struct_time instance or None if the string has wrong format.""" 2343 | 2344 | mo = InternalDate.match(resp) 2345 | if not mo: 2346 | return None 2347 | 2348 | mon = Mon2num[mo.group('mon')] 2349 | zonen = mo.group('zonen') 2350 | 2351 | day = int(mo.group('day')) 2352 | year = int(mo.group('year')) 2353 | hour = int(mo.group('hour')) 2354 | min = int(mo.group('min')) 2355 | sec = int(mo.group('sec')) 2356 | zoneh = int(mo.group('zoneh')) 2357 | zonem = int(mo.group('zonem')) 2358 | 2359 | # INTERNALDATE timezone must be subtracted to get UT 2360 | 2361 | zone = (zoneh*60 + zonem)*60 2362 | if zonen == b'-': 2363 | zone = -zone 2364 | 2365 | tt = (year, mon, day, hour, min, sec, -1, -1, -1) 2366 | return time.localtime(calendar.timegm(tt) - zone) 2367 | 2368 | Internaldate2tuple = Internaldate2Time # (Backward compatible) 2369 | 2370 | 2371 | 2372 | def Time2Internaldate(date_time): 2373 | 2374 | """'"DD-Mmm-YYYY HH:MM:SS +HHMM"' = Time2Internaldate(date_time) 2375 | 2376 | Convert 'date_time' to IMAP4 INTERNALDATE representation. 2377 | 2378 | The date_time argument can be a number (int or float) representing 2379 | seconds since epoch (as returned by time.time()), a 9-tuple 2380 | representing local time, an instance of time.struct_time (as 2381 | returned by time.localtime()), an aware datetime instance or a 2382 | double-quoted string. In the last case, it is assumed to already 2383 | be in the correct format.""" 2384 | 2385 | from datetime import datetime, timezone, timedelta 2386 | 2387 | if isinstance(date_time, (int, float)): 2388 | tt = time.localtime(date_time) 2389 | elif isinstance(date_time, tuple): 2390 | try: 2391 | gmtoff = date_time.tm_gmtoff 2392 | except AttributeError: 2393 | if time.daylight: 2394 | dst = date_time[8] 2395 | if dst == -1: 2396 | dst = time.localtime(time.mktime(date_time))[8] 2397 | gmtoff = -(time.timezone, time.altzone)[dst] 2398 | else: 2399 | gmtoff = -time.timezone 2400 | delta = timedelta(seconds=gmtoff) 2401 | dt = datetime(*date_time[:6], tzinfo=timezone(delta)) 2402 | elif isinstance(date_time, datetime): 2403 | if date_time.tzinfo is None: 2404 | raise ValueError("date_time must be aware") 2405 | dt = date_time 2406 | elif isinstance(date_time, str) and (date_time[0],date_time[-1]) == ('"','"'): 2407 | return date_time # Assume in correct format 2408 | else: 2409 | raise ValueError("date_time not of a known type") 2410 | 2411 | fmt = '"%d-{}-%Y %H:%M:%S %z"'.format(MonthNames[dt.month]) 2412 | return dt.strftime(fmt) 2413 | 2414 | 2415 | 2416 | FLAGS_cre = re.compile(br'.*FLAGS \((?P[^\)]*)\)') 2417 | 2418 | def ParseFlags(resp): 2419 | 2420 | """('flag', ...) = ParseFlags(line) 2421 | Convert IMAP4 flags response to python tuple.""" 2422 | 2423 | mo = FLAGS_cre.match(resp) 2424 | if not mo: 2425 | return () 2426 | 2427 | return tuple(mo.group('flags').split()) 2428 | 2429 | 2430 | 2431 | if __name__ == '__main__': 2432 | 2433 | # To test: invoke either as 'python imaplib2.py [IMAP4_server_hostname]', 2434 | # or as 'python imaplib2.py -s "rsh IMAP4_server_hostname exec /etc/rimapd"' 2435 | # or as 'python imaplib2.py -l keyfile[:certfile]|: [IMAP4_SSL_server_hostname]' 2436 | # 2437 | # Option "-d " turns on debugging (use "-d 5" for everything) 2438 | # Option "-i" tests that IDLE is interruptible 2439 | # Option "-p " allows alternate ports 2440 | 2441 | if not __debug__: 2442 | raise ValueError('Please run without -O') 2443 | 2444 | import getopt, getpass 2445 | 2446 | try: 2447 | optlist, args = getopt.getopt(sys.argv[1:], 'd:il:s:p:') 2448 | except getopt.error as val: 2449 | optlist, args = (), () 2450 | 2451 | debug, debug_buf_lvl, port, stream_command, keyfile, certfile, idle_intr = (None,)*7 2452 | for opt,val in optlist: 2453 | if opt == '-d': 2454 | debug = int(val) 2455 | debug_buf_lvl = debug - 1 2456 | elif opt == '-i': 2457 | idle_intr = 1 2458 | elif opt == '-l': 2459 | try: 2460 | keyfile,certfile = val.split(':') 2461 | except ValueError: 2462 | keyfile,certfile = val,val 2463 | elif opt == '-p': 2464 | port = int(val) 2465 | elif opt == '-s': 2466 | stream_command = val 2467 | if not args: args = (stream_command,) 2468 | 2469 | if not args: args = ('',) 2470 | if not port: port = (keyfile is not None) and IMAP4_SSL_PORT or IMAP4_PORT 2471 | 2472 | host = args[0] 2473 | 2474 | USER = getpass.getuser() 2475 | 2476 | data = open(os.path.exists("test.data") and "test.data" or __file__).read(1000) 2477 | test_mesg = 'From: %(user)s@localhost%(lf)sSubject: IMAP4 test%(lf)s%(lf)s%(data)s' \ 2478 | % {'user':USER, 'lf':'\n', 'data':data} 2479 | 2480 | test_seq1 = [ 2481 | ('list', ('""', '""')), 2482 | ('list', ('""', '"%"')), 2483 | ('create', ('imaplib2_test0',)), 2484 | ('rename', ('imaplib2_test0', 'imaplib2_test1')), 2485 | ('CREATE', ('imaplib2_test2',)), 2486 | ('append', ('imaplib2_test2', None, None, test_mesg)), 2487 | ('list', ('""', '"imaplib2_test%"')), 2488 | ('select', ('imaplib2_test2',)), 2489 | ('search', (None, 'SUBJECT', '"IMAP4 test"')), 2490 | ('fetch', ('1:*', '(FLAGS INTERNALDATE RFC822)')), 2491 | ('store', ('1', 'FLAGS', r'(\Deleted)')), 2492 | ('namespace', ()), 2493 | ('expunge', ()), 2494 | ('recent', ()), 2495 | ('close', ()), 2496 | ] 2497 | 2498 | test_seq2 = ( 2499 | ('select', ()), 2500 | ('response', ('UIDVALIDITY',)), 2501 | ('response', ('EXISTS',)), 2502 | ('append', (None, None, None, test_mesg)), 2503 | ('examine', ()), 2504 | ('select', ()), 2505 | ('fetch', ('1:*', '(FLAGS UID)')), 2506 | ('examine', ()), 2507 | ('select', ()), 2508 | ('uid', ('SEARCH', 'SUBJECT', '"IMAP4 test"')), 2509 | ('uid', ('SEARCH', 'ALL')), 2510 | ('uid', ('THREAD', 'references', 'UTF-8', '(SEEN)')), 2511 | ('recent', ()), 2512 | ) 2513 | 2514 | 2515 | AsyncError, M = None, None 2516 | 2517 | def responder(cb_arg_list): 2518 | response, cb_arg, error = cb_arg_list 2519 | global AsyncError 2520 | cmd, args = cb_arg 2521 | if error is not None: 2522 | AsyncError = error 2523 | M._log(0, '[cb] ERROR %s %.100s => %s' % (cmd, args, error)) 2524 | return 2525 | typ, dat = response 2526 | M._log(0, '[cb] %s %.100s => %s %.100s' % (cmd, args, typ, dat)) 2527 | if typ == 'NO': 2528 | AsyncError = (Exception, dat[0]) 2529 | 2530 | def run(cmd, args, cb=True): 2531 | if AsyncError: 2532 | M._log(1, 'AsyncError %s' % repr(AsyncError)) 2533 | M.logout() 2534 | typ, val = AsyncError 2535 | raise typ(val) 2536 | if not M.debug: M._log(0, '%s %.100s' % (cmd, args)) 2537 | try: 2538 | if cb: 2539 | typ, dat = getattr(M, cmd)(callback=responder, cb_arg=(cmd, args), *args) 2540 | M._log(1, '%s %.100s => %s %.100s' % (cmd, args, typ, dat)) 2541 | else: 2542 | typ, dat = getattr(M, cmd)(*args) 2543 | M._log(1, '%s %.100s => %s %.100s' % (cmd, args, typ, dat)) 2544 | except: 2545 | M._log(1, '%s - %s' % sys.exc_info()[:2]) 2546 | M.logout() 2547 | raise 2548 | if typ == 'NO': 2549 | M._log(1, 'NO') 2550 | M.logout() 2551 | raise Exception(dat[0]) 2552 | return dat 2553 | 2554 | try: 2555 | threading.current_thread().name = 'main' 2556 | 2557 | if keyfile is not None: 2558 | if not keyfile: keyfile = None 2559 | if not certfile: certfile = None 2560 | M = IMAP4_SSL(host=host, port=port, keyfile=keyfile, certfile=certfile, ssl_version="tls1", debug=debug, identifier='', timeout=10, debug_buf_lvl=debug_buf_lvl, tls_level="tls_no_ssl") 2561 | elif stream_command: 2562 | M = IMAP4_stream(stream_command, debug=debug, identifier='', timeout=10, debug_buf_lvl=debug_buf_lvl) 2563 | else: 2564 | M = IMAP4(host=host, port=port, debug=debug, identifier='', timeout=10, debug_buf_lvl=debug_buf_lvl) 2565 | if M.state != 'AUTH': # Login needed 2566 | PASSWD = getpass.getpass("IMAP password for %s on %s: " % (USER, host or "localhost")) 2567 | test_seq1.insert(0, ('login', (USER, PASSWD))) 2568 | M._log(0, 'PROTOCOL_VERSION = %s' % M.PROTOCOL_VERSION) 2569 | if 'COMPRESS=DEFLATE' in M.capabilities: 2570 | M.enable_compression() 2571 | 2572 | for cmd,args in test_seq1: 2573 | run(cmd, args) 2574 | 2575 | for ml in run('list', ('""', '"imaplib2_test%"'), cb=False): 2576 | mo = re.match(br'.*"([^"]+)"$', ml) 2577 | if mo: path = mo.group(1) 2578 | else: path = ml.split()[-1] 2579 | run('delete', (path,)) 2580 | 2581 | if 'ID' in M.capabilities: 2582 | run('id', ()) 2583 | run('id', ("(name imaplib2)",)) 2584 | run('id', ("version", __version__, "os", os.uname()[0])) 2585 | 2586 | for cmd,args in test_seq2: 2587 | if (cmd,args) != ('uid', ('SEARCH', 'SUBJECT', 'IMAP4 test')): 2588 | run(cmd, args) 2589 | continue 2590 | 2591 | dat = run(cmd, args, cb=False) 2592 | uid = dat[-1].split() 2593 | if not uid: continue 2594 | run('uid', ('FETCH', uid[-1], 2595 | '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)')) 2596 | run('uid', ('STORE', uid[-1], 'FLAGS', r'(\Deleted)')) 2597 | run('expunge', ()) 2598 | 2599 | if 'IDLE' in M.capabilities: 2600 | run('idle', (2,), cb=False) 2601 | run('idle', (99,)) # Asynchronous, to test interruption of 'idle' by 'noop' 2602 | time.sleep(1) 2603 | run('noop', (), cb=False) 2604 | 2605 | run('append', (None, None, None, test_mesg), cb=False) 2606 | num = run('search', (None, 'ALL'), cb=False)[0].split()[0] 2607 | dat = run('fetch', (num, '(FLAGS INTERNALDATE RFC822)'), cb=False) 2608 | M._mesg('fetch %s => %s' % (num, repr(dat))) 2609 | run('idle', (2,)) 2610 | run('store', (num, '-FLAGS', r'(\Seen)'), cb=False), 2611 | dat = run('fetch', (num, '(FLAGS INTERNALDATE RFC822)'), cb=False) 2612 | M._mesg('fetch %s => %s' % (num, repr(dat))) 2613 | run('uid', ('STORE', num, 'FLAGS', r'(\Deleted)')) 2614 | run('expunge', ()) 2615 | if idle_intr: 2616 | M._mesg('HIT CTRL-C to interrupt IDLE') 2617 | try: 2618 | run('idle', (99,), cb=False) # Synchronous, to test interruption of 'idle' by INTR 2619 | except KeyboardInterrupt: 2620 | M._mesg('Thanks!') 2621 | M._mesg('') 2622 | raise 2623 | elif idle_intr: 2624 | M._mesg('chosen server does not report IDLE capability') 2625 | 2626 | run('logout', (), cb=False) 2627 | 2628 | if debug: 2629 | M._mesg('') 2630 | M._print_log() 2631 | M._mesg('') 2632 | M._mesg('unused untagged responses in order, most recent last:') 2633 | for typ,dat in M.pop_untagged_responses(): M._mesg('\t%s %s' % (typ, dat)) 2634 | 2635 | print('All tests OK.') 2636 | 2637 | except: 2638 | if not idle_intr or M is None or not 'IDLE' in M.capabilities: 2639 | print('Tests failed.') 2640 | 2641 | if not debug: 2642 | print(''' 2643 | If you would like to see debugging output, 2644 | try: %s -d5 2645 | ''' % sys.argv[0]) 2646 | 2647 | raise 2648 | --------------------------------------------------------------------------------