diff --git a/.flake8 b/.flake8 index 66c37ed1..70e14a8e 100644 --- a/.flake8 +++ b/.flake8 @@ -1,3 +1,3 @@ [flake8] +ignore = E203, W503 max-line-length = 120 -exclude = .git,__pycache__,vendor diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..12bba736 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,74 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ "master" ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ "master" ] + schedule: + - cron: '35 7 * * 1' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'python' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + + # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 00000000..0d4a0136 --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,20 @@ +# Dependency Review Action +# +# This Action will scan dependency manifest files that change as part of a Pull Request, surfacing known-vulnerable versions of the packages declared or updated in the PR. Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable packages will be blocked from merging. +# +# Source repository: https://github.com/actions/dependency-review-action +# Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement +name: 'Dependency Review' +on: [pull_request] + +permissions: + contents: read + +jobs: + dependency-review: + runs-on: ubuntu-latest + steps: + - name: 'Checkout Repository' + uses: actions/checkout@v4 + - name: 'Dependency Review' + uses: actions/dependency-review-action@v4 diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index d958d529..3016a424 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -29,18 +29,18 @@ jobs: needs: pre_job strategy: matrix: - python-version: [3.6, 3.7, 3.8, 3.9, 3.10-dev] + python-version: ['3.9', '3.13'] include: - # Allow failure on Python dev - e.g. Cython install regularly fails - - python-version: 3.10-dev + # Allow failure on Python dev - e.g. Cython install regularly fails + - python-version: "3.14-dev" allowed_failure: true max-parallel: 1 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} @@ -50,21 +50,21 @@ jobs: # Only repo owners have access to the secret. PRs will run only the unit tests if: env.AES_256_CBC_PASS != '' run: | - openssl aes-256-cbc -d -in settings.yml.ghenc -out settings.yml -pass env:AES_256_CBC_PASS + openssl aes-256-cbc -d -md sha256 -in settings.yml.ghenc -out settings.yml -pass env:AES_256_CBC_PASS - name: Upgrade pip run: | - python -m pip install --upgrade pip + python -m pip install --upgrade pip wheel - name: Install cutting-edge Cython-based packages on Python dev versions continue-on-error: ${{ matrix.allowed_failure || false }} - if: matrix.python-version == '3.10-dev' + if: matrix.python-version == '3.14-dev' run: | sudo apt-get install libxml2-dev libxslt1-dev - python -m pip install hg+https://foss.heptapod.net/pypy/cffi python -m pip install git+https://github.com/cython/cython.git python -m pip install git+https://github.com/lxml/lxml.git python -m pip install git+https://github.com/yaml/pyyaml.git + python -m pip install git+https://github.com/python-cffi/cffi.git - name: Install dependencies continue-on-error: ${{ matrix.allowed_failure || false }} @@ -77,7 +77,11 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - unittest-parallel -j 4 --class-fixtures --coverage --coverage-source exchangelib + black --check --diff exchangelib tests scripts + isort --check --diff exchangelib tests scripts + flake8 exchangelib tests scripts + blacken-docs *.md docs/*.md + unittest-parallel -j 4 --level=class --coverage --coverage-source exchangelib coveralls --service=github cleanup: @@ -87,24 +91,26 @@ jobs: if: ${{ always() }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: - python-version: '3.9' + python-version: '3.13' - name: Unencrypt secret file env: AES_256_CBC_PASS: ${{ secrets.AES_256_CBC_PASS }} - # Only repo owners have access to the secret. PRs will run only the unit tests + # Only repo owners have access to the secret. PRs will run only the unit tests. + # The encrypted file was created as: + # openssl aes-256-cbc -e -md sha256 -in settings.yml -out settings.yml.ghenc if: env.AES_256_CBC_PASS != '' run: | - openssl aes-256-cbc -d -in settings.yml.ghenc -out settings.yml -pass env:AES_256_CBC_PASS + openssl aes-256-cbc -d -md sha256 -in settings.yml.ghenc -out settings.yml -pass env:AES_256_CBC_PASS - name: Upgrade pip run: | - python -m pip install --upgrade pip + python -m pip install --upgrade pip wheel - name: Install dependencies run: | diff --git a/.gitignore b/.gitignore index a8b47e6b..bce3dca7 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ build dist __pycache__ +venv settings.yml scratch*.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..df263b15 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,42 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + - repo: https://github.com/compilerla/conventional-pre-commit + rev: v3.6.0 + hooks: + - id: conventional-pre-commit + stages: [ commit-msg ] + args: [ ] # optional: list of Conventional Commits types to allow + - repo: local + hooks: + - id: black + name: black + stages: [ pre-commit ] + entry: black --check --diff + language: system + types: [ python ] + - id: isort + name: isort + stages: [ pre-commit ] + entry: isort --check --diff + types: [ python ] + language: system + - id: flake8 + name: flake8 + stages: [ pre-commit ] + entry: flake8 + types: [ python ] + language: system + - id: blacken-docs + name: blacken-docs + stages: [ pre-commit ] + entry: blacken-docs + types: [ markdown ] + language: system diff --git a/CHANGELOG.md b/CHANGELOG.md index 29e9a776..64ea2706 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,385 +5,551 @@ HEAD ---- +5.5.1 +----- +- `Account.version` is now lazy and merely creating an `Account` will not throw errors if the specified credentials + have insufficient permissions to the account. This only happens if an attempt is made to access the mailbox. + + +5.5.0 +----- +- Dropped support for Python 3.8 which is EOL per October 7, 2024. +- Fix setting OOF on servers that only accept UTC timestamps. + + +5.4.3 +----- +- Fix access to shared folders + + +5.4.2 +----- +- Remove timezone warnings in `GetUserAvailability` +- Update `NoVerifyHTTPAdapter` for newer requests versions + + +5.4.1 +----- +- Fix traversal of public folders in `Account.public_folders_root` +- Mark certain distinguished folders as only supported on newer Exchange versions +- Fetch *all* autodiscover information by default + + +5.4.0 +----- +- Add `O365InteractiveConfiguration` helper class to set up MSAL auth for O365. +- Add `exchangelib[msal]` installation flavor to match the above. +- Various bug fixes related to distinguished folders. + + +5.3.0 +----- +- Fix various issues related to public folders and archive folders +- Support read-write for `Contact.im_addresses` +- Improve reporting of inbox rule validation errors + + +5.2.1 +----- +- Fix `ErrorAccessDenied: Not allowed to access Non IPM folder` caused by recent changes in O365. +- Add more intuitive API for inbox rules +- Fix various bugs with inbox creation + + +5.2.0 +----- +- Allow setting a custom `Configuration.max_conections` in autodiscover mode +- Add support for inbox rules. See documentation for examples. +- Fix shared folder access in delegate mode +- Support subscribing to all folders instead of specific folders + + +5.1.0 +----- +- Fix QuerySet operations on shared folders +- Fix globbing on patterns with more than two folder levels +- Fix case sensitivity of "/" folder navigation +- Multiple improvements related to consistency and graceful error handling + + +5.0.3 +----- +- Bugfix release + + +5.0.2 +----- +- Fix bug where certain folders were being assigned the wrong Python class. + + +5.0.1 +----- +- Fix PyPI package. No source code changes. + + +5.0.0 +----- +- Make SOAP-based autodiscovery the default, and remove support for POX-based + discovery. This also removes support for autodiscovery on Exchange 2007. + Only `Account(..., autodiscover=True)` is supported again. +- Deprecated `RetryPolicy.may_retry_on_error`. Instead, add custom retry logic + in `RetryPolicy.raise_response_errors`. +- Moved `exchangelib.util.RETRY_WAIT` to `BaseProtocol.RETRY_WAIT`. + + +4.9.0 +----- +- Added support for SOAP-based autodiscovery, in addition to the existing POX + (plain old XML) implementation. You can specify the autodiscover + implementation explicitly using the `autodiscover` argument: + `Account(..., autodiscover="soap")` or `Account(..., autodiscover="pox")`. POX + is still the default. + + +4.8.0 +----- +- Added new `OAuth2LegacyCredentials` class to support username/password auth + over OAuth. + + +4.7.6 +----- +- Fixed token refresh bug with OAuth2 authentication, again + + +4.7.5 +----- +- Fixed `Protocol.get_free_busy_info()` when called with +100 accounts. +- Allowed configuring DNS timeout for a single nameserver + (`Autodiscovery.DNS_RESOLVER_ATTRS["timeout""]`) and the total query lifetime + (`Autodiscovery.DNS_RESOLVER_LIFETIME`) separately. +- Fixed token refresh bug with OAuth2 authentication + + +4.7.4 +----- +- Bugfix release + + +4.7.3 +----- +- Bugfix release + + +4.7.2 +----- +- Fixed field name to match API: `BaseReplyItem.received_by_representing` to + `BaseReplyItem.received_representing` +- Added fields `received_by` and `received_representing` to `MeetingRequest`, + `MeetingMessage` and `MeetingCancellation` +- Fixed `AppointmentStateField.CANCELLED` enum value. + + +4.7.1 +----- +- Fixed issue where creating an Account with autodiscover and no config would + never set a default retry policy. + + +4.7.0 +----- +- Fixed some spelling mistakes: + - `ALL_OCCURRENCIES` to `ALL_OCCURRENCES` in `exchangelib.items.base` + - `Persona.orgnaization_main_phones` to `Persona.organization_main_phones` +- Removed deprecated methods `EWSTimeZone.localize()`, `EWSTimeZone.normalize()`, + `EWSTimeZone.timezone()` and `QuerySet.iterator()`. +- Disambiguated `chunk_size` and `page_size` in querysets and services. Add a + new `QuerySet.chunk_size` attribute and let it replace the task that + `QuerySet.page_size` previously had. Chunk size is the number of items we send + in e.g. a `GetItem` call, while `page_size` is the number of items we request + per page in services like `FindItem` that support paging. +- Support creating a proper response when getting a notification request + on the callback URL of a push subscription. +- `FolderCollection.subscribe_to_[pull|push|streaming]()` now return a single + subscription instead of a 1-element generator. +- `FolderCollection` now has the same `[pull|push|streaming]_subscription()` + context managers as folders. + + +4.6.2 +----- + +- Fix filtering on array-type extended properties. +- Exceptions in `GetStreamingEvents` responses are now raised. +- Support affinity cookies for pull and streaming subscriptions. + + +4.6.1 +----- + +- Support `tzlocal>=4.1` +- Bug fixes for paging in multi-folder requests. + 4.6.0 ----- + - Support microsecond precision in `EWSDateTime.ewsformat()` - Remove usage of the `multiprocessing` module to allow running in AWS Lambda - Support `tzlocal>=4` - 4.5.2 ----- -- Make `FileAttachment.fp` a proper `BytesIO` implementation -- Add missing `CalendarItem.recurrence_id` field -- Add `SingleFolderQuerySet.resolve()` to aid accessing a folder shared by a different account: + +- Make `FileAttachment.fp` a proper `BytesIO` implementation +- Add missing `CalendarItem.recurrence_id` field +- Add `SingleFolderQuerySet.resolve()` to aid accessing a folder shared by a different account: + ```python from exchangelib import Account from exchangelib.folders import Calendar, SingleFolderQuerySet from exchangelib.properties import DistinguishedFolderId, Mailbox account = Account(primary_smtp_address="some_user@example.com", ...) -shared_calendar = SingleFolderQuerySet(account=account, folder=DistinguishedFolderId( - id=Calendar.DISTINGUISHED_FOLDER_ID, - mailbox=Mailbox(email_address="other_user@example.com") -)).resolve() +shared_calendar = SingleFolderQuerySet( + account=account, + folder=DistinguishedFolderId( + id=Calendar.DISTINGUISHED_FOLDER_ID, + mailbox=Mailbox(email_address="other_user@example.com"), + ), +).resolve() ``` -- Minor bugfixes +- Minor bugfixes 4.5.1 ----- -- Support updating items in `Account.upload()`. Previously, only insert was supported. -- Fixed types for `Contact.manager_mailbox` and `Contact.direct_reports`. -- Support getting `text_body` field on item attachments. +- Support updating items in `Account.upload()`. Previously, only insert was supported. +- Fixed types for `Contact.manager_mailbox` and `Contact.direct_reports`. +- Support getting `text_body` field on item attachments. 4.5.0 ----- -- Fixed bug when updating indexed fields on `Contact` items. -- Fixed bug preventing parsing of `CalendarPermission` items in the `permission_set` field. -- Add support for parsing push notification POST requests sent from the Exchange server - to the callback URL. +- Fixed bug when updating indexed fields on `Contact` items. +- Fixed bug preventing parsing of `CalendarPermission` items in the `permission_set` field. +- Add support for parsing push notification POST requests sent from the Exchange server to the callback URL. 4.4.0 ----- -- Add `Folder.move()` to move folders to a different parent folder. +- Add `Folder.move()` to move folders to a different parent folder. 4.3.0 ----- -- Add context managers `Folder.pull_subscription()`, `Folder.push_subscription()` and - `Folder.streaming_subscription()` that handle unsubscriptions automatically. +- Add context managers `Folder.pull_subscription()`, `Folder.push_subscription()` and + `Folder.streaming_subscription()` that handle unsubscriptions automatically. 4.2.0 ----- -- Move `util._may_retry_on_error` and and `util._raise_response_errors` to - `RetryPolicy.may_retry_on_error` and `RetryPolicy.raise_response_errors`, respectively. This allows - for easier customization of the retry logic. +- Move `util._may_retry_on_error` and and `util._raise_response_errors` to + `RetryPolicy.may_retry_on_error` and `RetryPolicy.raise_response_errors`, respectively. This allows for easier + customization of the retry logic. 4.1.0 ----- -- Add support for synchronization, subscriptions and notifications. Both pull, push and streaming - notifications are supported. See https://ecederstrand.github.io/exchangelib/#synchronization-subscriptions-and-notifications +- Add support for synchronization, subscriptions and notifications. Both pull, push and streaming notifications are + supported. See https://ecederstrand.github.io/exchangelib/#synchronization-subscriptions-and-notifications 4.0.0 ----- -- Add a new `max_connections` option for the `Configuration` class, to increase the session pool size - on a per-server, per-credentials basis. Useful when exchangelib is used with threads, where one may - wish to increase the number of concurrent connections to the server. -- Add `Message.mark_as_junk()` and complementary `QuerySet.mark_as_junk()` methods to mark or un-mark - messages as junk email, and optionally move them to the junk folder. -- Add support for Master Category Lists, also known as User Configurations. These are custom values - that can be assigned to folders. Available via `Folder.get_user_configuration()`. -- `Persona` objects as returned by `QuerySet.people()` now support almost all documented fields. -- Improved `QuerySet.people()` to call the `GetPersona` service if at least one field is requested that - is not supported by the `FindPeople` service. -- Removed the internal caching in `QuerySet`. It's not necessary in most use cases for exchangelib, - and the memory overhead and complexity is not worth the extra effort. This means that `.iterator()` - is now a no-op and marked as deprecated. ATTENTION: If you previously relied on caching of results - in `QuerySet`, you need to do you own caching now. -- Allow plain `date`, `datetime` and `zoneinfo.ZoneInfo` objects as values for fields and methods. This - lowers the barrier for using the library. We still use `EWSDate`, `EWSDateTime` and `EWSTimeZone` for - all values returned from the server, but these classes are subclasses of `date`, `datetime` and - `zoneinfo.ZoneInfo` objects and instances will behave just like instance of their parent class. +- Add a new `max_connections` option for the `Configuration` class, to increase the session pool size on a per-server, + per-credentials basis. Useful when exchangelib is used with threads, where one may wish to increase the number of + concurrent connections to the server. +- Add `Message.mark_as_junk()` and complementary `QuerySet.mark_as_junk()` methods to mark or un-mark messages as junk + email, and optionally move them to the junk folder. +- Add support for Master Category Lists, also known as User Configurations. These are custom values that can be assigned + to folders. Available via `Folder.get_user_configuration()`. +- `Persona` objects as returned by `QuerySet.people()` now support almost all documented fields. +- Improved `QuerySet.people()` to call the `GetPersona` service if at least one field is requested that is not supported + by the `FindPeople` service. +- Removed the internal caching in `QuerySet`. It's not necessary in most use cases for exchangelib, and the memory + overhead and complexity is not worth the extra effort. This means that `.iterator()` + is now a no-op and marked as deprecated. ATTENTION: If you previously relied on caching of results in `QuerySet`, you + need to do you own caching now. +- Allow plain `date`, `datetime` and `zoneinfo.ZoneInfo` objects as values for fields and methods. This lowers the + barrier for using the library. We still use `EWSDate`, `EWSDateTime` and `EWSTimeZone` for all values returned from + the server, but these classes are subclasses of `date`, `datetime` and + `zoneinfo.ZoneInfo` objects and instances will behave just like instance of their parent class. 3.3.2 ----- -- Change Kerberos dependency from `requests_kerberos` to `requests_gssapi` -- Let `EWSDateTime.from_datetime()` accept `datetime.datetime` objects with `tzinfo` objects that - are `dateutil`, `zoneinfo` and `pytz` instances, in addition to `EWSTimeZone`. +- Change Kerberos dependency from `requests_kerberos` to `requests_gssapi` +- Let `EWSDateTime.from_datetime()` accept `datetime.datetime` objects with `tzinfo` objects that are `dateutil` + , `zoneinfo` and `pytz` instances, in addition to `EWSTimeZone`. 3.3.1 ----- -- Allow overriding `dns.resolver.Resolver` class attributes via `Autodiscovery.DNS_RESOLVER_ATTRS`. +- Allow overriding `dns.resolver.Resolver` class attributes via `Autodiscovery.DNS_RESOLVER_ATTRS`. 3.3.0 ----- -- Switch `EWSTimeZone` to be implemented on top of the new `zoneinfo` module in Python 3.9 instead - of `pytz`. `backports.zoneinfo` is used for earlier versions of Python. This means that the - `ÈWSTimeZone` methods `timezone()`, `normalize()` and `localize()` methods are now deprecated. -- Add `EWSTimeZone.from_dateutil()` to support converting `dateutil.tz` timezones to `EWSTimeZone`. -- Dropped support for Python 3.5 which is EOL per September 2020. -- Added support for `CalendaItem.appointment_state`, `CalendaItem.conflicting_meetings` and - `CalendarItem.adjacent_meetings` fields. -- Added support for the `Message.reminder_message_data` field. -- Added support for `Contact.manager_mailbox`, `Contact.direct_reports` and `Contact.complete_name` fields. -- Added support for `Item.response_objects` field. -- Changed `Task.due_date` and `Tas.start_date` fields from datetime to date fields, since the time - was being truncated anyway by the server. -- Added support for `Task.recurrence` field. -- Added read-only support for `Contact.user_smime_certificate` and `Contact.ms_exchange_certificate`. - This means that all fields on all item types are now supported. +- Switch `EWSTimeZone` to be implemented on top of the new `zoneinfo` module in Python 3.9 instead of `pytz` + . `backports.zoneinfo` is used for earlier versions of Python. This means that the + `ÈWSTimeZone` methods `timezone()`, `normalize()` and `localize()` methods are now deprecated. +- Add `EWSTimeZone.from_dateutil()` to support converting `dateutil.tz` timezones to `EWSTimeZone`. +- Dropped support for Python 3.5 which is EOL per September 2020. +- Added support for `CalendaItem.appointment_state`, `CalendaItem.conflicting_meetings` and + `CalendarItem.adjacent_meetings` fields. +- Added support for the `Message.reminder_message_data` field. +- Added support for `Contact.manager_mailbox`, `Contact.direct_reports` and `Contact.complete_name` fields. +- Added support for `Item.response_objects` field. +- Changed `Task.due_date` and `Tas.start_date` fields from datetime to date fields, since the time was being truncated + anyway by the server. +- Added support for `Task.recurrence` field. +- Added read-only support for `Contact.user_smime_certificate` and `Contact.ms_exchange_certificate`. This means that + all fields on all item types are now supported. 3.2.1 ----- -- Fix bug leading to an exception in `CalendarItem.cancel()`. -- Improve stability of `.order_by()` in edge cases where sorting must be done client-side. -- Allow increasing the session pool-size dynamically. -- Change semantics of `.filter(foo__in=[])` to return an empty result. This was previously undefined - behavior. Now we adopt the behaviour of Django in this case. This is still undefined behavior for - list-type fields. -- Moved documentation to GitHub Pages and auto-documentation generated by `pdoc3`. +- Fix bug leading to an exception in `CalendarItem.cancel()`. +- Improve stability of `.order_by()` in edge cases where sorting must be done client-side. +- Allow increasing the session pool-size dynamically. +- Change semantics of `.filter(foo__in=[])` to return an empty result. This was previously undefined behavior. Now we + adopt the behaviour of Django in this case. This is still undefined behavior for list-type fields. +- Moved documentation to GitHub Pages and auto-documentation generated by `pdoc3`. 3.2.0 ----- -- Remove use of `ThreadPool` objects. Threads were used to implement async HTTP requests, but - were creating massive memory leaks. Async requests should be reimplemented using a real async - HTTP request package, so this is just an emergency fix. This also lowers the default - `Protocol.SESSION_POOLSIZE` to 1 because no internal code is running multi-threaded anymore. -- All-day calendar items (created as `CalendarItem(is_all_day=True, ...)`) now accept `EWSDate` - instances for the `start` and `end` values. Similarly, all-day calendar items fetched from - the server now return `start` and `end` values as `EWSDate` instances. In this case, start - and end values are inclusive; a one-day event starts and ends on the same `EWSDate` value. -- Add support for `RecurringMasterItemId` and `OccurrenceItemId` elements that allow to request - the master recurrence from a `CalendarItem` occurrence, and to request a specific occurrence - from a `CalendarItem` master recurrence. `CalendarItem.master_recurrence()` and - `CalendarItem.occurrence(some_occurrence_index)` methods were added to aid this traversal. - `some_occurrence_index` in the last method specifies which item in the list of occurrences to - target; `CalendarItem.occurrence(3)` gets the third occurrence in the recurrence. -- Change `Contact.birthday` and `Contact.wedding_anniversary` from `EWSDateTime` to `EWSDate` - fields. EWS still expects and sends datetime values but has started to reset the time part to - 11:59. Dates are a better match for these two fields anyway. -- Remove support for `len(some_queryset)`. It had the nasty side-effect of forcing - `list(some_queryset)` to run the query twice, once for pre-allocating the list via the result - of `len(some_queryset)`, and then once more to fetch the results. All occurrences of - `len(some_queryset)` can be replaced with `some_queryset.count()`. Unfortunately, there is - no way to keep backwards-compatibility for this feature. -- Added `Account.identity`, an attribute to contain extra information for impersonation. Setting - `Account.identity.upn` or `Account.identity.sid` removes the need for an AD lookup on every request. - `upn` will often be the same as `primary_smtp_address`, but it is not guaranteed. If you have - access to your organization's AD servers, you can look up these values once and add them to your - `Account` object to improve performance of the following requests. -- Added support for CBA authentication +- Remove use of `ThreadPool` objects. Threads were used to implement async HTTP requests, but were creating massive + memory leaks. Async requests should be reimplemented using a real async HTTP request package, so this is just an + emergency fix. This also lowers the default + `Protocol.SESSION_POOLSIZE` to 1 because no internal code is running multi-threaded anymore. +- All-day calendar items (created as `CalendarItem(is_all_day=True, ...)`) now accept `EWSDate` + instances for the `start` and `end` values. Similarly, all-day calendar items fetched from the server now + return `start` and `end` values as `EWSDate` instances. In this case, start and end values are inclusive; a one-day + event starts and ends on the same `EWSDate` value. +- Add support for `RecurringMasterItemId` and `OccurrenceItemId` elements that allow to request the master recurrence + from a `CalendarItem` occurrence, and to request a specific occurrence from a `CalendarItem` master + recurrence. `CalendarItem.master_recurrence()` and + `CalendarItem.occurrence(some_occurrence_index)` methods were added to aid this traversal. + `some_occurrence_index` in the last method specifies which item in the list of occurrences to + target; `CalendarItem.occurrence(3)` gets the third occurrence in the recurrence. +- Change `Contact.birthday` and `Contact.wedding_anniversary` from `EWSDateTime` to `EWSDate` + fields. EWS still expects and sends datetime values but has started to reset the time part to 11:59. Dates are a + better match for these two fields anyway. +- Remove support for `len(some_queryset)`. It had the nasty side-effect of forcing + `list(some_queryset)` to run the query twice, once for pre-allocating the list via the result of `len(some_queryset)`, + and then once more to fetch the results. All occurrences of + `len(some_queryset)` can be replaced with `some_queryset.count()`. Unfortunately, there is no way to keep + backwards-compatibility for this feature. +- Added `Account.identity`, an attribute to contain extra information for impersonation. Setting + `Account.identity.upn` or `Account.identity.sid` removes the need for an AD lookup on every request. + `upn` will often be the same as `primary_smtp_address`, but it is not guaranteed. If you have access to your + organization's AD servers, you can look up these values once and add them to your + `Account` object to improve performance of the following requests. +- Added support for CBA authentication 3.1.1 ----- -- The `max_wait` argument to `FaultTolerance` changed semantics. Previously, it triggered when - the delay until the next attempt would exceed this value. It now triggers after the given - timespan since the *first* request attempt. -- Fixed a bug when pagination is combined with `max_items` (#710) -- Other minor bug fixes +- The `max_wait` argument to `FaultTolerance` changed semantics. Previously, it triggered when the delay until the next + attempt would exceed this value. It now triggers after the given timespan since the *first* request attempt. +- Fixed a bug when pagination is combined with `max_items` (#710) +- Other minor bug fixes 3.1.0 ----- -- Removed the legacy autodiscover implementation. -- Added `QuerySet.depth()` to configure item traversal of querysets. Default is `Shallow` except - for the `CommonViews` folder where default is `Associated`. -- Updating credentials on `Account.protocol` after getting an `UnauthorizedError` now works. +- Removed the legacy autodiscover implementation. +- Added `QuerySet.depth()` to configure item traversal of querysets. Default is `Shallow` except for the `CommonViews` + folder where default is `Associated`. +- Updating credentials on `Account.protocol` after getting an `UnauthorizedError` now works. 3.0.0 ----- -- The new Autodiscover implementation added in 2.2.0 is now default. To switch back to the old - implementation, set the environment variable `EXCHANGELIB_AUTODISCOVER_VERSION=legacy`. -- Removed support for Python 2 +- The new Autodiscover implementation added in 2.2.0 is now default. To switch back to the old implementation, set the + environment variable `EXCHANGELIB_AUTODISCOVER_VERSION=legacy`. +- Removed support for Python 2 2.2.0 ----- -- Added support for specifying a separate retry policy for the autodiscover service endpoint - selection. Set via the `exchangelib.autodiscover.legacy.INITIAL_RETRY_POLICY` module variable - for the the old autodiscover implementation, and via the - `exchangelib.autodiscover.Autodiscovery.INITIAL_RETRY_POLICY` class variable for the new one. -- Support the authorization code OAuth 2.0 grant type (see issue #698) -- Removed the `RootOfHierarchy.permission_set` field. It was causing too many failures in the wild. -- The full autodiscover response containing all contents of the reponse is now available as `Account.ad_response`. -- Added a new Autodiscover implementation that is closer to the specification and easier to debug. To switch - to the new implementation, set the environment variable `EXCHANGELIB_AUTODISCOVER_VERSION=new`. The old - one is still the default if the variable is not set, or set to `EXCHANGELIB_AUTODISCOVER_VERSION=legacy`. -- The `Item.mime_content` field was switched back from a string type to a `bytes` type. It turns out trying - to decode the data was an error (see issue #709). +- Added support for specifying a separate retry policy for the autodiscover service endpoint selection. Set via + the `exchangelib.autodiscover.legacy.INITIAL_RETRY_POLICY` module variable for the the old autodiscover + implementation, and via the + `exchangelib.autodiscover.Autodiscovery.INITIAL_RETRY_POLICY` class variable for the new one. +- Support the authorization code OAuth 2.0 grant type (see issue #698) +- Removed the `RootOfHierarchy.permission_set` field. It was causing too many failures in the wild. +- The full autodiscover response containing all contents of the reponse is now available as `Account.ad_response`. +- Added a new Autodiscover implementation that is closer to the specification and easier to debug. To switch to the new + implementation, set the environment variable `EXCHANGELIB_AUTODISCOVER_VERSION=new`. The old one is still the default + if the variable is not set, or set to `EXCHANGELIB_AUTODISCOVER_VERSION=legacy`. +- The `Item.mime_content` field was switched back from a string type to a `bytes` type. It turns out trying to decode + the data was an error (see issue #709). 2.1.1 ----- -- Bugfix release. +- Bugfix release. 2.1.0 ----- -- Added support for OAuth 2.0 authentication -- Fixed a bug in `RelativeMonthlyPattern` and `RelativeYearlyPattern` where the `weekdays` field was thought to - be a list, but is in fact a single value. Renamed the field to `weekday` to reflect the change. -- Added support for archiving items to the archive mailbox, if the account has one. -- Added support for getting delegate information on an Account, as `Account.delegates`. -- Added support for the `ConvertId` service. Available as `Protocol.convert_ids()`. +- Added support for OAuth 2.0 authentication +- Fixed a bug in `RelativeMonthlyPattern` and `RelativeYearlyPattern` where the `weekdays` field was thought to be a + list, but is in fact a single value. Renamed the field to `weekday` to reflect the change. +- Added support for archiving items to the archive mailbox, if the account has one. +- Added support for getting delegate information on an Account, as `Account.delegates`. +- Added support for the `ConvertId` service. Available as `Protocol.convert_ids()`. 2.0.1 ----- -- Fixed a bug where version 2.x could not open autodiscover cache files generated by - version 1.x packages. +- Fixed a bug where version 2.x could not open autodiscover cache files generated by version 1.x packages. 2.0.0 ----- -- `Item.mime_content` is now a text field instead of a binary field. Encoding and - decoding is done automatically. -- The `Item.item_id`, `Folder.folder_id` and `Occurrence.item_id` fields that were renamed - to just `id` in 1.12.0, have now been removed. -- The `Persona.persona_id` field was replaced with `Persona.id` and `Persona.changekey`, to - align with the `Item` and `Folder` classes. -- In addition to bulk deleting via a QuerySet (`qs.delete()`), it is now possible to also - bulk send, move and copy items in a QuerySet (via `qs.send()`, `qs.move()` and `qs.copy()`, - respectively). -- SSPI support was added but dependencies are not installed by default since it only works - in Win32 environments. Install as `pip install exchangelib[sspi]` to get SSPI support. - Install with `pip install exchangelib[complete]` to get both Kerberos and SSPI auth. -- The custom `extern_id` field is no longer registered by default. If you require this field, - register it manually as part of your setup code on the item types you need: + +- `Item.mime_content` is now a text field instead of a binary field. Encoding and decoding is done automatically. +- The `Item.item_id`, `Folder.folder_id` and `Occurrence.item_id` fields that were renamed to just `id` in 1.12.0, have + now been removed. +- The `Persona.persona_id` field was replaced with `Persona.id` and `Persona.changekey`, to align with the `Item` + and `Folder` classes. +- In addition to bulk deleting via a QuerySet (`qs.delete()`), it is now possible to also bulk send, move and copy items + in a QuerySet (via `qs.send()`, `qs.move()` and `qs.copy()`, respectively). +- SSPI support was added but dependencies are not installed by default since it only works in Win32 environments. + Install as `pip install exchangelib[sspi]` to get SSPI support. Install with `pip install exchangelib[complete]` to + get both Kerberos and SSPI auth. +- The custom `extern_id` field is no longer registered by default. If you require this field, register it manually as + part of your setup code on the item types you need: ```python from exchangelib import CalendarItem, Message, Contact, Task from exchangelib.extended_properties import ExternId - CalendarItem.register('extern_id', ExternId) - Message.register('extern_id', ExternId) - Contact.register('extern_id', ExternId) - Task.register('extern_id', ExternId) + CalendarItem.register("extern_id", ExternId) + Message.register("extern_id", ExternId) + Contact.register("extern_id", ExternId) + Task.register("extern_id", ExternId) ``` -- The `ServiceAccount` class has been removed. If you want fault tolerance, set it in a - `Configuration` object: +- The `ServiceAccount` class has been removed. If you want fault tolerance, set it in a + `Configuration` object: ```python from exchangelib import Configuration, Credentials, FaultTolerance - c = Credentials('foo', 'bar') + + c = Credentials("foo", "bar") config = Configuration(credentials=c, retry_policy=FaultTolerance()) ``` -- It is now possible to use Kerberos and SSPI auth without providing a dummy - `Credentials('', '')` object. -- The `has_ssl` argument of `Configuration` was removed. If you want to connect to a - plain HTTP endpoint, pass the full URL in the `service_endpoint` argument. -- We no longer look in `types.xsd` for a hint of which API version the server is running. Instead, - we query the service directly, starting with the latest version first. - +- It is now possible to use Kerberos and SSPI auth without providing a dummy + `Credentials('', '')` object. +- The `has_ssl` argument of `Configuration` was removed. If you want to connect to a plain HTTP endpoint, pass the full + URL in the `service_endpoint` argument. +- We no longer look in `types.xsd` for a hint of which API version the server is running. Instead, we query the service + directly, starting with the latest version first. 1.12.5 ------ -- Bugfix release. +- Bugfix release. 1.12.4 ------ -- Fix bug that left out parts of the folder hierarchy when traversing `account.root`. -- Fix bug that did not properly find all attachments if an item has a mix of item - and file attachments. +- Fix bug that left out parts of the folder hierarchy when traversing `account.root`. +- Fix bug that did not properly find all attachments if an item has a mix of item and file attachments. 1.12.3 ------ -- Add support for reading and writing `PermissionSet` field on folders. -- Add support for Exchange 2019 build IDs. +- Add support for reading and writing `PermissionSet` field on folders. +- Add support for Exchange 2019 build IDs. 1.12.2 ------ -- Add `Protocol.expand_dl()` to get members of a distribution list. +- Add `Protocol.expand_dl()` to get members of a distribution list. 1.12.1 ------ -- Lower the session pool size automatically in response to ErrorServerBusy and - ErrorTooManyObjectsOpened errors from the server. -- Unusual slicing and indexing (e.g. `inbox.all()[9000]` and `inbox.all()[9000:9001]`) - is now efficient. -- Downloading large attachments is now more memory-efficient. We can now stream the file - content without ever storing the full file content in memory, using the new - `Attachment.fp` context manager. + +- Lower the session pool size automatically in response to ErrorServerBusy and ErrorTooManyObjectsOpened errors from the + server. +- Unusual slicing and indexing (e.g. `inbox.all()[9000]` and `inbox.all()[9000:9001]`) + is now efficient. +- Downloading large attachments is now more memory-efficient. We can now stream the file content without ever storing + the full file content in memory, using the new + `Attachment.fp` context manager. 1.12.0 ------ -- Add a MAINFEST.in to ensure the LICENSE file gets included + CHANGELOG.md - and README.md to sdist tarball -- Renamed `Item.item_id`, `Folder.folder_id` and `Occurrence.item_id` to just - `Item.id`, `Folder.id` and `Occurrence.id`, respectively. This removes - redundancy in the naming and provides consistency. For all classes that - have an ID, the ID can now be accessed using the `id` attribute. Backwards - compatibility and deprecation warnings were added. -- Support folder traversal without creating a full cache of the folder - hierarchy first, using the `some_folder // 'sub_folder' // 'leaf'` - (double-slash) syntax. -- Fix a bug in traversal of public and archive folders. These folder - hierarchies are now fully supported. -- Fix a bug where the timezone of a calendar item changed when the item was - fetched and then saved. -- Kerberos support is now optional and Kerberos dependencies are not - installed by default. Install as `pip install exchangelib[kerberos]` to get - Kerberos support. +- Add a MAINFEST.in to ensure the LICENSE file gets included + CHANGELOG.md and README.md to sdist tarball +- Renamed `Item.item_id`, `Folder.folder_id` and `Occurrence.item_id` to just + `Item.id`, `Folder.id` and `Occurrence.id`, respectively. This removes redundancy in the naming and provides + consistency. For all classes that have an ID, the ID can now be accessed using the `id` attribute. Backwards + compatibility and deprecation warnings were added. +- Support folder traversal without creating a full cache of the folder hierarchy first, using + the `some_folder // 'sub_folder' // 'leaf'` + (double-slash) syntax. +- Fix a bug in traversal of public and archive folders. These folder hierarchies are now fully supported. +- Fix a bug where the timezone of a calendar item changed when the item was fetched and then saved. +- Kerberos support is now optional and Kerberos dependencies are not installed by default. Install + as `pip install exchangelib[kerberos]` to get Kerberos support. 1.11.4 ------ -- Improve back off handling when receiving `ErrorServerBusy` error messages - from the server -- Fixed bug where `Account.root` and its children would point to the root - folder of the connecting account instead of the target account when - connecting to other accounts. +- Improve back off handling when receiving `ErrorServerBusy` error messages from the server +- Fixed bug where `Account.root` and its children would point to the root folder of the connecting account instead of + the target account when connecting to other accounts. 1.11.3 ------ -- Add experimental Kerberos support. This adds the `pykerberos` package, - which needs the following system packages to be installed on Ubuntu/Debian - systems: `apt-get install build-essential libssl-dev libffi-dev python-dev libkrb5-dev`. +- Add experimental Kerberos support. This adds the `pykerberos` package, which needs the following system packages to be + installed on Ubuntu/Debian systems: `apt-get install build-essential libssl-dev libffi-dev python-dev libkrb5-dev`. 1.11.2 ------ -- Bugfix release +- Bugfix release 1.11.1 ------ -- Bugfix release +- Bugfix release 1.11.0 ------ -- Added `cancel` to `CalendarItem` and `CancelCalendarItem` class to - allow cancelling meetings that were set up -- Added `accept`, `decline` and `tentatively_accept` to `CalendarItem` - as wrapper methods -- Added `accept`, `decline` and `tentatively_accept` to - `MeetingRequest` to respond to incoming invitations -- Added `BaseMeetingItem` (inheriting from `Item`) being used as base - for MeetingCancellation, MeetingMessage, MeetingRequest and - MeetingResponse -- Added `AssociatedCalendarItemId` (property), - `AssociatedCalendarItemIdField` and `ReferenceItemIdField` -- Added `PostReplyItem` -- Removed `Folder.get_folder_by_name()` which has been deprecated - since version `1.10.2`. -- Added `Item.copy(to_folder=some_folder)` method which copies an item - to the given folder and returns the ID of the new item. -- We now respect the back off value of an `ErrorServerBusy` - server error. -- Added support for fetching free/busy availability information ofr a - list of accounts. -- Added `Message.reply()`, `Message.reply_all()`, and - `Message.forward()` methods. -- The full search API now works on single folders *and* collections of - folders, e.g. `some_folder.glob('foo*').filter()`, - `some_folder.children.filter()` and `some_folder.walk().filter()`. -- Deprecated `EWSService.CHUNKSIZE` in favor of a per-request - chunk\_size available on `Account.bulk_foo()` methods. -- Support searching the GAL and other contact folders using - `some_contact_folder.people()`. -- Deprecated the `page_size` argument for `QuerySet.iterator()` because it - was inconsistent with other API methods. You can still set the page size - of a queryset like this: +- Added `cancel` to `CalendarItem` and `CancelCalendarItem` class to allow cancelling meetings that were set up +- Added `accept`, `decline` and `tentatively_accept` to `CalendarItem` + as wrapper methods +- Added `accept`, `decline` and `tentatively_accept` to + `MeetingRequest` to respond to incoming invitations +- Added `BaseMeetingItem` (inheriting from `Item`) being used as base for MeetingCancellation, MeetingMessage, + MeetingRequest and MeetingResponse +- Added `AssociatedCalendarItemId` (property), + `AssociatedCalendarItemIdField` and `ReferenceItemIdField` +- Added `PostReplyItem` +- Removed `Folder.get_folder_by_name()` which has been deprecated since version `1.10.2`. +- Added `Item.copy(to_folder=some_folder)` method which copies an item to the given folder and returns the ID of the new + item. +- We now respect the back off value of an `ErrorServerBusy` + server error. +- Added support for fetching free/busy availability information ofr a list of accounts. +- Added `Message.reply()`, `Message.reply_all()`, and + `Message.forward()` methods. +- The full search API now works on single folders *and* collections of folders, e.g. `some_folder.glob('foo*').filter()` + , + `some_folder.children.filter()` and `some_folder.walk().filter()`. +- Deprecated `EWSService.CHUNKSIZE` in favor of a per-request chunk\_size available on `Account.bulk_foo()` methods. +- Support searching the GAL and other contact folders using + `some_contact_folder.people()`. +- Deprecated the `page_size` argument for `QuerySet.iterator()` because it was inconsistent with other API methods. You + can still set the page size of a queryset like this: ```python qs = a.inbox.filter(...).iterator() @@ -395,231 +561,194 @@ shared_calendar = SingleFolderQuerySet(account=account, folder=DistinguishedFold 1.10.7 ------ -- Added support for registering extended properties on folders. -- Added support for creating, updating, deleting and emptying folders. +- Added support for registering extended properties on folders. +- Added support for creating, updating, deleting and emptying folders. 1.10.6 ------ -- Added support for getting and setting `Account.oof_settings` using - the new `OofSettings` class. -- Added snake\_case named shortcuts to all distinguished folders on - the `Account` model. E.g. `Account.search_folders`. +- Added support for getting and setting `Account.oof_settings` using the new `OofSettings` class. +- Added snake\_case named shortcuts to all distinguished folders on the `Account` model. E.g. `Account.search_folders`. 1.10.5 ------ -- Bugfix release +- Bugfix release 1.10.4 ------ -- Added support for most item fields. The remaining ones are mentioned - in issue \#203. +- Added support for most item fields. The remaining ones are mentioned in issue \#203. 1.10.3 ------ -- Added an `exchangelib.util.PrettyXmlHandler` log handler which will - pretty-print and highlight XML requests and responses. +- Added an `exchangelib.util.PrettyXmlHandler` log handler which will pretty-print and highlight XML requests and + responses. 1.10.2 ------ -- Greatly improved folder navigation. See the 'Folders' section in the - README -- Added deprecation warnings for `Account.folders` and - `Folder.get_folder_by_name()` +- Greatly improved folder navigation. See the 'Folders' section in the README +- Added deprecation warnings for `Account.folders` and + `Folder.get_folder_by_name()` 1.10.1 ------ -- Bugfix release +- Bugfix release 1.10.0 ------ -- Removed the `verify_ssl` argument to `Account`, `discover` and - `Configuration`. If you need to disable TLS verification, register a - custom `HTTPAdapter` class. A sample adapter class is provided for - convenience: +- Removed the `verify_ssl` argument to `Account`, `discover` and + `Configuration`. If you need to disable TLS verification, register a custom `HTTPAdapter` class. A sample adapter + class is provided for convenience: ```python from exchangelib.protocol import BaseProtocol, NoVerifyHTTPAdapter + BaseProtocol.HTTP_ADAPTER_CLS = NoVerifyHTTPAdapter ``` 1.9.6 ----- -- Support new Office365 build numbers +- Support new Office365 build numbers 1.9.5 ----- -- Added support for the `effective_rights`field on items and folders. -- Added support for custom `requests` transport adapters, to allow - proxy support, custom TLS validation etc. -- Default value for the `affected_task_occurrences` argument to - `Item.move_to_trash()`, `Item.soft_delete()` and `Item.delete()` was - changed to `'AllOccurrences'` as a less surprising default when - working with simple tasks. -- Added `Task.complete()` helper method to mark tasks as complete. +- Added support for the `effective_rights`field on items and folders. +- Added support for custom `requests` transport adapters, to allow proxy support, custom TLS validation etc. +- Default value for the `affected_task_occurrences` argument to + `Item.move_to_trash()`, `Item.soft_delete()` and `Item.delete()` was changed to `'AllOccurrences'` as a less + surprising default when working with simple tasks. +- Added `Task.complete()` helper method to mark tasks as complete. 1.9.4 ----- -- Added minimal support for the `PostItem` item type -- Added support for the `DistributionList` item type -- Added support for receiving naive datetimes from the server. They - will be localized using the new `default_timezone` attribute on - `Account` -- Added experimental support for recurring calendar items. See - examples in issue \#37. +- Added minimal support for the `PostItem` item type +- Added support for the `DistributionList` item type +- Added support for receiving naive datetimes from the server. They will be localized using the new `default_timezone` + attribute on + `Account` +- Added experimental support for recurring calendar items. See examples in issue \#37. 1.9.3 ----- -- Improved support for `filter()`, `.only()`, `.order_by()` etc. on - indexed properties. It is now possible to specify labels and - subfields, e.g. - `.filter(phone_numbers=PhoneNumber(label='CarPhone', phone_number='123'))` - `.filter(phone_numbers__CarPhone='123')`, - `.filter(physical_addresses__Home__street='Elm St. 123')`, - .only('physical\_addresses\_\_Home\_\_street')\` etc. -- Improved performance of `.order_by()` when sorting on - multiple fields. -- Implemented QueryString search. You can now filter using an EWS - QueryString, e.g. `filter('subject:XXX')` +- Improved support for `filter()`, `.only()`, `.order_by()` etc. on indexed properties. It is now possible to specify + labels and subfields, e.g. + `.filter(phone_numbers=PhoneNumber(label='CarPhone', phone_number='123'))` + `.filter(phone_numbers__CarPhone='123')`, + `.filter(physical_addresses__Home__street='Elm St. 123')`, .only('physical\_addresses\_\_Home\_\_street')\` etc. +- Improved performance of `.order_by()` when sorting on multiple fields. +- Implemented QueryString search. You can now filter using an EWS QueryString, e.g. `filter('subject:XXX')` 1.9.2 ----- -- Added `EWSTimeZone.localzone()` to get the local timezone -- Support `some_folder.get(item_id=..., changekey=...)` as a shortcut - to get a single item when you know the ID and changekey. -- Support attachments on Exchange 2007 +- Added `EWSTimeZone.localzone()` to get the local timezone +- Support `some_folder.get(item_id=..., changekey=...)` as a shortcut to get a single item when you know the ID and + changekey. +- Support attachments on Exchange 2007 1.9.1 ----- -- Fixed XML generation for Exchange 2010 and other picky server - versions -- Fixed timezone localization for `EWSTimeZone` created from a static - timezone +- Fixed XML generation for Exchange 2010 and other picky server versions +- Fixed timezone localization for `EWSTimeZone` created from a static timezone 1.9.0 ----- -- Expand support for `ExtendedProperty` to include all - possible attributes. This required renaming the `property_id` - attribute to `property_set_id`. -- When using the `Credentials` class, `UnauthorizedError` is now - raised if the credentials are wrong. -- Add a new `version` attribute to `Configuration`, to force the - server version if version guessing does not work. Accepts a - `exchangelib.version.Version` object. -- Rework bulk operations `Account.bulk_foo()` and `Account.fetch()` to - return some exceptions unraised, if it is deemed the exception does - not apply to all items. This means that e.g. `fetch()` can return a - mix of `` `Item `` and `ErrorItemNotFound` instances, if only some - of the requested `ItemId` were valid. Other exceptions will be - raised immediately, e.g. `ErrorNonExistentMailbox` because the - exception applies to all items. It is the responsibility of the - caller to check the type of the returned values. -- The `Folder` class has new attributes `total_count`, `unread_count` - and `child_folder_count`, and a `refresh()` method to update - these values. -- The argument to `Account.upload()` was renamed from `upload_data` to - just `data` -- Support for using a string search expression for `Folder.filter()` - was removed. It was a cool idea but using QuerySet chaining and `Q` - objects is even cooler and provides the same functionality, - and more. -- Add support for `reminder_due_by` and - `reminder_minutes_before_start` fields on `Item` objects. Submitted - by `@vikipha`. -- Added a new `ServiceAccount` class which is like `Credentials` but - does what `is_service_account` did before. If you need - fault-tolerane and used `Credentials(..., is_service_account=True)` - before, use `ServiceAccount` now. This also disables fault-tolerance - for the `Credentials` class, which is in line with what most - users expected. -- Added an optional `update_fields` attribute to `save()` to specify - only some fields to be updated. -- Code in in `folders.py` has been split into multiple files, and some - classes will have new import locaions. The most commonly used - classes have a shortcut in \_\_init\_\_.py -- Added support for the `exists` lookup in filters, e.g. - `my_folder.filter(categories__exists=True|False)` to filter on the - existence of that field on items in the folder. -- When filtering, `foo__in=value` now requires the value to be a list, - and `foo__contains` requires the value to be a list if the field - itself is a list, e.g. `categories__contains=['a', 'b']`. -- Added support for fields and enum entries that are only supported in - some EWS versions -- Added a new field `Item.text_body` which is a read-only version of - HTML body content, where HTML tags are stripped by the server. Only - supported from Exchange 2013 and up. -- Added a new choice `WorkingElsewhere` to the - `CalendarItem.legacy_free_busy_status` enum. Only supported from - Exchange 2013 and up. +- Expand support for `ExtendedProperty` to include all possible attributes. This required renaming the `property_id` + attribute to `property_set_id`. +- When using the `Credentials` class, `UnauthorizedError` is now raised if the credentials are wrong. +- Add a new `version` attribute to `Configuration`, to force the server version if version guessing does not work. + Accepts a + `exchangelib.version.Version` object. +- Rework bulk operations `Account.bulk_foo()` and `Account.fetch()` to return some exceptions unraised, if it is deemed + the exception does not apply to all items. This means that e.g. `fetch()` can return a mix of `` `Item `` + and `ErrorItemNotFound` instances, if only some of the requested `ItemId` were valid. Other exceptions will be raised + immediately, e.g. `ErrorNonExistentMailbox` because the exception applies to all items. It is the responsibility of + the caller to check the type of the returned values. +- The `Folder` class has new attributes `total_count`, `unread_count` + and `child_folder_count`, and a `refresh()` method to update these values. +- The argument to `Account.upload()` was renamed from `upload_data` to just `data` +- Support for using a string search expression for `Folder.filter()` + was removed. It was a cool idea but using QuerySet chaining and `Q` + objects is even cooler and provides the same functionality, and more. +- Add support for `reminder_due_by` and + `reminder_minutes_before_start` fields on `Item` objects. Submitted by `@vikipha`. +- Added a new `ServiceAccount` class which is like `Credentials` but does what `is_service_account` did before. If you + need fault-tolerane and used `Credentials(..., is_service_account=True)` + before, use `ServiceAccount` now. This also disables fault-tolerance for the `Credentials` class, which is in line + with what most users expected. +- Added an optional `update_fields` attribute to `save()` to specify only some fields to be updated. +- Code in in `folders.py` has been split into multiple files, and some classes will have new import locaions. The most + commonly used classes have a shortcut in \_\_init\_\_.py +- Added support for the `exists` lookup in filters, e.g. + `my_folder.filter(categories__exists=True|False)` to filter on the existence of that field on items in the folder. +- When filtering, `foo__in=value` now requires the value to be a list, and `foo__contains` requires the value to be a + list if the field itself is a list, e.g. `categories__contains=['a', 'b']`. +- Added support for fields and enum entries that are only supported in some EWS versions +- Added a new field `Item.text_body` which is a read-only version of HTML body content, where HTML tags are stripped by + the server. Only supported from Exchange 2013 and up. +- Added a new choice `WorkingElsewhere` to the + `CalendarItem.legacy_free_busy_status` enum. Only supported from Exchange 2013 and up. 1.8.1 ----- -- Fix completely botched `Message.from` field renaming in 1.8.0 -- Improve performance of QuerySet slicing and indexing. For example, - `account.inbox.all()[10]` and `account.inbox.all()[:10]` now only - fetch 10 items from the server even though `account.inbox.all()` - could contain thousands of messages. +- Fix completely botched `Message.from` field renaming in 1.8.0 +- Improve performance of QuerySet slicing and indexing. For example, + `account.inbox.all()[10]` and `account.inbox.all()[:10]` now only fetch 10 items from the server even + though `account.inbox.all()` + could contain thousands of messages. 1.8.0 ----- -- Renamed `Message.from` field to `Message.author`. `from` is a Python - keyword so `from` could only be accessed as - `Getattr(my_essage, 'from')` which is just stupid. -- Make `EWSTimeZone` Windows timezone name translation more robust -- Add read-only `Message.message_id` which holds the Internet Message - Id -- Memory and speed improvements when sorting querysets using - `order_by()` on a single field. -- Allow setting `Mailbox` and `Attendee`-type attributes as plain - strings, e.g.: +- Renamed `Message.from` field to `Message.author`. `from` is a Python keyword so `from` could only be accessed as + `Getattr(my_essage, 'from')` which is just stupid. +- Make `EWSTimeZone` Windows timezone name translation more robust +- Add read-only `Message.message_id` which holds the Internet Message Id +- Memory and speed improvements when sorting querysets using + `order_by()` on a single field. +- Allow setting `Mailbox` and `Attendee`-type attributes as plain strings, e.g.: ```python - calendar_item.organizer = 'anne@example.com' - calendar_item.required_attendees = ['john@example.com', 'bill@example.com'] + calendar_item.organizer = "anne@example.com" + calendar_item.required_attendees = ["john@example.com", "bill@example.com"] - message.to_recipients = ['john@example.com', 'anne@example.com'] + message.to_recipients = ["john@example.com", "anne@example.com"] ``` 1.7.6 ----- -- Bugfix release +- Bugfix release 1.7.5 ----- -- `Account.fetch()` and `Folder.fetch()` are now generators. They will - do nothing before being evaluated. -- Added optional `page_size` attribute to `QuerySet.iterator()` to - specify the number of items to return per HTTP request for large - query results. Default `page_size` is 100. -- Many minor changes to make queries less greedy and return earlier +- `Account.fetch()` and `Folder.fetch()` are now generators. They will do nothing before being evaluated. +- Added optional `page_size` attribute to `QuerySet.iterator()` to specify the number of items to return per HTTP + request for large query results. Default `page_size` is 100. +- Many minor changes to make queries less greedy and return earlier 1.7.4 ----- -- Add Python2 support +- Add Python2 support 1.7.3 ----- -- Implement attachments support. It's now possible to create, delete - and get attachments connected to any item type: +- Implement attachments support. It's now possible to create, delete and get attachments connected to any item type: ```python from exchangelib.folders import FileAttachment, ItemAttachment @@ -627,252 +756,224 @@ shared_calendar = SingleFolderQuerySet(account=account, folder=DistinguishedFold # Process attachments on existing items for item in my_folder.all(): for attachment in item.attachments: - local_path = os.path.join('/tmp', attachment.name) - with open(local_path, 'wb') as f: + local_path = os.path.join("/tmp", attachment.name) + with open(local_path, "wb") as f: f.write(attachment.content) - print('Saved attachment to', local_path) + print("Saved attachment to", local_path) # Create a new item with an attachment item = Message(...) - binary_file_content = 'Hello from unicode æøå'.encode('utf-8') # Or read from file, BytesIO etc. - my_file = FileAttachment(name='my_file.txt', content=binary_file_content) + binary_file_content = "Hello from unicode æøå".encode( + "utf-8" + ) # Or read from file, BytesIO etc. + my_file = FileAttachment(name="my_file.txt", content=binary_file_content) item.attach(my_file) my_calendar_item = CalendarItem(...) - my_appointment = ItemAttachment(name='my_appointment', item=my_calendar_item) + my_appointment = ItemAttachment(name="my_appointment", item=my_calendar_item) item.attach(my_appointment) item.save() # Add an attachment on an existing item - my_other_file = FileAttachment(name='my_other_file.txt', content=binary_file_content) + my_other_file = FileAttachment(name="my_other_file.txt", content=binary_file_content) item.attach(my_other_file) # Remove the attachment again item.detach(my_file) ``` - Be aware that adding and deleting attachments from items that are - already created in Exchange (items that have an `item_id`) will - update the `changekey` of the item. + Be aware that adding and deleting attachments from items that are already created in Exchange (items that have + an `item_id`) will update the `changekey` of the item. -- Implement `Item.headers` which contains custom Internet - message headers. Primarily useful for `Message` objects. Read-only - for now. +- Implement `Item.headers` which contains custom Internet message headers. Primarily useful for `Message` objects. + Read-only for now. 1.7.2 ----- -- Implement the `Contact.physical_addresses` attribute. This is a list - of `exchangelib.folders.PhysicalAddress` items. -- Implement the `CalendarItem.is_all_day` boolean to create - all-day appointments. -- Implement `my_folder.export()` and `my_folder.upload()`. Thanks to - @SamCB! -- Fixed `Account.folders` for non-distinguished folders -- Added `Folder.get_folder_by_name()` to make it easier to get - sub-folders by name. -- Implement `CalendarView` searches as - `my_calendar.view(start=..., end=...)`. A view differs from a normal - `filter()` in that a view expands recurring items and returns - recurring item occurrences that are valid in the time span of - the view. -- Persistent storage location for autodiscover cache is now platform - independent -- Implemented custom extended properties. To add support for your own - custom property, subclass `exchangelib.folders.ExtendedProperty` and - call `register()` on the item class you want to use the extended - property with. When you have registered your extended property, you - can use it exactly like you would use any other attribute on this - item type. If you change your mind, you can remove the extended - property again with `deregister()`: +- Implement the `Contact.physical_addresses` attribute. This is a list of `exchangelib.folders.PhysicalAddress` items. +- Implement the `CalendarItem.is_all_day` boolean to create all-day appointments. +- Implement `my_folder.export()` and `my_folder.upload()`. Thanks to @SamCB! +- Fixed `Account.folders` for non-distinguished folders +- Added `Folder.get_folder_by_name()` to make it easier to get sub-folders by name. +- Implement `CalendarView` searches as + `my_calendar.view(start=..., end=...)`. A view differs from a normal + `filter()` in that a view expands recurring items and returns recurring item occurrences that are valid in the time + span of the view. +- Persistent storage location for autodiscover cache is now platform independent +- Implemented custom extended properties. To add support for your own custom property, + subclass `exchangelib.folders.ExtendedProperty` and call `register()` on the item class you want to use the extended + property with. When you have registered your extended property, you can use it exactly like you would use any other + attribute on this item type. If you change your mind, you can remove the extended property again with `deregister()`: ```python class LunchMenu(ExtendedProperty): - property_id = '12345678-1234-1234-1234-123456781234' - property_name = 'Catering from the cafeteria' - property_type = 'String' + property_id = "12345678-1234-1234-1234-123456781234" + property_name = "Catering from the cafeteria" + property_type = "String" + - CalendarItem.register('lunch_menu', LunchMenu) - item = CalendarItem(..., lunch_menu='Foie gras et consommé de légumes') + CalendarItem.register("lunch_menu", LunchMenu) + item = CalendarItem(..., lunch_menu="Foie gras et consommé de légumes") item.save() - CalendarItem.deregister('lunch_menu') + CalendarItem.deregister("lunch_menu") ``` -- Fixed a bug on folder items where an existing HTML body would be - converted to text when calling `save()`. When creating or updating - an item body, you can use the two new helper classes - `exchangelib.Body` and `exchangelib.HTMLBody` to specify if your - body should be saved as HTML or text. E.g.: +- Fixed a bug on folder items where an existing HTML body would be converted to text when calling `save()`. When + creating or updating an item body, you can use the two new helper classes + `exchangelib.Body` and `exchangelib.HTMLBody` to specify if your body should be saved as HTML or text. E.g.: ```python item = CalendarItem(...) # Plain-text body - item.body = Body('Hello UNIX-beard pine user!') + item.body = Body("Hello UNIX-beard pine user!") # Also plain-text body, works as before - item.body = 'Hello UNIX-beard pine user!' + item.body = "Hello UNIX-beard pine user!" # Exchange will see this as an HTML body and display nicely in clients - item.body = HTMLBody('Hello happy OWA user!') + item.body = HTMLBody("Hello happy OWA user!") item.save() ``` 1.7.1 ----- -- Fix bug where fetching items from a folder that can contain multiple - item types (e.g. the Deleted Items folder) would only return one - item type. -- Added `Item.move(to_folder=...)` that moves an item to another - folder, and `Item.refresh()` that updates the Item with data - from EWS. -- Support reverse sort on individual fields in `order_by()`, e.g. - `my_folder.all().order_by('subject', '-start')` -- `Account.bulk_create()` was added to create items that don't need a - folder, e.g. `Message.send()` -- `Account.fetch()` was added to fetch items without knowing the - containing folder. -- Implemented `SendItem` service to send existing messages. -- `Folder.bulk_delete()` was moved to `Account.bulk_delete()` -- `Folder.bulk_update()` was moved to `Account.bulk_update()` and - changed to expect a list of `(Item, fieldnames)` tuples where Item - is e.g. a `Message` instance and `fieldnames` is a list of - attributes names that need updating. E.g.: +- Fix bug where fetching items from a folder that can contain multiple item types (e.g. the Deleted Items folder) would + only return one item type. +- Added `Item.move(to_folder=...)` that moves an item to another folder, and `Item.refresh()` that updates the Item with + data from EWS. +- Support reverse sort on individual fields in `order_by()`, e.g. + `my_folder.all().order_by('subject', '-start')` +- `Account.bulk_create()` was added to create items that don't need a folder, e.g. `Message.send()` +- `Account.fetch()` was added to fetch items without knowing the containing folder. +- Implemented `SendItem` service to send existing messages. +- `Folder.bulk_delete()` was moved to `Account.bulk_delete()` +- `Folder.bulk_update()` was moved to `Account.bulk_update()` and changed to expect a list of `(Item, fieldnames)` + tuples where Item is e.g. a `Message` instance and `fieldnames` is a list of attributes names that need updating. + E.g.: ```python items = [] for i in range(4): - item = Message(subject='Test %s' % i) + item = Message(subject="Test %s" % i) items.append(item) account.sent.bulk_create(items=items) item_changes = [] for i, item in enumerate(items): - item.subject = 'Changed subject' % i - item_changes.append(item, ['subject']) + item.subject = "Changed subject" % i + item_changes.append(item, ["subject"]) account.bulk_update(items=item_changes) ``` 1.7.0 ----- -- Added the `is_service_account` flag to `Credentials`. - `is_service_account=False` disables the fault-tolerant error - handling policy and enables immediate failures. -- `Configuration` now expects a single `credentials` attribute instead - of separate `username` and `password` attributes. -- Added support for distinguished folders `Account.trash`, - `Account.drafts`, `Account.outbox`, `Account.sent` and - `Account.junk`. -- Renamed `Folder.find_items()` to `Folder.filter()` -- Renamed `Folder.add_items()` to `Folder.bulk_create()` -- Renamed `Folder.update_items()` to `Folder.bulk_update()` -- Renamed `Folder.delete_items()` to `Folder.bulk_delete()` -- Renamed `Folder.get_items()` to `Folder.fetch()` -- Made various policies for message saving, meeting invitation - sending, conflict resolution, task occurrences and deletion - available on `bulk_create()`, `bulk_update()` and `bulk_delete()`. -- Added convenience methods `Item.save()`, `Item.delete()`, - `Item.soft_delete()`, `Item.move_to_trash()`, and methods - `Message.send()` and `Message.send_and_save()` that are specific to - `Message` objects. These methods make it easier to create, update - and delete single items. -- Removed `fetch(.., with_extra=True)` in favor of the more - fine-grained `fetch(.., only_fields=[...])` -- Added a `QuerySet` class that supports QuerySet-returning methods - `filter()`, `exclude()`, `only()`, `order_by()`, - `reverse()``values()` and `values_list()` that all allow - for chaining. `QuerySet` also has methods `iterator()`, `get()`, - `count()`, `exists()` and `delete()`. All these methods behave like - their counterparts in Django. +- Added the `is_service_account` flag to `Credentials`. + `is_service_account=False` disables the fault-tolerant error handling policy and enables immediate failures. +- `Configuration` now expects a single `credentials` attribute instead of separate `username` and `password` attributes. +- Added support for distinguished folders `Account.trash`, + `Account.drafts`, `Account.outbox`, `Account.sent` and + `Account.junk`. +- Renamed `Folder.find_items()` to `Folder.filter()` +- Renamed `Folder.add_items()` to `Folder.bulk_create()` +- Renamed `Folder.update_items()` to `Folder.bulk_update()` +- Renamed `Folder.delete_items()` to `Folder.bulk_delete()` +- Renamed `Folder.get_items()` to `Folder.fetch()` +- Made various policies for message saving, meeting invitation sending, conflict resolution, task occurrences and + deletion available on `bulk_create()`, `bulk_update()` and `bulk_delete()`. +- Added convenience methods `Item.save()`, `Item.delete()`, + `Item.soft_delete()`, `Item.move_to_trash()`, and methods + `Message.send()` and `Message.send_and_save()` that are specific to + `Message` objects. These methods make it easier to create, update and delete single items. +- Removed `fetch(.., with_extra=True)` in favor of the more fine-grained `fetch(.., only_fields=[...])` +- Added a `QuerySet` class that supports QuerySet-returning methods + `filter()`, `exclude()`, `only()`, `order_by()`, + `reverse()`, `values()` and `values_list()` that all allow for chaining. `QuerySet` also has methods `iterator()` + , `get()`, + `count()`, `exists()` and `delete()`. All these methods behave like their counterparts in Django. 1.6.2 ----- -- Use of `my_folder.with_extra_fields = True` to get the extra fields - in `Item.EXTRA_ITEM_FIELDS` is deprecated (it was a kludge anyway). - Instead, use `my_folder.get_items(ids, with_extra=[True, False])`. - The default was also changed to `True`, to avoid head-scratching - with newcomers. +- Use of `my_folder.with_extra_fields = True` to get the extra fields in `Item.EXTRA_ITEM_FIELDS` is deprecated (it was + a kludge anyway). Instead, use `my_folder.get_items(ids, with_extra=[True, False])`. The default was also changed + to `True`, to avoid head-scratching with newcomers. 1.6.1 ----- -- Simplify `Q` objects and `Restriction.from_source()` by using Item - attribute names in expressions and kwargs instead of EWS - FieldURI values. Change `Folder.find_items()` to accept either a - search expression, or a list of `Q` objects just like Django - `filter()` does. E.g.: +- Simplify `Q` objects and `Restriction.from_source()` by using Item attribute names in expressions and kwargs instead + of EWS FieldURI values. Change `Folder.find_items()` to accept either a search expression, or a list of `Q` objects + just like Django + `filter()` does. E.g.: ```python ids = account.calendar.find_items( - "start < '2016-01-02T03:04:05T' and end > '2016-01-01T03:04:05T' and categories in ('foo', 'bar')", - shape=IdOnly + "start < '2016-01-02T03:04:05T' and end > '2016-01-01T03:04:05T' and categories in ('foo', 'bar')", + shape=IdOnly, ) - q1, q2 = (Q(subject__iexact='foo') | Q(subject__contains='bar')), ~Q(subject__startswith='baz') + q1, q2 = (Q(subject__iexact="foo") | Q(subject__contains="bar")), ~Q( + subject__startswith="baz" + ) ids = account.calendar.find_items(q1, q2, shape=IdOnly) ``` 1.6.0 ----- -- Complete rewrite of `Folder.find_items()`. The old `start`, `end`, - `subject` and `categories` args are deprecated in favor of a Django - QuerySet filter() syntax. The supported lookup types are `__gt`, - `__lt`, `__gte`, `__lte`, `__range`, `__in`, `__exact`, `__iexact`, - `__contains`, `__icontains`, `__contains`, `__icontains`, - `__startswith`, `__istartswith`, plus an additional `__not` which - translates to `!=`. Additionally, *all* fields on the item are now - supported in `Folder.find_items()`. +- Complete rewrite of `Folder.find_items()`. The old `start`, `end`, + `subject` and `categories` args are deprecated in favor of a Django QuerySet filter() syntax. The supported lookup + types are `__gt`, + `__lt`, `__gte`, `__lte`, `__range`, `__in`, `__exact`, `__iexact`, + `__contains`, `__icontains`, `__contains`, `__icontains`, + `__startswith`, `__istartswith`, plus an additional `__not` which translates to `!=`. Additionally, *all* fields on + the item are now supported in `Folder.find_items()`. - **WARNING**: This change is backwards-incompatible! Old uses of - `Folder.find_items()` like this: + **WARNING**: This change is backwards-incompatible! Old uses of + `Folder.find_items()` like this: ```python ids = account.calendar.find_items( start=tz.localize(EWSDateTime(year, month, day)), end=tz.localize(EWSDateTime(year, month, day + 1)), - categories=['foo', 'bar'], + categories=["foo", "bar"], ) ``` - must be rewritten like this: + must be rewritten like this: ```python ids = account.calendar.find_items( start__lt=tz.localize(EWSDateTime(year, month, day + 1)), end__gt=tz.localize(EWSDateTime(year, month, day)), - categories__contains=['foo', 'bar'], + categories__contains=["foo", "bar"], ) ``` - failing to do so will most likely result in empty or wrong results. + failing to do so will most likely result in empty or wrong results. -- Added a `exchangelib.restrictions.Q` class much like Django Q - objects that can be used to create even more complex filtering. Q - objects must be passed directly to `exchangelib.services.FindItem`. +- Added a `exchangelib.restrictions.Q` class much like Django Q objects that can be used to create even more complex + filtering. Q objects must be passed directly to `exchangelib.services.FindItem`. 1.3.6 ----- -- Don't require sequence arguments to `Folder.*_items()` methods to - support `len()` (e.g. generators and `map` instances are - now supported) -- Allow empty sequences as argument to `Folder.*_items()` methods +- Don't require sequence arguments to `Folder.*_items()` methods to support `len()` (e.g. generators and `map` instances + are now supported) +- Allow empty sequences as argument to `Folder.*_items()` methods 1.3.4 ----- -- Add support for `required_attendees`, `optional_attendees` and - `resources` attribute on `folders.CalendarItem`. These are - implemented with a new `folders.Attendee` class. +- Add support for `required_attendees`, `optional_attendees` and + `resources` attribute on `folders.CalendarItem`. These are implemented with a new `folders.Attendee` class. 1.3.3 ----- -- Add support for `organizer` attribute on `CalendarItem`. Implemented - with a new `folders.Mailbox` class. +- Add support for `organizer` attribute on `CalendarItem`. Implemented with a new `folders.Mailbox` class. 1.2 --- -- Initial import - +- Initial import diff --git a/MANIFEST.in b/MANIFEST.in index ab277940..fab603fb 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,3 +2,4 @@ include MANIFEST.in include LICENSE include CHANGELOG.md include README.md +exclude tests/* diff --git a/README.md b/README.md index b20102f1..23f00dfb 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,7 @@ exporting and uploading calendar, mailbox, task, contact and distribution list i [![image](https://img.shields.io/pypi/v/exchangelib.svg)](https://pypi.org/project/exchangelib/) [![image](https://img.shields.io/pypi/pyversions/exchangelib.svg)](https://pypi.org/project/exchangelib/) -[![image](https://api.codacy.com/project/badge/Grade/5f805ad901054a889f4b99a82d6c1cb7)](https://www.codacy.com/app/ecederstrand/exchangelib) -[![image](https://api.travis-ci.com/ecederstrand/exchangelib.png)](http://travis-ci.com/ecederstrand/exchangelib) +[![image](https://api.codacy.com/project/badge/Grade/5f805ad901054a889f4b99a82d6c1cb7)](https://app.codacy.com/gh/ecederstrand/exchangelib) [![image](https://coveralls.io/repos/github/ecederstrand/exchangelib/badge.svg?branch=master)](https://coveralls.io/github/ecederstrand/exchangelib?branch=master) [![xscode](https://img.shields.io/badge/Available%20on-xs%3Acode-blue)](https://xscode.com/ecederstrand/exchangelib) @@ -23,10 +22,10 @@ Here's a short example of how `exchangelib` works. Let's print the first ```python from exchangelib import Credentials, Account -credentials = Credentials('john@example.com', 'topsecret') -account = Account('john@example.com', credentials=credentials, autodiscover=True) +credentials = Credentials("john@example.com", "topsecret") +account = Account("john@example.com", credentials=credentials, autodiscover=True) -for item in account.inbox.all().order_by('-datetime_received')[:100]: +for item in account.inbox.all().order_by("-datetime_received")[:100]: print(item.subject, item.sender, item.datetime_received) ``` diff --git a/dev-requirements.txt b/dev-requirements.txt new file mode 100644 index 00000000..e51b80df --- /dev/null +++ b/dev-requirements.txt @@ -0,0 +1,13 @@ +# For packaging +build +pdoc3 +twine +wheel + +# For developing +pre-commit + +# extras +msal +requests_gssapi +requests_negotiate_sspi diff --git a/docs/assets/img/api_permissions.png b/docs/assets/img/api_permissions.png new file mode 100644 index 00000000..4967e241 Binary files /dev/null and b/docs/assets/img/api_permissions.png differ diff --git a/docs/assets/img/app_registration.png b/docs/assets/img/app_registration.png new file mode 100644 index 00000000..349e1d21 Binary files /dev/null and b/docs/assets/img/app_registration.png differ diff --git a/docs/assets/img/delegate_app_api_permissions.png b/docs/assets/img/delegate_app_api_permissions.png new file mode 100644 index 00000000..01b3b954 Binary files /dev/null and b/docs/assets/img/delegate_app_api_permissions.png differ diff --git a/docs/assets/img/delegate_app_permissions.png b/docs/assets/img/delegate_app_permissions.png new file mode 100644 index 00000000..b7f8fe95 Binary files /dev/null and b/docs/assets/img/delegate_app_permissions.png differ diff --git a/docs/assets/img/delegate_app_registration.png b/docs/assets/img/delegate_app_registration.png new file mode 100644 index 00000000..85ade323 Binary files /dev/null and b/docs/assets/img/delegate_app_registration.png differ diff --git a/docs/assets/img/permissions.png b/docs/assets/img/permissions.png new file mode 100644 index 00000000..6a609cbd Binary files /dev/null and b/docs/assets/img/permissions.png differ diff --git a/docs/exchangelib/account.html b/docs/exchangelib/account.html index 0484475e..63add70c 100644 --- a/docs/exchangelib/account.html +++ b/docs/exchangelib/account.html @@ -2,18 +2,32 @@ - - + + exchangelib.account API documentation - - - - - - + + + + + + - - + +
@@ -22,74 +36,39 @@

Module exchangelib.account

+
+
+
+
+
+
+
+
+

Classes

+
+
+class Account +(primary_smtp_address,
fullname=None,
access_type=None,
autodiscover=False,
credentials=None,
config=None,
locale=None,
default_timezone=None)
+
+
Expand source code -
from locale import getlocale
-from logging import getLogger
-
-from cached_property import threaded_cached_property
-
-from .autodiscover import discover
-from .configuration import Configuration
-from .credentials import DELEGATE, IMPERSONATION, ACCESS_TYPES
-from .errors import UnknownTimeZone
-from .ewsdatetime import EWSTimeZone, UTC
-from .fields import FieldPath
-from .folders import Folder, AdminAuditLogs, ArchiveDeletedItems, ArchiveInbox, ArchiveMsgFolderRoot, \
-    ArchiveRecoverableItemsDeletions, ArchiveRecoverableItemsPurges, ArchiveRecoverableItemsRoot, \
-    ArchiveRecoverableItemsVersions, ArchiveRoot, Calendar, Conflicts, Contacts, ConversationHistory, DeletedItems, \
-    Directory, Drafts, Favorites, IMContactList, Inbox, Journal, JunkEmail, LocalFailures, MsgFolderRoot, MyContacts, \
-    Notes, Outbox, PeopleConnect, PublicFoldersRoot, QuickContacts, RecipientCache, RecoverableItemsDeletions, \
-    RecoverableItemsPurges, RecoverableItemsRoot, RecoverableItemsVersions, Root, SearchFolders, SentItems, \
-    ServerFailures, SyncIssues, Tasks, ToDoSearch, VoiceMail
-from .items import HARD_DELETE, AUTO_RESOLVE, SEND_TO_NONE, SAVE_ONLY, ALL_OCCURRENCIES, ID_ONLY
-from .properties import Mailbox, SendingAs
-from .protocol import Protocol
-from .queryset import QuerySet
-from .services import ExportItems, UploadItems, GetItem, CreateItem, UpdateItem, DeleteItem, MoveItem, SendItem, \
-    CopyItem, GetUserOofSettings, SetUserOofSettings, GetMailTips, ArchiveItem, GetDelegate, MarkAsJunk, GetPersona
-from .util import get_domain, peek
-
-log = getLogger(__name__)
-
-
-class Identity:
-    """Contains information that uniquely identifies an account. Currently only used for SOAP impersonation headers."""
-
-    def __init__(self, primary_smtp_address=None, smtp_address=None, upn=None, sid=None):
-        """
-
-        :param primary_smtp_address: The primary email address associated with the account (Default value = None)
-        :param smtp_address: The (non-)primary email address associated with the account (Default value = None)
-        :param upn: (Default value = None)
-        :param sid: (Default value = None)
-        :return:
-        """
-        self.primary_smtp_address = primary_smtp_address
-        self.smtp_address = smtp_address
-        self.upn = upn
-        self.sid = sid
-
-    def __eq__(self, other):
-        for k in self.__dict__:
-            if getattr(self, k) != getattr(other, k):
-                return False
-        return True
-
-    def __hash__(self):
-        return hash(repr(self))
-
-    def __repr__(self):
-        return self.__class__.__name__ + repr((self.primary_smtp_address, self.smtp_address, self.upn, self.sid))
-
-
-class Account:
+
class Account:
     """Models an Exchange server user account."""
 
-    def __init__(self, primary_smtp_address, fullname=None, access_type=None, autodiscover=False, credentials=None,
-                 config=None, locale=None, default_timezone=None):
+    def __init__(
+        self,
+        primary_smtp_address,
+        fullname=None,
+        access_type=None,
+        autodiscover=False,
+        credentials=None,
+        config=None,
+        locale=None,
+        default_timezone=None,
+    ):
         """
 
         :param primary_smtp_address: The primary email address associated with the account on the Exchange server
@@ -106,65 +85,98 @@ 

Module exchangelib.account

assume values to be in the provided timezone. Defaults to the timezone of the host. :return: """ - if '@' not in primary_smtp_address: - raise ValueError("primary_smtp_address %r is not an email address" % primary_smtp_address) + if "@" not in primary_smtp_address: + raise ValueError(f"primary_smtp_address {primary_smtp_address!r} is not an email address") self.fullname = fullname # Assume delegate access if individual credentials are provided. Else, assume service user with impersonation self.access_type = access_type or (DELEGATE if credentials else IMPERSONATION) if self.access_type not in ACCESS_TYPES: - raise ValueError("'access_type' %r must be one of %s" % (self.access_type, ACCESS_TYPES)) + raise InvalidEnumValue("access_type", self.access_type, ACCESS_TYPES) try: - self.locale = locale or getlocale()[0] or None # get_locale() might not be able to determine the locale + # get_locale() might not be able to determine the locale + self.locale = locale or stdlib_locale.getlocale()[0] or None except ValueError as e: # getlocale() may throw ValueError if it fails to parse the system locale - log.warning('Failed to get locale (%s)', e) + log.warning("Failed to get locale (%s)", e) self.locale = None if not isinstance(self.locale, (type(None), str)): - raise ValueError("Expected 'locale' to be a string, got %r" % self.locale) + raise InvalidTypeError("locale", self.locale, str) if default_timezone: try: self.default_timezone = EWSTimeZone.from_timezone(default_timezone) except TypeError: - raise ValueError("Expected 'default_timezone' to be an EWSTimeZone, got %r" % default_timezone) + raise InvalidTypeError("default_timezone", default_timezone, EWSTimeZone) else: try: self.default_timezone = EWSTimeZone.localzone() except (ValueError, UnknownTimeZone) as e: # There is no translation from local timezone name to Windows timezone name, or e failed to find the # local timezone. - log.warning('%s. Fallback to UTC', e.args[0]) + log.warning("%s. Fallback to UTC", e.args[0]) self.default_timezone = UTC if not isinstance(config, (Configuration, type(None))): - raise ValueError("Expected 'config' to be a Configuration, got %r" % config) + raise InvalidTypeError("config", config, Configuration) if autodiscover: if config: - retry_policy, auth_type = config.retry_policy, config.auth_type + auth_type, retry_policy, version, max_connections = ( + config.auth_type, + config.retry_policy, + config.version, + config.max_connections, + ) if not credentials: credentials = config.credentials else: - retry_policy, auth_type = None, None - self.ad_response, self.protocol = discover( - email=primary_smtp_address, credentials=credentials, auth_type=auth_type, retry_policy=retry_policy - ) + auth_type, retry_policy, version, max_connections = None, None, None, None + self.ad_response, self.protocol = Autodiscovery( + email=primary_smtp_address, credentials=credentials + ).discover() + # Let's not use the auth_package hint from the AD response. It could be incorrect and we can just guess. + self.protocol.config.auth_type = auth_type + if retry_policy: + self.protocol.config.retry_policy = retry_policy + if version: + self.protocol.config.version = version + self.protocol.max_connections = max_connections primary_smtp_address = self.ad_response.autodiscover_smtp_address else: if not config: - raise AttributeError('non-autodiscover requires a config') + raise AttributeError("non-autodiscover requires a config") self.ad_response = None self.protocol = Protocol(config=config) # Other ways of identifying the account can be added later self.identity = Identity(primary_smtp_address=primary_smtp_address) - # We may need to override the default server version on a per-account basis because Microsoft may report one - # server version up-front but delegate account requests to an older backend server. - self.version = self.protocol.version - log.debug('Added account: %s', self) + # For maintaining affinity in e.g. subscriptions + self.affinity_cookie = None + + self._version = None + self._version_lock = Lock() + log.debug("Added account: %s", self) @property def primary_smtp_address(self): return self.identity.primary_smtp_address + @property + def version(self): + # We may need to override the default server version on a per-account basis because Microsoft may report one + # server version up-front but delegate account requests to an older backend server. Create a new instance to + # avoid changing the protocol version instance. + if self._version: + return self._version + with self._version_lock: + if self._version: + return self._version + self._version = self.protocol.version.copy() + return self._version + + @version.setter + def version(self, value): + with self._version_lock: + self._version = value + @threaded_cached_property def admin_audit_logs(self): return self.root.get_default_folder(AdminAuditLogs) @@ -367,7 +379,7 @@

Module exchangelib.account

# We accept generators, so it's not always convenient for caller to know up-front if 'ids' is empty. Allow # empty 'ids' and return early. return - kwargs['items'] = items + kwargs["items"] = items yield from service_cls(account=self, chunk_size=chunk_size).call(**kwargs) def export(self, items, chunk_size=None): @@ -378,9 +390,7 @@

Module exchangelib.account

:return: A list of strings, the exported representation of the object """ - return list( - self._consume_item_service(service_cls=ExportItems, items=items, chunk_size=chunk_size, kwargs={}) - ) + return list(self._consume_item_service(service_cls=ExportItems, items=items, chunk_size=chunk_size, kwargs={})) def upload(self, data, chunk_size=None): """Upload objects retrieved from an export to the given folders. @@ -402,12 +412,11 @@

Module exchangelib.account

-> [("idA", "changekey"), ("idB", "changekey"), ("idC", "changekey")] """ items = ((f, (None, False, d) if isinstance(d, str) else d) for f, d in data) - return list( - self._consume_item_service(service_cls=UploadItems, items=items, chunk_size=chunk_size, kwargs={}) - ) + return list(self._consume_item_service(service_cls=UploadItems, items=items, chunk_size=chunk_size, kwargs={})) - def bulk_create(self, folder, items, message_disposition=SAVE_ONLY, send_meeting_invitations=SEND_TO_NONE, - chunk_size=None): + def bulk_create( + self, folder, items, message_disposition=SAVE_ONLY, send_meeting_invitations=SEND_TO_NONE, chunk_size=None + ): """Create new items in 'folder'. :param folder: the folder to create the items in @@ -424,23 +433,36 @@

Module exchangelib.account

""" if isinstance(items, QuerySet): # bulk_create() on a queryset does not make sense because it returns items that have already been created - raise ValueError('Cannot bulk create items from a QuerySet') + raise ValueError("Cannot bulk create items from a QuerySet") log.debug( - 'Adding items for %s (folder %s, message_disposition: %s, send_meeting_invitations: %s)', + "Adding items for %s (folder %s, message_disposition: %s, send_meeting_invitations: %s)", self, folder, message_disposition, send_meeting_invitations, ) - return list(self._consume_item_service(service_cls=CreateItem, items=items, chunk_size=chunk_size, kwargs=dict( - folder=folder, - message_disposition=message_disposition, - send_meeting_invitations=send_meeting_invitations, - ))) - - def bulk_update(self, items, conflict_resolution=AUTO_RESOLVE, message_disposition=SAVE_ONLY, - send_meeting_invitations_or_cancellations=SEND_TO_NONE, suppress_read_receipts=True, - chunk_size=None): + return list( + self._consume_item_service( + service_cls=CreateItem, + items=items, + chunk_size=chunk_size, + kwargs=dict( + folder=folder, + message_disposition=message_disposition, + send_meeting_invitations=send_meeting_invitations, + ), + ) + ) + + def bulk_update( + self, + items, + conflict_resolution=AUTO_RESOLVE, + message_disposition=SAVE_ONLY, + send_meeting_invitations_or_cancellations=SEND_TO_NONE, + suppress_read_receipts=True, + chunk_size=None, + ): """Bulk update existing items. :param items: a list of (Item, fieldnames) tuples, where 'Item' is an Item object, and 'fieldnames' is a list @@ -460,23 +482,37 @@

Module exchangelib.account

# fact, it could be dangerous if the queryset contains an '.only()'. This would wipe out certain fields # entirely. if isinstance(items, QuerySet): - raise ValueError('Cannot bulk update on a queryset') + raise ValueError("Cannot bulk update on a queryset") log.debug( - 'Updating items for %s (conflict_resolution %s, message_disposition: %s, send_meeting_invitations: %s)', + "Updating items for %s (conflict_resolution %s, message_disposition: %s, send_meeting_invitations: %s)", self, conflict_resolution, message_disposition, send_meeting_invitations_or_cancellations, ) - return list(self._consume_item_service(service_cls=UpdateItem, items=items, chunk_size=chunk_size, kwargs=dict( - conflict_resolution=conflict_resolution, - message_disposition=message_disposition, - send_meeting_invitations_or_cancellations=send_meeting_invitations_or_cancellations, - suppress_read_receipts=suppress_read_receipts, - ))) - - def bulk_delete(self, ids, delete_type=HARD_DELETE, send_meeting_cancellations=SEND_TO_NONE, - affected_task_occurrences=ALL_OCCURRENCIES, suppress_read_receipts=True, chunk_size=None): + return list( + self._consume_item_service( + service_cls=UpdateItem, + items=items, + chunk_size=chunk_size, + kwargs=dict( + conflict_resolution=conflict_resolution, + message_disposition=message_disposition, + send_meeting_invitations_or_cancellations=send_meeting_invitations_or_cancellations, + suppress_read_receipts=suppress_read_receipts, + ), + ) + ) + + def bulk_delete( + self, + ids, + delete_type=HARD_DELETE, + send_meeting_cancellations=SEND_TO_NONE, + affected_task_occurrences=ALL_OCCURRENCES, + suppress_read_receipts=True, + chunk_size=None, + ): """Bulk delete items. :param ids: an iterable of either (id, changekey) tuples or Item objects. @@ -485,26 +521,31 @@

Module exchangelib.account

:param send_meeting_cancellations: only applicable to CalendarItem. Possible values are specified in SEND_MEETING_CANCELLATIONS_CHOICES. (Default value = SEND_TO_NONE) :param affected_task_occurrences: only applicable for recurring Task items. Possible values are specified in - AFFECTED_TASK_OCCURRENCES_CHOICES. (Default value = ALL_OCCURRENCIES) + AFFECTED_TASK_OCCURRENCES_CHOICES. (Default value = ALL_OCCURRENCES) :param suppress_read_receipts: only supported from Exchange 2013. True or False. (Default value = True) :param chunk_size: The number of items to send to the server in a single request (Default value = None) :return: a list of either True or exception instances, in the same order as the input """ log.debug( - 'Deleting items for %s (delete_type: %s, send_meeting_invitations: %s, affected_task_occurences: %s)', + "Deleting items for %s (delete_type: %s, send_meeting_invitations: %s, affected_task_occurrences: %s)", self, delete_type, send_meeting_cancellations, affected_task_occurrences, ) return list( - self._consume_item_service(service_cls=DeleteItem, items=ids, chunk_size=chunk_size, kwargs=dict( - delete_type=delete_type, - send_meeting_cancellations=send_meeting_cancellations, - affected_task_occurrences=affected_task_occurrences, - suppress_read_receipts=suppress_read_receipts, - )) + self._consume_item_service( + service_cls=DeleteItem, + items=ids, + chunk_size=chunk_size, + kwargs=dict( + delete_type=delete_type, + send_meeting_cancellations=send_meeting_cancellations, + affected_task_occurrences=affected_task_occurrences, + suppress_read_receipts=suppress_read_receipts, + ), + ) ) def bulk_send(self, ids, save_copy=True, copy_to_folder=None, chunk_size=None): @@ -522,9 +563,14 @@

Module exchangelib.account

if save_copy and not copy_to_folder: copy_to_folder = self.sent # 'Sent' is default EWS behaviour return list( - self._consume_item_service(service_cls=SendItem, items=ids, chunk_size=chunk_size, kwargs=dict( - saved_item_folder=copy_to_folder, - )) + self._consume_item_service( + service_cls=SendItem, + items=ids, + chunk_size=chunk_size, + kwargs=dict( + saved_item_folder=copy_to_folder, + ), + ) ) def bulk_copy(self, ids, to_folder, chunk_size=None): @@ -536,9 +582,16 @@

Module exchangelib.account

:return: Status for each send operation, in the same order as the input """ - return list(self._consume_item_service(service_cls=CopyItem, items=ids, chunk_size=chunk_size, kwargs=dict( - to_folder=to_folder, - ))) + return list( + self._consume_item_service( + service_cls=CopyItem, + items=ids, + chunk_size=chunk_size, + kwargs=dict( + to_folder=to_folder, + ), + ) + ) def bulk_move(self, ids, to_folder, chunk_size=None): """Move items to another folder. @@ -550,9 +603,16 @@

Module exchangelib.account

:return: The new IDs of the moved items, in the same order as the input. If 'to_folder' is a public folder or a folder in a different mailbox, an empty list is returned. """ - return list(self._consume_item_service(service_cls=MoveItem, items=ids, chunk_size=chunk_size, kwargs=dict( - to_folder=to_folder, - ))) + return list( + self._consume_item_service( + service_cls=MoveItem, + items=ids, + chunk_size=chunk_size, + kwargs=dict( + to_folder=to_folder, + ), + ) + ) def bulk_archive(self, ids, to_folder, chunk_size=None): """Archive items to a folder in the archive mailbox. An archive mailbox must be enabled in order for this @@ -564,9 +624,15 @@

Module exchangelib.account

:return: A list containing True or an exception instance in stable order of the requested items """ - return list(self._consume_item_service(service_cls=ArchiveItem, items=ids, chunk_size=chunk_size, kwargs=dict( - to_folder=to_folder, - )) + return list( + self._consume_item_service( + service_cls=ArchiveItem, + items=ids, + chunk_size=chunk_size, + kwargs=dict( + to_folder=to_folder, + ), + ) ) def bulk_mark_as_junk(self, ids, is_junk, move_item, chunk_size=None): @@ -580,10 +646,17 @@

Module exchangelib.account

:return: A list containing the new IDs of the moved items, if items were moved, or True, or an exception instance, in stable order of the requested items. """ - return list(self._consume_item_service(service_cls=MarkAsJunk, items=ids, chunk_size=chunk_size, kwargs=dict( - is_junk=is_junk, - move_item=move_item, - ))) + return list( + self._consume_item_service( + service_cls=MarkAsJunk, + items=ids, + chunk_size=chunk_size, + kwargs=dict( + is_junk=is_junk, + move_item=move_item, + ), + ) + ) def fetch(self, ids, folder=None, only_fields=None, chunk_size=None): """Fetch items by ID. @@ -608,13 +681,19 @@

Module exchangelib.account

for field in only_fields: validation_folder.validate_item_field(field=field, version=self.version) # Remove ItemId and ChangeKey. We get them unconditionally - additional_fields = {f for f in validation_folder.normalize_fields(fields=only_fields) - if not f.field.is_attribute} + additional_fields = { + f for f in validation_folder.normalize_fields(fields=only_fields) if not f.field.is_attribute + } # Always use IdOnly here, because AllProperties doesn't actually get *all* properties - yield from self._consume_item_service(service_cls=GetItem, items=ids, chunk_size=chunk_size, kwargs=dict( + yield from self._consume_item_service( + service_cls=GetItem, + items=ids, + chunk_size=chunk_size, + kwargs=dict( additional_fields=additional_fields, shape=ID_ONLY, - )) + ), + ) def fetch_personas(self, ids): """Fetch personas by ID. @@ -630,52 +709,143 @@

Module exchangelib.account

# We accept generators, so it's not always convenient for caller to know up-front if 'ids' is empty. Allow # empty 'ids' and return early. return - # GetPersona only accepts one persona ID per request. Crazy. - svc = GetPersona(account=self) - for i in ids: - yield svc.call(persona=i) + yield from GetPersona(account=self).call(personas=ids) @property def mail_tips(self): """See self.oof_settings about caching considerations.""" - # mail_tips_requested must be one of properties.MAIL_TIPS_TYPES return GetMailTips(protocol=self.protocol).get( sending_as=SendingAs(email_address=self.primary_smtp_address), recipients=[Mailbox(email_address=self.primary_smtp_address)], - mail_tips_requested='All', + mail_tips_requested="All", ) @property def delegates(self): """Return a list of DelegateUser objects representing the delegates that are set on this account.""" - delegates = [] - for d in GetDelegate(account=self).call(user_ids=None, include_permissions=True): - if isinstance(d, Exception): - raise d - delegates.append(d) - return delegates + return list(GetDelegate(account=self).call(user_ids=None, include_permissions=True)) + + @property + def rules(self): + """Return a list of Rule objects representing the rules that are set on this account.""" + return list(GetInboxRules(account=self).call()) + + def create_rule(self, rule: Rule): + """Create an Inbox rule. + + :param rule: The rule to create. Must have at least 'display_name' set. + :return: None if success, else raises an error. + """ + CreateInboxRule(account=self).get(rule=rule, remove_outlook_rule_blob=True) + # After creating the rule, query all rules, + # find the rule that was just created, and return its ID. + try: + rule.id = {i.display_name: i for i in GetInboxRules(account=self).call()}[rule.display_name].id + except KeyError: + raise ResponseMessageError(f"Failed to create rule ({rule.display_name})!") + + def set_rule(self, rule: Rule): + """Modify an Inbox rule. + + :param rule: The rule to set. Must have an ID. + :return: None if success, else raises an error. + """ + SetInboxRule(account=self).get(rule=rule) + + def delete_rule(self, rule: Rule): + """Delete an Inbox rule. + + :param rule: The rule to delete. Must have an ID. + :return: None if success, else raises an error. + """ + if not rule.id: + raise ValueError("Rule must have an ID") + DeleteInboxRule(account=self).get(rule=rule) + rule.id = None + + def subscribe_to_pull(self, event_types=None, watermark=None, timeout=60): + """Create a pull subscription. + + :param event_types: List of event types to subscribe to. Possible values defined in SubscribeToPull.EVENT_TYPES + :param watermark: An event bookmark as returned by some sync services + :param timeout: Timeout of the subscription, in minutes. Timeout is reset when the server receives a + GetEvents request for this subscription. + :return: The subscription ID and a watermark + """ + if event_types is None: + event_types = SubscribeToPull.EVENT_TYPES + return SubscribeToPull(account=self).get( + folders=None, + event_types=event_types, + watermark=watermark, + timeout=timeout, + ) + + def subscribe_to_push(self, callback_url, event_types=None, watermark=None, status_frequency=1): + """Create a push subscription. + + :param callback_url: A client-defined URL that the server will call + :param event_types: List of event types to subscribe to. Possible values defined in SubscribeToPush.EVENT_TYPES + :param watermark: An event bookmark as returned by some sync services + :param status_frequency: The frequency, in minutes, that the callback URL will be called with. + :return: The subscription ID and a watermark + """ + if event_types is None: + event_types = SubscribeToPush.EVENT_TYPES + return SubscribeToPush(account=self).get( + folders=None, + event_types=event_types, + watermark=watermark, + status_frequency=status_frequency, + url=callback_url, + ) + + def subscribe_to_streaming(self, event_types=None): + """Create a streaming subscription. + + :param event_types: List of event types to subscribe to. Possible values defined in SubscribeToPush.EVENT_TYPES + :return: The subscription ID + """ + if event_types is None: + event_types = SubscribeToStreaming.EVENT_TYPES + return SubscribeToStreaming(account=self).get(folders=None, event_types=event_types) + + def pull_subscription(self, **kwargs): + return PullSubscription(target=self, **kwargs) + + def push_subscription(self, **kwargs): + return PushSubscription(target=self, **kwargs) + + def streaming_subscription(self, **kwargs): + return StreamingSubscription(target=self, **kwargs) + + def unsubscribe(self, subscription_id): + """Unsubscribe. Only applies to pull and streaming notifications. + + :param subscription_id: A subscription ID as acquired by .subscribe_to_[pull|streaming]() + :return: True + + This method doesn't need the current collection instance, but it makes sense to keep the method along the other + sync methods. + """ + return Unsubscribe(account=self).get(subscription_id=subscription_id) + + def __getstate__(self): + # The lock cannot be pickled + state = self.__dict__.copy() + del state["_version_lock"] + return state + + def __setstate__(self, state): + # Restore the lock + self.__dict__.update(state) + self._version_lock = Lock() def __str__(self): - txt = '%s' % self.primary_smtp_address if self.fullname: - txt += ' (%s)' % self.fullname - return txt
+ return f"{self.primary_smtp_address} ({self.fullname})" + return self.primary_smtp_address
-
-
-
-
-
-
-
-
-

Classes

-
-
-class Account -(primary_smtp_address, fullname=None, access_type=None, autodiscover=False, credentials=None, config=None, locale=None, default_timezone=None) -
-

Models an Exchange server user account.

:param primary_smtp_address: The primary email address associated with the account on the Exchange server :param fullname: The full name of the account. Optional. (Default value = None) @@ -690,644 +860,86 @@

Classes

:param default_timezone: EWS may return some datetime values without timezone information. In this case, we will assume values to be in the provided timezone. Defaults to the timezone of the host. :return:

+

Instance variables

+
+
var admin_audit_logs
+
Expand source code -
class Account:
-    """Models an Exchange server user account."""
-
-    def __init__(self, primary_smtp_address, fullname=None, access_type=None, autodiscover=False, credentials=None,
-                 config=None, locale=None, default_timezone=None):
-        """
+
def __get__(self, obj, cls):
+    if obj is None:
+        return self
 
-        :param primary_smtp_address: The primary email address associated with the account on the Exchange server
-        :param fullname: The full name of the account. Optional. (Default value = None)
-        :param access_type: The access type granted to 'credentials' for this account. Valid options are 'delegate'
-            and 'impersonation'. 'delegate' is default if 'credentials' is set. Otherwise, 'impersonation' is default.
-        :param autodiscover: Whether to look up the EWS endpoint automatically using the autodiscover protocol.
-            (Default value = False)
-        :param credentials: A Credentials object containing valid credentials for this account. (Default value = None)
-        :param config: A Configuration object containing EWS endpoint information. Required if autodiscover is disabled
-            (Default value = None)
-        :param locale: The locale of the user, e.g. 'en_US'. Defaults to the locale of the host, if available.
-        :param default_timezone: EWS may return some datetime values without timezone information. In this case, we will
-            assume values to be in the provided timezone. Defaults to the timezone of the host.
-        :return:
-        """
-        if '@' not in primary_smtp_address:
-            raise ValueError("primary_smtp_address %r is not an email address" % primary_smtp_address)
-        self.fullname = fullname
-        # Assume delegate access if individual credentials are provided. Else, assume service user with impersonation
-        self.access_type = access_type or (DELEGATE if credentials else IMPERSONATION)
-        if self.access_type not in ACCESS_TYPES:
-            raise ValueError("'access_type' %r must be one of %s" % (self.access_type, ACCESS_TYPES))
+    obj_dict = obj.__dict__
+    name = self.func.__name__
+    with self.lock:
         try:
-            self.locale = locale or getlocale()[0] or None  # get_locale() might not be able to determine the locale
-        except ValueError as e:
-            # getlocale() may throw ValueError if it fails to parse the system locale
-            log.warning('Failed to get locale (%s)', e)
-            self.locale = None
-        if not isinstance(self.locale, (type(None), str)):
-            raise ValueError("Expected 'locale' to be a string, got %r" % self.locale)
-        if default_timezone:
-            try:
-                self.default_timezone = EWSTimeZone.from_timezone(default_timezone)
-            except TypeError:
-                raise ValueError("Expected 'default_timezone' to be an EWSTimeZone, got %r" % default_timezone)
-        else:
-            try:
-                self.default_timezone = EWSTimeZone.localzone()
-            except (ValueError, UnknownTimeZone) as e:
-                # There is no translation from local timezone name to Windows timezone name, or e failed to find the
-                # local timezone.
-                log.warning('%s. Fallback to UTC', e.args[0])
-                self.default_timezone = UTC
-        if not isinstance(config, (Configuration, type(None))):
-            raise ValueError("Expected 'config' to be a Configuration, got %r" % config)
-        if autodiscover:
-            if config:
-                retry_policy, auth_type = config.retry_policy, config.auth_type
-                if not credentials:
-                    credentials = config.credentials
-            else:
-                retry_policy, auth_type = None, None
-            self.ad_response, self.protocol = discover(
-                email=primary_smtp_address, credentials=credentials, auth_type=auth_type, retry_policy=retry_policy
-            )
-            primary_smtp_address = self.ad_response.autodiscover_smtp_address
-        else:
-            if not config:
-                raise AttributeError('non-autodiscover requires a config')
-            self.ad_response = None
-            self.protocol = Protocol(config=config)
-
-        # Other ways of identifying the account can be added later
-        self.identity = Identity(primary_smtp_address=primary_smtp_address)
+            # check if the value was computed before the lock was acquired
+            return obj_dict[name]
 
-        # We may need to override the default server version on a per-account basis because Microsoft may report one
-        # server version up-front but delegate account requests to an older backend server.
-        self.version = self.protocol.version
-        log.debug('Added account: %s', self)
+        except KeyError:
+            # if not, do the calculation and release the lock
+            return obj_dict.setdefault(name, self.func(obj))
+
+
+
+
var archive_deleted_items
+
+
+ +Expand source code + +
def __get__(self, obj, cls):
+    if obj is None:
+        return self
 
-    @property
-    def primary_smtp_address(self):
-        return self.identity.primary_smtp_address
+    obj_dict = obj.__dict__
+    name = self.func.__name__
+    with self.lock:
+        try:
+            # check if the value was computed before the lock was acquired
+            return obj_dict[name]
 
-    @threaded_cached_property
-    def admin_audit_logs(self):
-        return self.root.get_default_folder(AdminAuditLogs)
+        except KeyError:
+            # if not, do the calculation and release the lock
+            return obj_dict.setdefault(name, self.func(obj))
+
+
+
+
var archive_inbox
+
+
+ +Expand source code + +
def __get__(self, obj, cls):
+    if obj is None:
+        return self
 
-    @threaded_cached_property
-    def archive_deleted_items(self):
-        return self.archive_root.get_default_folder(ArchiveDeletedItems)
+    obj_dict = obj.__dict__
+    name = self.func.__name__
+    with self.lock:
+        try:
+            # check if the value was computed before the lock was acquired
+            return obj_dict[name]
 
-    @threaded_cached_property
-    def archive_inbox(self):
-        return self.archive_root.get_default_folder(ArchiveInbox)
-
-    @threaded_cached_property
-    def archive_msg_folder_root(self):
-        return self.archive_root.get_default_folder(ArchiveMsgFolderRoot)
-
-    @threaded_cached_property
-    def archive_recoverable_items_deletions(self):
-        return self.archive_root.get_default_folder(ArchiveRecoverableItemsDeletions)
-
-    @threaded_cached_property
-    def archive_recoverable_items_purges(self):
-        return self.archive_root.get_default_folder(ArchiveRecoverableItemsPurges)
-
-    @threaded_cached_property
-    def archive_recoverable_items_root(self):
-        return self.archive_root.get_default_folder(ArchiveRecoverableItemsRoot)
-
-    @threaded_cached_property
-    def archive_recoverable_items_versions(self):
-        return self.archive_root.get_default_folder(ArchiveRecoverableItemsVersions)
-
-    @threaded_cached_property
-    def archive_root(self):
-        return ArchiveRoot.get_distinguished(account=self)
-
-    @threaded_cached_property
-    def calendar(self):
-        # If the account contains a shared calendar from a different user, that calendar will be in the folder list.
-        # Attempt not to return one of those. An account may not always have a calendar called "Calendar", but a
-        # Calendar folder with a localized name instead. Return that, if it's available, but always prefer any
-        # distinguished folder returned by the server.
-        return self.root.get_default_folder(Calendar)
-
-    @threaded_cached_property
-    def conflicts(self):
-        return self.root.get_default_folder(Conflicts)
-
-    @threaded_cached_property
-    def contacts(self):
-        return self.root.get_default_folder(Contacts)
-
-    @threaded_cached_property
-    def conversation_history(self):
-        return self.root.get_default_folder(ConversationHistory)
-
-    @threaded_cached_property
-    def directory(self):
-        return self.root.get_default_folder(Directory)
-
-    @threaded_cached_property
-    def drafts(self):
-        return self.root.get_default_folder(Drafts)
-
-    @threaded_cached_property
-    def favorites(self):
-        return self.root.get_default_folder(Favorites)
-
-    @threaded_cached_property
-    def im_contact_list(self):
-        return self.root.get_default_folder(IMContactList)
-
-    @threaded_cached_property
-    def inbox(self):
-        return self.root.get_default_folder(Inbox)
-
-    @threaded_cached_property
-    def journal(self):
-        return self.root.get_default_folder(Journal)
-
-    @threaded_cached_property
-    def junk(self):
-        return self.root.get_default_folder(JunkEmail)
-
-    @threaded_cached_property
-    def local_failures(self):
-        return self.root.get_default_folder(LocalFailures)
-
-    @threaded_cached_property
-    def msg_folder_root(self):
-        return self.root.get_default_folder(MsgFolderRoot)
-
-    @threaded_cached_property
-    def my_contacts(self):
-        return self.root.get_default_folder(MyContacts)
-
-    @threaded_cached_property
-    def notes(self):
-        return self.root.get_default_folder(Notes)
-
-    @threaded_cached_property
-    def outbox(self):
-        return self.root.get_default_folder(Outbox)
-
-    @threaded_cached_property
-    def people_connect(self):
-        return self.root.get_default_folder(PeopleConnect)
-
-    @threaded_cached_property
-    def public_folders_root(self):
-        return PublicFoldersRoot.get_distinguished(account=self)
-
-    @threaded_cached_property
-    def quick_contacts(self):
-        return self.root.get_default_folder(QuickContacts)
-
-    @threaded_cached_property
-    def recipient_cache(self):
-        return self.root.get_default_folder(RecipientCache)
-
-    @threaded_cached_property
-    def recoverable_items_deletions(self):
-        return self.root.get_default_folder(RecoverableItemsDeletions)
-
-    @threaded_cached_property
-    def recoverable_items_purges(self):
-        return self.root.get_default_folder(RecoverableItemsPurges)
-
-    @threaded_cached_property
-    def recoverable_items_root(self):
-        return self.root.get_default_folder(RecoverableItemsRoot)
-
-    @threaded_cached_property
-    def recoverable_items_versions(self):
-        return self.root.get_default_folder(RecoverableItemsVersions)
-
-    @threaded_cached_property
-    def root(self):
-        return Root.get_distinguished(account=self)
-
-    @threaded_cached_property
-    def search_folders(self):
-        return self.root.get_default_folder(SearchFolders)
-
-    @threaded_cached_property
-    def sent(self):
-        return self.root.get_default_folder(SentItems)
-
-    @threaded_cached_property
-    def server_failures(self):
-        return self.root.get_default_folder(ServerFailures)
-
-    @threaded_cached_property
-    def sync_issues(self):
-        return self.root.get_default_folder(SyncIssues)
-
-    @threaded_cached_property
-    def tasks(self):
-        return self.root.get_default_folder(Tasks)
-
-    @threaded_cached_property
-    def todo_search(self):
-        return self.root.get_default_folder(ToDoSearch)
-
-    @threaded_cached_property
-    def trash(self):
-        return self.root.get_default_folder(DeletedItems)
-
-    @threaded_cached_property
-    def voice_mail(self):
-        return self.root.get_default_folder(VoiceMail)
-
-    @property
-    def domain(self):
-        return get_domain(self.primary_smtp_address)
-
-    @property
-    def oof_settings(self):
-        # We don't want to cache this property because then we can't easily get updates. 'threaded_cached_property'
-        # supports the 'del self.oof_settings' syntax to invalidate the cache, but does not support custom setter
-        # methods. Having a non-cached service call here goes against the assumption that properties are cheap, but the
-        # alternative is to create get_oof_settings() and set_oof_settings(), and that's just too Java-ish for my taste.
-        return GetUserOofSettings(account=self).get(
-            mailbox=Mailbox(email_address=self.primary_smtp_address),
-        )
-
-    @oof_settings.setter
-    def oof_settings(self, value):
-        SetUserOofSettings(account=self).get(
-            oof_settings=value,
-            mailbox=Mailbox(email_address=self.primary_smtp_address),
-        )
-
-    def _consume_item_service(self, service_cls, items, chunk_size, kwargs):
-        if isinstance(items, QuerySet):
-            # We just want an iterator over the results
-            items = iter(items)
-        is_empty, items = peek(items)
-        if is_empty:
-            # We accept generators, so it's not always convenient for caller to know up-front if 'ids' is empty. Allow
-            # empty 'ids' and return early.
-            return
-        kwargs['items'] = items
-        yield from service_cls(account=self, chunk_size=chunk_size).call(**kwargs)
-
-    def export(self, items, chunk_size=None):
-        """Return export strings of the given items.
-
-        :param items: An iterable containing the Items we want to export
-        :param chunk_size: The number of items to send to the server in a single request (Default value = None)
-
-        :return: A list of strings, the exported representation of the object
-        """
-        return list(
-            self._consume_item_service(service_cls=ExportItems, items=items, chunk_size=chunk_size, kwargs={})
-        )
-
-    def upload(self, data, chunk_size=None):
-        """Upload objects retrieved from an export to the given folders.
-
-        :param data: An iterable of tuples containing the folder we want to upload the data to and the string outputs of
-            exports. If you want to update items instead of create, the data must be a tuple of
-            (ItemId, is_associated, data) values.
-        :param chunk_size: The number of items to send to the server in a single request (Default value = None)
-
-        :return: A list of tuples with the new ids and changekeys
-
-          Example:
-          account.upload([
-              (account.inbox, "AABBCC..."),
-              (account.inbox, (ItemId('AA', 'BB'), False, "XXYYZZ...")),
-              (account.inbox, (('CC', 'DD'), None, "XXYYZZ...")),
-              (account.calendar, "ABCXYZ..."),
-          ])
-          -> [("idA", "changekey"), ("idB", "changekey"), ("idC", "changekey")]
-        """
-        items = ((f, (None, False, d) if isinstance(d, str) else d) for f, d in data)
-        return list(
-            self._consume_item_service(service_cls=UploadItems, items=items, chunk_size=chunk_size, kwargs={})
-        )
-
-    def bulk_create(self, folder, items, message_disposition=SAVE_ONLY, send_meeting_invitations=SEND_TO_NONE,
-                    chunk_size=None):
-        """Create new items in 'folder'.
-
-        :param folder: the folder to create the items in
-        :param items: an iterable of Item objects
-        :param message_disposition: only applicable to Message items. Possible values are specified in
-            MESSAGE_DISPOSITION_CHOICES (Default value = SAVE_ONLY)
-        :param send_meeting_invitations: only applicable to CalendarItem items. Possible values are specified in
-            SEND_MEETING_INVITATIONS_CHOICES (Default value = SEND_TO_NONE)
-        :param chunk_size: The number of items to send to the server in a single request (Default value = None)
-
-        :return: a list of either BulkCreateResult or exception instances in the same order as the input. The returned
-          BulkCreateResult objects are normal Item objects except they only contain the 'id' and 'changekey'
-          of the created item, and the 'id' of any attachments that were also created.
-        """
-        if isinstance(items, QuerySet):
-            # bulk_create() on a queryset does not make sense because it returns items that have already been created
-            raise ValueError('Cannot bulk create items from a QuerySet')
-        log.debug(
-            'Adding items for %s (folder %s, message_disposition: %s, send_meeting_invitations: %s)',
-            self,
-            folder,
-            message_disposition,
-            send_meeting_invitations,
-        )
-        return list(self._consume_item_service(service_cls=CreateItem, items=items, chunk_size=chunk_size, kwargs=dict(
-            folder=folder,
-            message_disposition=message_disposition,
-            send_meeting_invitations=send_meeting_invitations,
-        )))
-
-    def bulk_update(self, items, conflict_resolution=AUTO_RESOLVE, message_disposition=SAVE_ONLY,
-                    send_meeting_invitations_or_cancellations=SEND_TO_NONE, suppress_read_receipts=True,
-                    chunk_size=None):
-        """Bulk update existing items.
-
-        :param items: a list of (Item, fieldnames) tuples, where 'Item' is an Item object, and 'fieldnames' is a list
-            containing the attributes on this Item object that we want to be updated.
-        :param conflict_resolution: Possible values are specified in CONFLICT_RESOLUTION_CHOICES
-            (Default value = AUTO_RESOLVE)
-        :param message_disposition: only applicable to Message items. Possible values are specified in
-            MESSAGE_DISPOSITION_CHOICES (Default value = SAVE_ONLY)
-        :param send_meeting_invitations_or_cancellations: only applicable to CalendarItem items. Possible values are
-            specified in SEND_MEETING_INVITATIONS_AND_CANCELLATIONS_CHOICES (Default value = SEND_TO_NONE)
-        :param suppress_read_receipts: nly supported from Exchange 2013. True or False (Default value = True)
-        :param chunk_size: The number of items to send to the server in a single request (Default value = None)
-
-        :return: a list of either (id, changekey) tuples or exception instances, in the same order as the input
-        """
-        # bulk_update() on a queryset does not make sense because there would be no opportunity to alter the items. In
-        # fact, it could be dangerous if the queryset contains an '.only()'. This would wipe out certain fields
-        # entirely.
-        if isinstance(items, QuerySet):
-            raise ValueError('Cannot bulk update on a queryset')
-        log.debug(
-            'Updating items for %s (conflict_resolution %s, message_disposition: %s, send_meeting_invitations: %s)',
-            self,
-            conflict_resolution,
-            message_disposition,
-            send_meeting_invitations_or_cancellations,
-        )
-        return list(self._consume_item_service(service_cls=UpdateItem, items=items, chunk_size=chunk_size, kwargs=dict(
-            conflict_resolution=conflict_resolution,
-            message_disposition=message_disposition,
-            send_meeting_invitations_or_cancellations=send_meeting_invitations_or_cancellations,
-            suppress_read_receipts=suppress_read_receipts,
-        )))
-
-    def bulk_delete(self, ids, delete_type=HARD_DELETE, send_meeting_cancellations=SEND_TO_NONE,
-                    affected_task_occurrences=ALL_OCCURRENCIES, suppress_read_receipts=True, chunk_size=None):
-        """Bulk delete items.
-
-        :param ids: an iterable of either (id, changekey) tuples or Item objects.
-        :param delete_type: the type of delete to perform. Possible values are specified in DELETE_TYPE_CHOICES
-            (Default value = HARD_DELETE)
-        :param send_meeting_cancellations: only applicable to CalendarItem. Possible values are specified in
-            SEND_MEETING_CANCELLATIONS_CHOICES. (Default value = SEND_TO_NONE)
-        :param affected_task_occurrences: only applicable for recurring Task items. Possible values are specified in
-            AFFECTED_TASK_OCCURRENCES_CHOICES. (Default value = ALL_OCCURRENCIES)
-        :param suppress_read_receipts: only supported from Exchange 2013. True or False. (Default value = True)
-        :param chunk_size: The number of items to send to the server in a single request (Default value = None)
-
-        :return: a list of either True or exception instances, in the same order as the input
-        """
-        log.debug(
-            'Deleting items for %s (delete_type: %s, send_meeting_invitations: %s, affected_task_occurences: %s)',
-            self,
-            delete_type,
-            send_meeting_cancellations,
-            affected_task_occurrences,
-        )
-        return list(
-            self._consume_item_service(service_cls=DeleteItem, items=ids, chunk_size=chunk_size, kwargs=dict(
-                delete_type=delete_type,
-                send_meeting_cancellations=send_meeting_cancellations,
-                affected_task_occurrences=affected_task_occurrences,
-                suppress_read_receipts=suppress_read_receipts,
-            ))
-        )
-
-    def bulk_send(self, ids, save_copy=True, copy_to_folder=None, chunk_size=None):
-        """Send existing draft messages. If requested, save a copy in 'copy_to_folder'.
-
-        :param ids: an iterable of either (id, changekey) tuples or Item objects.
-        :param save_copy: If true, saves a copy of the message (Default value = True)
-        :param copy_to_folder: If requested, save a copy of the message in this folder. Default is the Sent folder
-        :param chunk_size: The number of items to send to the server in a single request (Default value = None)
-
-        :return: Status for each send operation, in the same order as the input
-        """
-        if copy_to_folder and not save_copy:
-            raise AttributeError("'save_copy' must be True when 'copy_to_folder' is set")
-        if save_copy and not copy_to_folder:
-            copy_to_folder = self.sent  # 'Sent' is default EWS behaviour
-        return list(
-            self._consume_item_service(service_cls=SendItem, items=ids, chunk_size=chunk_size, kwargs=dict(
-                saved_item_folder=copy_to_folder,
-            ))
-        )
-
-    def bulk_copy(self, ids, to_folder, chunk_size=None):
-        """Copy items to another folder.
-
-        :param ids: an iterable of either (id, changekey) tuples or Item objects.
-        :param to_folder: The destination folder of the copy operation
-        :param chunk_size: The number of items to send to the server in a single request (Default value = None)
-
-        :return: Status for each send operation, in the same order as the input
-        """
-        return list(self._consume_item_service(service_cls=CopyItem, items=ids, chunk_size=chunk_size, kwargs=dict(
-            to_folder=to_folder,
-        )))
-
-    def bulk_move(self, ids, to_folder, chunk_size=None):
-        """Move items to another folder.
-
-        :param ids: an iterable of either (id, changekey) tuples or Item objects.
-        :param to_folder: The destination folder of the copy operation
-        :param chunk_size: The number of items to send to the server in a single request (Default value = None)
-
-        :return: The new IDs of the moved items, in the same order as the input. If 'to_folder' is a public folder or a
-          folder in a different mailbox, an empty list is returned.
-        """
-        return list(self._consume_item_service(service_cls=MoveItem, items=ids, chunk_size=chunk_size, kwargs=dict(
-            to_folder=to_folder,
-        )))
-
-    def bulk_archive(self, ids, to_folder, chunk_size=None):
-        """Archive items to a folder in the archive mailbox. An archive mailbox must be enabled in order for this
-        to work.
-
-        :param ids: an iterable of either (id, changekey) tuples or Item objects.
-        :param to_folder: The destination folder of the archive operation
-        :param chunk_size: The number of items to send to the server in a single request (Default value = None)
-
-        :return: A list containing True or an exception instance in stable order of the requested items
-        """
-        return list(self._consume_item_service(service_cls=ArchiveItem, items=ids, chunk_size=chunk_size, kwargs=dict(
-                to_folder=to_folder,
-            ))
-        )
-
-    def bulk_mark_as_junk(self, ids, is_junk, move_item, chunk_size=None):
-        """Mark or un-mark message items as junk email and add or remove the sender from the blocked sender list.
-
-        :param ids: an iterable of either (id, changekey) tuples or Item objects.
-        :param is_junk: Whether the messages are junk or not
-        :param move_item: Whether to move the messages to the junk folder or not
-        :param chunk_size: The number of items to send to the server in a single request (Default value = None)
-
-        :return: A list containing the new IDs of the moved items, if items were moved, or True, or an exception
-          instance, in stable order of the requested items.
-        """
-        return list(self._consume_item_service(service_cls=MarkAsJunk, items=ids, chunk_size=chunk_size, kwargs=dict(
-            is_junk=is_junk,
-            move_item=move_item,
-        )))
-
-    def fetch(self, ids, folder=None, only_fields=None, chunk_size=None):
-        """Fetch items by ID.
-
-        :param ids: an iterable of either (id, changekey) tuples or Item objects.
-        :param folder: used for validating 'only_fields' (Default value = None)
-        :param only_fields: A list of string or FieldPath items specifying the fields to fetch. Default to all fields
-        :param chunk_size: The number of items to send to the server in a single request (Default value = None)
-
-        :return: A generator of Item objects, in the same order as the input
-        """
-        validation_folder = folder or Folder(root=self.root)  # Default to a folder type that supports all item types
-        # 'ids' could be an unevaluated QuerySet, e.g. if we ended up here via `fetch(ids=some_folder.filter(...))`. In
-        # that case, we want to use its iterator. Otherwise, peek() will start a count() which is wasteful because we
-        # need the item IDs immediately afterwards. iterator() will only do the bare minimum.
-        if only_fields is None:
-            # We didn't restrict list of field paths. Get all fields from the server, including extended properties.
-            additional_fields = {
-                FieldPath(field=f) for f in validation_folder.allowed_item_fields(version=self.version)
-            }
-        else:
-            for field in only_fields:
-                validation_folder.validate_item_field(field=field, version=self.version)
-            # Remove ItemId and ChangeKey. We get them unconditionally
-            additional_fields = {f for f in validation_folder.normalize_fields(fields=only_fields)
-                                 if not f.field.is_attribute}
-        # Always use IdOnly here, because AllProperties doesn't actually get *all* properties
-        yield from self._consume_item_service(service_cls=GetItem, items=ids, chunk_size=chunk_size, kwargs=dict(
-                additional_fields=additional_fields,
-                shape=ID_ONLY,
-        ))
-
-    def fetch_personas(self, ids):
-        """Fetch personas by ID.
-
-        :param ids: an iterable of either (id, changekey) tuples or Persona objects.
-        :return: A generator of Persona objects, in the same order as the input
-        """
-        if isinstance(ids, QuerySet):
-            # We just want an iterator over the results
-            ids = iter(ids)
-        is_empty, ids = peek(ids)
-        if is_empty:
-            # We accept generators, so it's not always convenient for caller to know up-front if 'ids' is empty. Allow
-            # empty 'ids' and return early.
-            return
-        # GetPersona only accepts one persona ID per request. Crazy.
-        svc = GetPersona(account=self)
-        for i in ids:
-            yield svc.call(persona=i)
-
-    @property
-    def mail_tips(self):
-        """See self.oof_settings about caching considerations."""
-        # mail_tips_requested must be one of properties.MAIL_TIPS_TYPES
-        return GetMailTips(protocol=self.protocol).get(
-            sending_as=SendingAs(email_address=self.primary_smtp_address),
-            recipients=[Mailbox(email_address=self.primary_smtp_address)],
-            mail_tips_requested='All',
-        )
-
-    @property
-    def delegates(self):
-        """Return a list of DelegateUser objects representing the delegates that are set on this account."""
-        delegates = []
-        for d in GetDelegate(account=self).call(user_ids=None, include_permissions=True):
-            if isinstance(d, Exception):
-                raise d
-            delegates.append(d)
-        return delegates
-
-    def __str__(self):
-        txt = '%s' % self.primary_smtp_address
-        if self.fullname:
-            txt += ' (%s)' % self.fullname
-        return txt
-
-

Instance variables

-
-
var admin_audit_logs
-
-
-
- -Expand source code - -
def __get__(self, obj, cls):
-    if obj is None:
-        return self
-
-    obj_dict = obj.__dict__
-    name = self.func.__name__
-    with self.lock:
-        try:
-            # check if the value was computed before the lock was acquired
-            return obj_dict[name]
-
-        except KeyError:
-            # if not, do the calculation and release the lock
-            return obj_dict.setdefault(name, self.func(obj))
-
-
-
var archive_deleted_items
-
-
-
- -Expand source code - -
def __get__(self, obj, cls):
-    if obj is None:
-        return self
-
-    obj_dict = obj.__dict__
-    name = self.func.__name__
-    with self.lock:
-        try:
-            # check if the value was computed before the lock was acquired
-            return obj_dict[name]
-
-        except KeyError:
-            # if not, do the calculation and release the lock
-            return obj_dict.setdefault(name, self.func(obj))
-
-
-
var archive_inbox
-
-
-
- -Expand source code - -
def __get__(self, obj, cls):
-    if obj is None:
-        return self
+        except KeyError:
+            # if not, do the calculation and release the lock
+            return obj_dict.setdefault(name, self.func(obj))
+
+
+
+
var archive_msg_folder_root
+
+
+ +Expand source code + +
def __get__(self, obj, cls):
+    if obj is None:
+        return self
 
     obj_dict = obj.__dict__
     name = self.func.__name__
@@ -1340,33 +952,10 @@ 

Instance variables

# if not, do the calculation and release the lock return obj_dict.setdefault(name, self.func(obj))
-
-
var archive_msg_folder_root
-
-
- -Expand source code - -
def __get__(self, obj, cls):
-    if obj is None:
-        return self
-
-    obj_dict = obj.__dict__
-    name = self.func.__name__
-    with self.lock:
-        try:
-            # check if the value was computed before the lock was acquired
-            return obj_dict[name]
-
-        except KeyError:
-            # if not, do the calculation and release the lock
-            return obj_dict.setdefault(name, self.func(obj))
-
var archive_recoverable_items_deletions
-
Expand source code @@ -1386,10 +975,10 @@

Instance variables

# if not, do the calculation and release the lock return obj_dict.setdefault(name, self.func(obj))
+
var archive_recoverable_items_purges
-
Expand source code @@ -1409,10 +998,10 @@

Instance variables

# if not, do the calculation and release the lock return obj_dict.setdefault(name, self.func(obj))
+
var archive_recoverable_items_root
-
Expand source code @@ -1432,10 +1021,10 @@

Instance variables

# if not, do the calculation and release the lock return obj_dict.setdefault(name, self.func(obj))
+
var archive_recoverable_items_versions
-
Expand source code @@ -1455,10 +1044,10 @@

Instance variables

# if not, do the calculation and release the lock return obj_dict.setdefault(name, self.func(obj))
+
var archive_root
-
Expand source code @@ -1478,10 +1067,10 @@

Instance variables

# if not, do the calculation and release the lock return obj_dict.setdefault(name, self.func(obj))
+
var calendar
-
Expand source code @@ -1501,10 +1090,10 @@

Instance variables

# if not, do the calculation and release the lock return obj_dict.setdefault(name, self.func(obj))
+
var conflicts
-
Expand source code @@ -1524,10 +1113,10 @@

Instance variables

# if not, do the calculation and release the lock return obj_dict.setdefault(name, self.func(obj))
+
var contacts
-
Expand source code @@ -1547,10 +1136,10 @@

Instance variables

# if not, do the calculation and release the lock return obj_dict.setdefault(name, self.func(obj))
+
var conversation_history
-
Expand source code @@ -1570,10 +1159,10 @@

Instance variables

# if not, do the calculation and release the lock return obj_dict.setdefault(name, self.func(obj))
+
-
var delegates
+
prop delegates
-

Return a list of DelegateUser objects representing the delegates that are set on this account.

Expand source code @@ -1581,17 +1170,12 @@

Instance variables

@property
 def delegates(self):
     """Return a list of DelegateUser objects representing the delegates that are set on this account."""
-    delegates = []
-    for d in GetDelegate(account=self).call(user_ids=None, include_permissions=True):
-        if isinstance(d, Exception):
-            raise d
-        delegates.append(d)
-    return delegates
+ return list(GetDelegate(account=self).call(user_ids=None, include_permissions=True))
+

Return a list of DelegateUser objects representing the delegates that are set on this account.

var directory
-
Expand source code @@ -1611,10 +1195,10 @@

Instance variables

# if not, do the calculation and release the lock return obj_dict.setdefault(name, self.func(obj))
+
-
var domain
+
prop domain
-
Expand source code @@ -1623,10 +1207,10 @@

Instance variables

def domain(self): return get_domain(self.primary_smtp_address)
+
var drafts
-
Expand source code @@ -1646,10 +1230,10 @@

Instance variables

# if not, do the calculation and release the lock return obj_dict.setdefault(name, self.func(obj))
+
var favorites
-
Expand source code @@ -1669,10 +1253,10 @@

Instance variables

# if not, do the calculation and release the lock return obj_dict.setdefault(name, self.func(obj))
+
var im_contact_list
-
Expand source code @@ -1692,10 +1276,10 @@

Instance variables

# if not, do the calculation and release the lock return obj_dict.setdefault(name, self.func(obj))
+
var inbox
-
Expand source code @@ -1715,10 +1299,10 @@

Instance variables

# if not, do the calculation and release the lock return obj_dict.setdefault(name, self.func(obj))
+
var journal
-
Expand source code @@ -1738,10 +1322,10 @@

Instance variables

# if not, do the calculation and release the lock return obj_dict.setdefault(name, self.func(obj))
+
var junk
-
Expand source code @@ -1761,10 +1345,10 @@

Instance variables

# if not, do the calculation and release the lock return obj_dict.setdefault(name, self.func(obj))
+
var local_failures
-
Expand source code @@ -1784,10 +1368,10 @@

Instance variables

# if not, do the calculation and release the lock return obj_dict.setdefault(name, self.func(obj))
+
-
var mail_tips
+
prop mail_tips
-

See self.oof_settings about caching considerations.

Expand source code @@ -1795,17 +1379,16 @@

Instance variables

@property
 def mail_tips(self):
     """See self.oof_settings about caching considerations."""
-    # mail_tips_requested must be one of properties.MAIL_TIPS_TYPES
     return GetMailTips(protocol=self.protocol).get(
         sending_as=SendingAs(email_address=self.primary_smtp_address),
         recipients=[Mailbox(email_address=self.primary_smtp_address)],
-        mail_tips_requested='All',
+        mail_tips_requested="All",
     )
+

See self.oof_settings about caching considerations.

var msg_folder_root
-
Expand source code @@ -1825,10 +1408,10 @@

Instance variables

# if not, do the calculation and release the lock return obj_dict.setdefault(name, self.func(obj))
+
var my_contacts
-
Expand source code @@ -1848,10 +1431,10 @@

Instance variables

# if not, do the calculation and release the lock return obj_dict.setdefault(name, self.func(obj))
+
var notes
-
Expand source code @@ -1871,10 +1454,10 @@

Instance variables

# if not, do the calculation and release the lock return obj_dict.setdefault(name, self.func(obj))
+
-
var oof_settings
+
prop oof_settings
-
Expand source code @@ -1889,10 +1472,10 @@

Instance variables

mailbox=Mailbox(email_address=self.primary_smtp_address), )
+
var outbox
-
Expand source code @@ -1912,10 +1495,10 @@

Instance variables

# if not, do the calculation and release the lock return obj_dict.setdefault(name, self.func(obj))
+
var people_connect
-
Expand source code @@ -1935,10 +1518,10 @@

Instance variables

# if not, do the calculation and release the lock return obj_dict.setdefault(name, self.func(obj))
+
-
var primary_smtp_address
+
prop primary_smtp_address
-
Expand source code @@ -1947,10 +1530,10 @@

Instance variables

def primary_smtp_address(self): return self.identity.primary_smtp_address
+
var public_folders_root
-
Expand source code @@ -1970,10 +1553,10 @@

Instance variables

# if not, do the calculation and release the lock return obj_dict.setdefault(name, self.func(obj))
+
var quick_contacts
-
Expand source code @@ -1993,10 +1576,10 @@

Instance variables

# if not, do the calculation and release the lock return obj_dict.setdefault(name, self.func(obj))
+
var recipient_cache
-
Expand source code @@ -2016,10 +1599,10 @@

Instance variables

# if not, do the calculation and release the lock return obj_dict.setdefault(name, self.func(obj))
+
var recoverable_items_deletions
-
Expand source code @@ -2039,10 +1622,10 @@

Instance variables

# if not, do the calculation and release the lock return obj_dict.setdefault(name, self.func(obj))
+
var recoverable_items_purges
-
Expand source code @@ -2062,10 +1645,10 @@

Instance variables

# if not, do the calculation and release the lock return obj_dict.setdefault(name, self.func(obj))
+
var recoverable_items_root
-
Expand source code @@ -2085,10 +1668,10 @@

Instance variables

# if not, do the calculation and release the lock return obj_dict.setdefault(name, self.func(obj))
+
var recoverable_items_versions
-
Expand source code @@ -2108,10 +1691,10 @@

Instance variables

# if not, do the calculation and release the lock return obj_dict.setdefault(name, self.func(obj))
+
var root
-
Expand source code @@ -2131,10 +1714,23 @@

Instance variables

# if not, do the calculation and release the lock return obj_dict.setdefault(name, self.func(obj))
+
+
+
prop rules
+
+
+ +Expand source code + +
@property
+def rules(self):
+    """Return a list of Rule objects representing the rules that are set on this account."""
+    return list(GetInboxRules(account=self).call())
+
+

Return a list of Rule objects representing the rules that are set on this account.

var search_folders
-
Expand source code @@ -2154,10 +1750,10 @@

Instance variables

# if not, do the calculation and release the lock return obj_dict.setdefault(name, self.func(obj))
+
var sent
-
Expand source code @@ -2177,10 +1773,10 @@

Instance variables

# if not, do the calculation and release the lock return obj_dict.setdefault(name, self.func(obj))
+
var server_failures
-
Expand source code @@ -2200,10 +1796,10 @@

Instance variables

# if not, do the calculation and release the lock return obj_dict.setdefault(name, self.func(obj))
+
var sync_issues
-
Expand source code @@ -2223,10 +1819,10 @@

Instance variables

# if not, do the calculation and release the lock return obj_dict.setdefault(name, self.func(obj))
+
var tasks
-
Expand source code @@ -2246,10 +1842,10 @@

Instance variables

# if not, do the calculation and release the lock return obj_dict.setdefault(name, self.func(obj))
+
-
Expand source code @@ -2269,10 +1865,10 @@

Instance variables

# if not, do the calculation and release the lock return obj_dict.setdefault(name, self.func(obj))
+
var trash
-
Expand source code @@ -2292,10 +1888,31 @@

Instance variables

# if not, do the calculation and release the lock return obj_dict.setdefault(name, self.func(obj))
+
-
var voice_mail
+
prop version
+
+ +Expand source code + +
@property
+def version(self):
+    # We may need to override the default server version on a per-account basis because Microsoft may report one
+    # server version up-front but delegate account requests to an older backend server. Create a new instance to
+    # avoid changing the protocol version instance.
+    if self._version:
+        return self._version
+    with self._version_lock:
+        if self._version:
+            return self._version
+        self._version = self.protocol.version.copy()
+        return self._version
+
+
+
var voice_mail
+
Expand source code @@ -2315,6 +1932,7 @@

Instance variables

# if not, do the calculation and release the lock return obj_dict.setdefault(name, self.func(obj))
+

Methods

@@ -2323,12 +1941,6 @@

Methods

def bulk_archive(self, ids, to_folder, chunk_size=None)
-

Archive items to a folder in the archive mailbox. An archive mailbox must be enabled in order for this -to work.

-

:param ids: an iterable of either (id, changekey) tuples or Item objects. -:param to_folder: The destination folder of the archive operation -:param chunk_size: The number of items to send to the server in a single request (Default value = None)

-

:return: A list containing True or an exception instance in stable order of the requested items

Expand source code @@ -2343,21 +1955,28 @@

Methods

:return: A list containing True or an exception instance in stable order of the requested items """ - return list(self._consume_item_service(service_cls=ArchiveItem, items=ids, chunk_size=chunk_size, kwargs=dict( - to_folder=to_folder, - )) + return list( + self._consume_item_service( + service_cls=ArchiveItem, + items=ids, + chunk_size=chunk_size, + kwargs=dict( + to_folder=to_folder, + ), + ) )
+

Archive items to a folder in the archive mailbox. An archive mailbox must be enabled in order for this +to work.

+

:param ids: an iterable of either (id, changekey) tuples or Item objects. +:param to_folder: The destination folder of the archive operation +:param chunk_size: The number of items to send to the server in a single request (Default value = None)

+

:return: A list containing True or an exception instance in stable order of the requested items

def bulk_copy(self, ids, to_folder, chunk_size=None)
-

Copy items to another folder.

-

:param ids: an iterable of either (id, changekey) tuples or Item objects. -:param to_folder: The destination folder of the copy operation -:param chunk_size: The number of items to send to the server in a single request (Default value = None)

-

:return: Status for each send operation, in the same order as the input

Expand source code @@ -2371,32 +1990,34 @@

Methods

:return: Status for each send operation, in the same order as the input """ - return list(self._consume_item_service(service_cls=CopyItem, items=ids, chunk_size=chunk_size, kwargs=dict( - to_folder=to_folder, - ))) + return list( + self._consume_item_service( + service_cls=CopyItem, + items=ids, + chunk_size=chunk_size, + kwargs=dict( + to_folder=to_folder, + ), + ) + )
+

Copy items to another folder.

+

:param ids: an iterable of either (id, changekey) tuples or Item objects. +:param to_folder: The destination folder of the copy operation +:param chunk_size: The number of items to send to the server in a single request (Default value = None)

+

:return: Status for each send operation, in the same order as the input

-def bulk_create(self, folder, items, message_disposition='SaveOnly', send_meeting_invitations='SendToNone', chunk_size=None) +def bulk_create(self,
folder,
items,
message_disposition='SaveOnly',
send_meeting_invitations='SendToNone',
chunk_size=None)
-

Create new items in 'folder'.

-

:param folder: the folder to create the items in -:param items: an iterable of Item objects -:param message_disposition: only applicable to Message items. Possible values are specified in -MESSAGE_DISPOSITION_CHOICES (Default value = SAVE_ONLY) -:param send_meeting_invitations: only applicable to CalendarItem items. Possible values are specified in -SEND_MEETING_INVITATIONS_CHOICES (Default value = SEND_TO_NONE) -:param chunk_size: The number of items to send to the server in a single request (Default value = None)

-

:return: a list of either BulkCreateResult or exception instances in the same order as the input. The returned -BulkCreateResult objects are normal Item objects except they only contain the 'id' and 'changekey' -of the created item, and the 'id' of any attachments that were also created.

Expand source code -
def bulk_create(self, folder, items, message_disposition=SAVE_ONLY, send_meeting_invitations=SEND_TO_NONE,
-                chunk_size=None):
+
def bulk_create(
+    self, folder, items, message_disposition=SAVE_ONLY, send_meeting_invitations=SEND_TO_NONE, chunk_size=None
+):
     """Create new items in 'folder'.
 
     :param folder: the folder to create the items in
@@ -2413,42 +2034,56 @@ 

Methods

""" if isinstance(items, QuerySet): # bulk_create() on a queryset does not make sense because it returns items that have already been created - raise ValueError('Cannot bulk create items from a QuerySet') + raise ValueError("Cannot bulk create items from a QuerySet") log.debug( - 'Adding items for %s (folder %s, message_disposition: %s, send_meeting_invitations: %s)', + "Adding items for %s (folder %s, message_disposition: %s, send_meeting_invitations: %s)", self, folder, message_disposition, send_meeting_invitations, ) - return list(self._consume_item_service(service_cls=CreateItem, items=items, chunk_size=chunk_size, kwargs=dict( - folder=folder, - message_disposition=message_disposition, - send_meeting_invitations=send_meeting_invitations, - )))
+ return list( + self._consume_item_service( + service_cls=CreateItem, + items=items, + chunk_size=chunk_size, + kwargs=dict( + folder=folder, + message_disposition=message_disposition, + send_meeting_invitations=send_meeting_invitations, + ), + ) + )
+

Create new items in 'folder'.

+

:param folder: the folder to create the items in +:param items: an iterable of Item objects +:param message_disposition: only applicable to Message items. Possible values are specified in +MESSAGE_DISPOSITION_CHOICES (Default value = SAVE_ONLY) +:param send_meeting_invitations: only applicable to CalendarItem items. Possible values are specified in +SEND_MEETING_INVITATIONS_CHOICES (Default value = SEND_TO_NONE) +:param chunk_size: The number of items to send to the server in a single request (Default value = None)

+

:return: a list of either BulkCreateResult or exception instances in the same order as the input. The returned +BulkCreateResult objects are normal Item objects except they only contain the 'id' and 'changekey' +of the created item, and the 'id' of any attachments that were also created.

-def bulk_delete(self, ids, delete_type='HardDelete', send_meeting_cancellations='SendToNone', affected_task_occurrences='AllOccurrences', suppress_read_receipts=True, chunk_size=None) +def bulk_delete(self,
ids,
delete_type='HardDelete',
send_meeting_cancellations='SendToNone',
affected_task_occurrences='AllOccurrences',
suppress_read_receipts=True,
chunk_size=None)
-

Bulk delete items.

-

:param ids: an iterable of either (id, changekey) tuples or Item objects. -:param delete_type: the type of delete to perform. Possible values are specified in DELETE_TYPE_CHOICES -(Default value = HARD_DELETE) -:param send_meeting_cancellations: only applicable to CalendarItem. Possible values are specified in -SEND_MEETING_CANCELLATIONS_CHOICES. (Default value = SEND_TO_NONE) -:param affected_task_occurrences: only applicable for recurring Task items. Possible values are specified in -AFFECTED_TASK_OCCURRENCES_CHOICES. (Default value = ALL_OCCURRENCIES) -:param suppress_read_receipts: only supported from Exchange 2013. True or False. (Default value = True) -:param chunk_size: The number of items to send to the server in a single request (Default value = None)

-

:return: a list of either True or exception instances, in the same order as the input

Expand source code -
def bulk_delete(self, ids, delete_type=HARD_DELETE, send_meeting_cancellations=SEND_TO_NONE,
-                affected_task_occurrences=ALL_OCCURRENCIES, suppress_read_receipts=True, chunk_size=None):
+
def bulk_delete(
+    self,
+    ids,
+    delete_type=HARD_DELETE,
+    send_meeting_cancellations=SEND_TO_NONE,
+    affected_task_occurrences=ALL_OCCURRENCES,
+    suppress_read_receipts=True,
+    chunk_size=None,
+):
     """Bulk delete items.
 
     :param ids: an iterable of either (id, changekey) tuples or Item objects.
@@ -2457,40 +2092,49 @@ 

Methods

:param send_meeting_cancellations: only applicable to CalendarItem. Possible values are specified in SEND_MEETING_CANCELLATIONS_CHOICES. (Default value = SEND_TO_NONE) :param affected_task_occurrences: only applicable for recurring Task items. Possible values are specified in - AFFECTED_TASK_OCCURRENCES_CHOICES. (Default value = ALL_OCCURRENCIES) + AFFECTED_TASK_OCCURRENCES_CHOICES. (Default value = ALL_OCCURRENCES) :param suppress_read_receipts: only supported from Exchange 2013. True or False. (Default value = True) :param chunk_size: The number of items to send to the server in a single request (Default value = None) :return: a list of either True or exception instances, in the same order as the input """ log.debug( - 'Deleting items for %s (delete_type: %s, send_meeting_invitations: %s, affected_task_occurences: %s)', + "Deleting items for %s (delete_type: %s, send_meeting_invitations: %s, affected_task_occurrences: %s)", self, delete_type, send_meeting_cancellations, affected_task_occurrences, ) return list( - self._consume_item_service(service_cls=DeleteItem, items=ids, chunk_size=chunk_size, kwargs=dict( - delete_type=delete_type, - send_meeting_cancellations=send_meeting_cancellations, - affected_task_occurrences=affected_task_occurrences, - suppress_read_receipts=suppress_read_receipts, - )) + self._consume_item_service( + service_cls=DeleteItem, + items=ids, + chunk_size=chunk_size, + kwargs=dict( + delete_type=delete_type, + send_meeting_cancellations=send_meeting_cancellations, + affected_task_occurrences=affected_task_occurrences, + suppress_read_receipts=suppress_read_receipts, + ), + ) )
+

Bulk delete items.

+

:param ids: an iterable of either (id, changekey) tuples or Item objects. +:param delete_type: the type of delete to perform. Possible values are specified in DELETE_TYPE_CHOICES +(Default value = HARD_DELETE) +:param send_meeting_cancellations: only applicable to CalendarItem. Possible values are specified in +SEND_MEETING_CANCELLATIONS_CHOICES. (Default value = SEND_TO_NONE) +:param affected_task_occurrences: only applicable for recurring Task items. Possible values are specified in +AFFECTED_TASK_OCCURRENCES_CHOICES. (Default value = ALL_OCCURRENCES) +:param suppress_read_receipts: only supported from Exchange 2013. True or False. (Default value = True) +:param chunk_size: The number of items to send to the server in a single request (Default value = None)

+

:return: a list of either True or exception instances, in the same order as the input

def bulk_mark_as_junk(self, ids, is_junk, move_item, chunk_size=None)
-

Mark or un-mark message items as junk email and add or remove the sender from the blocked sender list.

-

:param ids: an iterable of either (id, changekey) tuples or Item objects. -:param is_junk: Whether the messages are junk or not -:param move_item: Whether to move the messages to the junk folder or not -:param chunk_size: The number of items to send to the server in a single request (Default value = None)

-

:return: A list containing the new IDs of the moved items, if items were moved, or True, or an exception -instance, in stable order of the requested items.

Expand source code @@ -2506,22 +2150,30 @@

Methods

:return: A list containing the new IDs of the moved items, if items were moved, or True, or an exception instance, in stable order of the requested items. """ - return list(self._consume_item_service(service_cls=MarkAsJunk, items=ids, chunk_size=chunk_size, kwargs=dict( - is_junk=is_junk, - move_item=move_item, - )))
+ return list( + self._consume_item_service( + service_cls=MarkAsJunk, + items=ids, + chunk_size=chunk_size, + kwargs=dict( + is_junk=is_junk, + move_item=move_item, + ), + ) + )
+

Mark or un-mark message items as junk email and add or remove the sender from the blocked sender list.

+

:param ids: an iterable of either (id, changekey) tuples or Item objects. +:param is_junk: Whether the messages are junk or not +:param move_item: Whether to move the messages to the junk folder or not +:param chunk_size: The number of items to send to the server in a single request (Default value = None)

+

:return: A list containing the new IDs of the moved items, if items were moved, or True, or an exception +instance, in stable order of the requested items.

def bulk_move(self, ids, to_folder, chunk_size=None)
-

Move items to another folder.

-

:param ids: an iterable of either (id, changekey) tuples or Item objects. -:param to_folder: The destination folder of the copy operation -:param chunk_size: The number of items to send to the server in a single request (Default value = None)

-

:return: The new IDs of the moved items, in the same order as the input. If 'to_folder' is a public folder or a -folder in a different mailbox, an empty list is returned.

Expand source code @@ -2536,21 +2188,28 @@

Methods

:return: The new IDs of the moved items, in the same order as the input. If 'to_folder' is a public folder or a folder in a different mailbox, an empty list is returned. """ - return list(self._consume_item_service(service_cls=MoveItem, items=ids, chunk_size=chunk_size, kwargs=dict( - to_folder=to_folder, - ))) + return list( + self._consume_item_service( + service_cls=MoveItem, + items=ids, + chunk_size=chunk_size, + kwargs=dict( + to_folder=to_folder, + ), + ) + )
+

Move items to another folder.

+

:param ids: an iterable of either (id, changekey) tuples or Item objects. +:param to_folder: The destination folder of the copy operation +:param chunk_size: The number of items to send to the server in a single request (Default value = None)

+

:return: The new IDs of the moved items, in the same order as the input. If 'to_folder' is a public folder or a +folder in a different mailbox, an empty list is returned.

def bulk_send(self, ids, save_copy=True, copy_to_folder=None, chunk_size=None)
-

Send existing draft messages. If requested, save a copy in 'copy_to_folder'.

-

:param ids: an iterable of either (id, changekey) tuples or Item objects. -:param save_copy: If true, saves a copy of the message (Default value = True) -:param copy_to_folder: If requested, save a copy of the message in this folder. Default is the Sent folder -:param chunk_size: The number of items to send to the server in a single request (Default value = None)

-

:return: Status for each send operation, in the same order as the input

Expand source code @@ -2570,35 +2229,40 @@

Methods

if save_copy and not copy_to_folder: copy_to_folder = self.sent # 'Sent' is default EWS behaviour return list( - self._consume_item_service(service_cls=SendItem, items=ids, chunk_size=chunk_size, kwargs=dict( - saved_item_folder=copy_to_folder, - )) + self._consume_item_service( + service_cls=SendItem, + items=ids, + chunk_size=chunk_size, + kwargs=dict( + saved_item_folder=copy_to_folder, + ), + ) )
+

Send existing draft messages. If requested, save a copy in 'copy_to_folder'.

+

:param ids: an iterable of either (id, changekey) tuples or Item objects. +:param save_copy: If true, saves a copy of the message (Default value = True) +:param copy_to_folder: If requested, save a copy of the message in this folder. Default is the Sent folder +:param chunk_size: The number of items to send to the server in a single request (Default value = None)

+

:return: Status for each send operation, in the same order as the input

-def bulk_update(self, items, conflict_resolution='AutoResolve', message_disposition='SaveOnly', send_meeting_invitations_or_cancellations='SendToNone', suppress_read_receipts=True, chunk_size=None) +def bulk_update(self,
items,
conflict_resolution='AutoResolve',
message_disposition='SaveOnly',
send_meeting_invitations_or_cancellations='SendToNone',
suppress_read_receipts=True,
chunk_size=None)
-

Bulk update existing items.

-

:param items: a list of (Item, fieldnames) tuples, where 'Item' is an Item object, and 'fieldnames' is a list -containing the attributes on this Item object that we want to be updated. -:param conflict_resolution: Possible values are specified in CONFLICT_RESOLUTION_CHOICES -(Default value = AUTO_RESOLVE) -:param message_disposition: only applicable to Message items. Possible values are specified in -MESSAGE_DISPOSITION_CHOICES (Default value = SAVE_ONLY) -:param send_meeting_invitations_or_cancellations: only applicable to CalendarItem items. Possible values are -specified in SEND_MEETING_INVITATIONS_AND_CANCELLATIONS_CHOICES (Default value = SEND_TO_NONE) -:param suppress_read_receipts: nly supported from Exchange 2013. True or False (Default value = True) -:param chunk_size: The number of items to send to the server in a single request (Default value = None)

-

:return: a list of either (id, changekey) tuples or exception instances, in the same order as the input

Expand source code -
def bulk_update(self, items, conflict_resolution=AUTO_RESOLVE, message_disposition=SAVE_ONLY,
-                send_meeting_invitations_or_cancellations=SEND_TO_NONE, suppress_read_receipts=True,
-                chunk_size=None):
+
def bulk_update(
+    self,
+    items,
+    conflict_resolution=AUTO_RESOLVE,
+    message_disposition=SAVE_ONLY,
+    send_meeting_invitations_or_cancellations=SEND_TO_NONE,
+    suppress_read_receipts=True,
+    chunk_size=None,
+):
     """Bulk update existing items.
 
     :param items: a list of (Item, fieldnames) tuples, where 'Item' is an Item object, and 'fieldnames' is a list
@@ -2618,30 +2282,94 @@ 

Methods

# fact, it could be dangerous if the queryset contains an '.only()'. This would wipe out certain fields # entirely. if isinstance(items, QuerySet): - raise ValueError('Cannot bulk update on a queryset') + raise ValueError("Cannot bulk update on a queryset") log.debug( - 'Updating items for %s (conflict_resolution %s, message_disposition: %s, send_meeting_invitations: %s)', + "Updating items for %s (conflict_resolution %s, message_disposition: %s, send_meeting_invitations: %s)", self, conflict_resolution, message_disposition, send_meeting_invitations_or_cancellations, ) - return list(self._consume_item_service(service_cls=UpdateItem, items=items, chunk_size=chunk_size, kwargs=dict( - conflict_resolution=conflict_resolution, - message_disposition=message_disposition, - send_meeting_invitations_or_cancellations=send_meeting_invitations_or_cancellations, - suppress_read_receipts=suppress_read_receipts, - )))
+ return list( + self._consume_item_service( + service_cls=UpdateItem, + items=items, + chunk_size=chunk_size, + kwargs=dict( + conflict_resolution=conflict_resolution, + message_disposition=message_disposition, + send_meeting_invitations_or_cancellations=send_meeting_invitations_or_cancellations, + suppress_read_receipts=suppress_read_receipts, + ), + ) + )
+
+

Bulk update existing items.

+

:param items: a list of (Item, fieldnames) tuples, where 'Item' is an Item object, and 'fieldnames' is a list +containing the attributes on this Item object that we want to be updated. +:param conflict_resolution: Possible values are specified in CONFLICT_RESOLUTION_CHOICES +(Default value = AUTO_RESOLVE) +:param message_disposition: only applicable to Message items. Possible values are specified in +MESSAGE_DISPOSITION_CHOICES (Default value = SAVE_ONLY) +:param send_meeting_invitations_or_cancellations: only applicable to CalendarItem items. Possible values are +specified in SEND_MEETING_INVITATIONS_AND_CANCELLATIONS_CHOICES (Default value = SEND_TO_NONE) +:param suppress_read_receipts: nly supported from Exchange 2013. True or False (Default value = True) +:param chunk_size: The number of items to send to the server in a single request (Default value = None)

+

:return: a list of either (id, changekey) tuples or exception instances, in the same order as the input

+
+
+def create_rule(self,
rule: Rule)
+
+
+
+ +Expand source code + +
def create_rule(self, rule: Rule):
+    """Create an Inbox rule.
+
+    :param rule: The rule to create. Must have at least 'display_name' set.
+    :return: None if success, else raises an error.
+    """
+    CreateInboxRule(account=self).get(rule=rule, remove_outlook_rule_blob=True)
+    # After creating the rule, query all rules,
+    # find the rule that was just created, and return its ID.
+    try:
+        rule.id = {i.display_name: i for i in GetInboxRules(account=self).call()}[rule.display_name].id
+    except KeyError:
+        raise ResponseMessageError(f"Failed to create rule ({rule.display_name})!")
+
+

Create an Inbox rule.

+

:param rule: The rule to create. Must have at least 'display_name' set. +:return: None if success, else raises an error.

+
+
+def delete_rule(self,
rule: Rule)
+
+
+
+ +Expand source code + +
def delete_rule(self, rule: Rule):
+    """Delete an Inbox rule.
+
+    :param rule: The rule to delete. Must have an ID.
+    :return: None if success, else raises an error.
+    """
+    if not rule.id:
+        raise ValueError("Rule must have an ID")
+    DeleteInboxRule(account=self).get(rule=rule)
+    rule.id = None
+

Delete an Inbox rule.

+

:param rule: The rule to delete. Must have an ID. +:return: None if success, else raises an error.

def export(self, items, chunk_size=None)
-

Return export strings of the given items.

-

:param items: An iterable containing the Items we want to export -:param chunk_size: The number of items to send to the server in a single request (Default value = None)

-

:return: A list of strings, the exported representation of the object

Expand source code @@ -2654,21 +2382,17 @@

Methods

:return: A list of strings, the exported representation of the object """ - return list( - self._consume_item_service(service_cls=ExportItems, items=items, chunk_size=chunk_size, kwargs={}) - ) + return list(self._consume_item_service(service_cls=ExportItems, items=items, chunk_size=chunk_size, kwargs={}))
+

Return export strings of the given items.

+

:param items: An iterable containing the Items we want to export +:param chunk_size: The number of items to send to the server in a single request (Default value = None)

+

:return: A list of strings, the exported representation of the object

def fetch(self, ids, folder=None, only_fields=None, chunk_size=None)
-

Fetch items by ID.

-

:param ids: an iterable of either (id, changekey) tuples or Item objects. -:param folder: used for validating 'only_fields' (Default value = None) -:param only_fields: A list of string or FieldPath items specifying the fields to fetch. Default to all fields -:param chunk_size: The number of items to send to the server in a single request (Default value = None)

-

:return: A generator of Item objects, in the same order as the input

Expand source code @@ -2696,22 +2420,31 @@

Methods

for field in only_fields: validation_folder.validate_item_field(field=field, version=self.version) # Remove ItemId and ChangeKey. We get them unconditionally - additional_fields = {f for f in validation_folder.normalize_fields(fields=only_fields) - if not f.field.is_attribute} + additional_fields = { + f for f in validation_folder.normalize_fields(fields=only_fields) if not f.field.is_attribute + } # Always use IdOnly here, because AllProperties doesn't actually get *all* properties - yield from self._consume_item_service(service_cls=GetItem, items=ids, chunk_size=chunk_size, kwargs=dict( + yield from self._consume_item_service( + service_cls=GetItem, + items=ids, + chunk_size=chunk_size, + kwargs=dict( additional_fields=additional_fields, shape=ID_ONLY, - )) + ), + )
+

Fetch items by ID.

+

:param ids: an iterable of either (id, changekey) tuples or Item objects. +:param folder: used for validating 'only_fields' (Default value = None) +:param only_fields: A list of string or FieldPath items specifying the fields to fetch. Default to all fields +:param chunk_size: The number of items to send to the server in a single request (Default value = None)

+

:return: A generator of Item objects, in the same order as the input

def fetch_personas(self, ids)
-

Fetch personas by ID.

-

:param ids: an iterable of either (id, changekey) tuples or Persona objects. -:return: A generator of Persona objects, in the same order as the input

Expand source code @@ -2730,30 +2463,189 @@

Methods

# We accept generators, so it's not always convenient for caller to know up-front if 'ids' is empty. Allow # empty 'ids' and return early. return - # GetPersona only accepts one persona ID per request. Crazy. - svc = GetPersona(account=self) - for i in ids: - yield svc.call(persona=i) + yield from GetPersona(account=self).call(personas=ids) +
+

Fetch personas by ID.

+

:param ids: an iterable of either (id, changekey) tuples or Persona objects. +:return: A generator of Persona objects, in the same order as the input

+
+
+def pull_subscription(self, **kwargs) +
+
+
+ +Expand source code + +
def pull_subscription(self, **kwargs):
+    return PullSubscription(target=self, **kwargs)
+
+
+
+
+def push_subscription(self, **kwargs) +
+
+
+ +Expand source code + +
def push_subscription(self, **kwargs):
+    return PushSubscription(target=self, **kwargs)
+
+
+
+
+def set_rule(self,
rule: Rule)
+
+
+
+ +Expand source code + +
def set_rule(self, rule: Rule):
+    """Modify an Inbox rule.
+
+    :param rule: The rule to set. Must have an ID.
+    :return: None if success, else raises an error.
+    """
+    SetInboxRule(account=self).get(rule=rule)
+
+

Modify an Inbox rule.

+

:param rule: The rule to set. Must have an ID. +:return: None if success, else raises an error.

+
+
+def streaming_subscription(self, **kwargs) +
+
+
+ +Expand source code + +
def streaming_subscription(self, **kwargs):
+    return StreamingSubscription(target=self, **kwargs)
+
+
+
+
+def subscribe_to_pull(self, event_types=None, watermark=None, timeout=60) +
+
+
+ +Expand source code + +
def subscribe_to_pull(self, event_types=None, watermark=None, timeout=60):
+    """Create a pull subscription.
+
+    :param event_types: List of event types to subscribe to. Possible values defined in SubscribeToPull.EVENT_TYPES
+    :param watermark: An event bookmark as returned by some sync services
+    :param timeout: Timeout of the subscription, in minutes. Timeout is reset when the server receives a
+    GetEvents request for this subscription.
+    :return: The subscription ID and a watermark
+    """
+    if event_types is None:
+        event_types = SubscribeToPull.EVENT_TYPES
+    return SubscribeToPull(account=self).get(
+        folders=None,
+        event_types=event_types,
+        watermark=watermark,
+        timeout=timeout,
+    )
+
+

Create a pull subscription.

+

:param event_types: List of event types to subscribe to. Possible values defined in SubscribeToPull.EVENT_TYPES +:param watermark: An event bookmark as returned by some sync services +:param timeout: Timeout of the subscription, in minutes. Timeout is reset when the server receives a +GetEvents request for this subscription. +:return: The subscription ID and a watermark

+
+
+def subscribe_to_push(self, callback_url, event_types=None, watermark=None, status_frequency=1) +
+
+
+ +Expand source code + +
def subscribe_to_push(self, callback_url, event_types=None, watermark=None, status_frequency=1):
+    """Create a push subscription.
+
+    :param callback_url: A client-defined URL that the server will call
+    :param event_types: List of event types to subscribe to. Possible values defined in SubscribeToPush.EVENT_TYPES
+    :param watermark: An event bookmark as returned by some sync services
+    :param status_frequency: The frequency, in minutes, that the callback URL will be called with.
+    :return: The subscription ID and a watermark
+    """
+    if event_types is None:
+        event_types = SubscribeToPush.EVENT_TYPES
+    return SubscribeToPush(account=self).get(
+        folders=None,
+        event_types=event_types,
+        watermark=watermark,
+        status_frequency=status_frequency,
+        url=callback_url,
+    )
+
+

Create a push subscription.

+

:param callback_url: A client-defined URL that the server will call +:param event_types: List of event types to subscribe to. Possible values defined in SubscribeToPush.EVENT_TYPES +:param watermark: An event bookmark as returned by some sync services +:param status_frequency: The frequency, in minutes, that the callback URL will be called with. +:return: The subscription ID and a watermark

+
+
+def subscribe_to_streaming(self, event_types=None) +
+
+
+ +Expand source code + +
def subscribe_to_streaming(self, event_types=None):
+    """Create a streaming subscription.
+
+    :param event_types: List of event types to subscribe to. Possible values defined in SubscribeToPush.EVENT_TYPES
+    :return: The subscription ID
+    """
+    if event_types is None:
+        event_types = SubscribeToStreaming.EVENT_TYPES
+    return SubscribeToStreaming(account=self).get(folders=None, event_types=event_types)
+
+

Create a streaming subscription.

+

:param event_types: List of event types to subscribe to. Possible values defined in SubscribeToPush.EVENT_TYPES +:return: The subscription ID

+
+
+def unsubscribe(self, subscription_id) +
+
+
+ +Expand source code + +
def unsubscribe(self, subscription_id):
+    """Unsubscribe. Only applies to pull and streaming notifications.
+
+    :param subscription_id: A subscription ID as acquired by .subscribe_to_[pull|streaming]()
+    :return: True
+
+    This method doesn't need the current collection instance, but it makes sense to keep the method along the other
+    sync methods.
+    """
+    return Unsubscribe(account=self).get(subscription_id=subscription_id)
+

Unsubscribe. Only applies to pull and streaming notifications.

+

:param subscription_id: A subscription ID as acquired by .subscribe_to_pull|streaming +:return: True

+

This method doesn't need the current collection instance, but it makes sense to keep the method along the other +sync methods.

def upload(self, data, chunk_size=None)
-

Upload objects retrieved from an export to the given folders.

-

:param data: An iterable of tuples containing the folder we want to upload the data to and the string outputs of -exports. If you want to update items instead of create, the data must be a tuple of -(ItemId, is_associated, data) values. -:param chunk_size: The number of items to send to the server in a single request (Default value = None)

-

:return: A list of tuples with the new ids and changekeys

-

Example: -account.upload([ -(account.inbox, "AABBCC…"), -(account.inbox, (ItemId('AA', 'BB'), False, "XXYYZZ…")), -(account.inbox, (('CC', 'DD'), None, "XXYYZZ…")), -(account.calendar, "ABCXYZ…"), -]) --> [("idA", "changekey"), ("idB", "changekey"), ("idC", "changekey")]

Expand source code @@ -2778,63 +2670,97 @@

Methods

-> [("idA", "changekey"), ("idB", "changekey"), ("idC", "changekey")] """ items = ((f, (None, False, d) if isinstance(d, str) else d) for f, d in data) - return list( - self._consume_item_service(service_cls=UploadItems, items=items, chunk_size=chunk_size, kwargs={}) - ) + return list(self._consume_item_service(service_cls=UploadItems, items=items, chunk_size=chunk_size, kwargs={}))
+

Upload objects retrieved from an export to the given folders.

+

:param data: An iterable of tuples containing the folder we want to upload the data to and the string outputs of +exports. If you want to update items instead of create, the data must be a tuple of +(ItemId, is_associated, data) values. +:param chunk_size: The number of items to send to the server in a single request (Default value = None)

+

:return: A list of tuples with the new ids and changekeys

+

Example: +account.upload([ +(account.inbox, "AABBCC…"), +(account.inbox, (ItemId('AA', 'BB'), False, "XXYYZZ…")), +(account.inbox, (('CC', 'DD'), None, "XXYYZZ…")), +(account.calendar, "ABCXYZ…"), +]) +-> [("idA", "changekey"), ("idB", "changekey"), ("idC", "changekey")]

class Identity -(primary_smtp_address=None, smtp_address=None, upn=None, sid=None) +(**kwargs)
-

Contains information that uniquely identifies an account. Currently only used for SOAP impersonation headers.

-

:param primary_smtp_address: The primary email address associated with the account (Default value = None) -:param smtp_address: The (non-)primary email address associated with the account (Default value = None) -:param upn: (Default value = None) -:param sid: (Default value = None) -:return:

Expand source code -
class Identity:
+
class Identity(EWSElement):
     """Contains information that uniquely identifies an account. Currently only used for SOAP impersonation headers."""
 
-    def __init__(self, primary_smtp_address=None, smtp_address=None, upn=None, sid=None):
-        """
-
-        :param primary_smtp_address: The primary email address associated with the account (Default value = None)
-        :param smtp_address: The (non-)primary email address associated with the account (Default value = None)
-        :param upn: (Default value = None)
-        :param sid: (Default value = None)
-        :return:
-        """
-        self.primary_smtp_address = primary_smtp_address
-        self.smtp_address = smtp_address
-        self.upn = upn
-        self.sid = sid
-
-    def __eq__(self, other):
-        for k in self.__dict__:
-            if getattr(self, k) != getattr(other, k):
-                return False
-        return True
+    ELEMENT_NAME = "ConnectingSID"
 
-    def __hash__(self):
-        return hash(repr(self))
-
-    def __repr__(self):
-        return self.__class__.__name__ + repr((self.primary_smtp_address, self.smtp_address, self.upn, self.sid))
+ # We have multiple options for uniquely identifying the user. Here's a prioritized list in accordance with + # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/connectingsid + sid = TextField(field_uri="SID") + upn = TextField(field_uri="PrincipalName") + smtp_address = TextField(field_uri="SmtpAddress") # The (non-)primary email address for the account + primary_smtp_address = TextField(field_uri="PrimarySmtpAddress") # The primary email address for the account
+

Contains information that uniquely identifies an account. Currently only used for SOAP impersonation headers.

+

Ancestors

+ +

Class variables

+
+
var ELEMENT_NAME
+
+
+
+
var FIELDS
+
+
+
+
+

Instance variables

+
+
var primary_smtp_address
+
+
+
+
var sid
+
+
+
+
var smtp_address
+
+
+
+
var upn
+
+
+
+
+

Inherited members

+